<commit_before>'use strict'

require('./support/common')
const _ = require('lodash');
const destroyAndCreate = require('./support/db').destroyAndCreate
const jobqueue = require('../lib')

describe('Job Queue', function() {

    beforeEach(function() {
        return destroyAndCreate()
    })

    afterEach(function() {
        return jobqueue.disconnect()
    })

    describe('addJob should throw an exception when called with invalid arguments', function() {
        this.validJob = {
            type: 'sendMail',
            scheduledFor: new Date(),
            maxAttempts: 1,
            data: {}
        }

        it('should reject non-objects', function() {
            expect(() => jobqueue.addJob(1)).to.throw(TypeError)
        })

        it('should reject scheduledFor specified with non-date', function() {
            var job = _.extend({}, this.validJob, {scheduledFor: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

        it('should reject invalid type', function() {
            var job = _.extend({}, this.validJob, {type: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

        it('should reject invalid maxAttempts', function() {
            var job = _.extend({}, this.validJob, {maxAttempts: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)

            job = _.extend({}, this.validJob, {maxAttempts: -123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

    })

    it('should accept a new job and then process it once', function() {

        function jobHandler(job, jobqueue) {
            // send email to job.data.recipient, message=job.data.message
            return job.finish()
        }

        var spy = sinon.spy(jobHandler)

        // setup a single job handler
        jobqueue.setHandlers({
            sendEmail: spy
        })

        var job = {
            type: 'sendEmail',
            data: {
                recipient: 'user@example.com',
                message: 'HELLO'
            }
        }

        // add a job
        return jobqueue.addJob(job).then(function() {
            // process the job
            return jobqueue.processNextJob().then(function() {

                // check the handler was called correctly
                expect(spy.calledOnce).to.be.true
                expect(spy.getCall(0).args[0].data).to.deep.equal(job.data)

                // try and process the job again (should fail)
                return expect(jobqueue.processNextJob()).to.eventually.be.rejected
            })
        })
    })

    it('should mark a job as failed if it throws an exception', function() {
        jobqueue.setHandlers({
            failingJob: function() {
                throw new Error('error message')
            }
        })

        var job = {
            type: 'failingJob',
            maxAttempts: 1
        }

        return jobqueue.addJob(job)
        .then(jobqueue.processNextJob)
        .then(jobqueue.getFailedJobs)
        .then(function(jobs) {

            expect(jobs.length).to.equal(1)
            var job = jobs[0]
            expect(job.failedAttempts).to.equal(1)
            expect(job.lastFailureMessage).to.equal('error message')
        })
    })

    it('should retry a failed job `maxAttempts` times', function() {
        jobqueue.setHandlers({
            failingJob: function() {
                throw new Error('error message')
            }
        })
        var job = {
            type: 'failingJob',
            maxAttempts: 5
        }
        return jobqueue.addJob(job)
        .then(function() {
            var loop = function() {
                return jobqueue.processNextJob().then(function() {
                    return loop()
                })
            }
            return loop()
        }).catch(function(e) {
            if (!(e instanceof jobqueue.errors.JobQueueEmpty)) {
                throw e
            }
        }).then(function() {
            // job has been run many times and should have reached complete failure
            // check that is true
            return jobqueue.getFailedJobs().then(function(jobs) {
                expect(jobs.length).to.equal(1)
                var job = jobs[0]
                expect(job.state).to.equal('failed')
                expect(job.failedAttempts).to.equal(5)
                expect(job.maxAttempts).to.equal(5)
                expect(job.lastFailureMessage).to.equal('error message')
            })
        })
    })

    it('should fail a job when job.fail() is called', function() {
        jobqueue.setHandlers({
            failingJob: function(job) {
                return job.fail('error message')
            }
        })
        var job = {
            type: 'failingJob'
        }
        return jobqueue.addJob(job)
        .then(function() {
            var loop = function() {
                return jobqueue.processNextJob().then(function() {
                    return loop()
                })
            }
            return loop()
        }).catch(function(e) {
            if (!(e instanceof jobqueue.errors.JobQueueEmpty)) {
                throw e
            }
        }).then(function() {
            // job has been run many times and should have reached complete failure
            // check that is true
            return jobqueue.getFailedJobs().then(function(jobs) {
                expect(jobs.length).to.equal(1)
                var job = jobs[0]
                expect(job.state).to.equal('failed')
                expect(job.failedAttempts).to.equal(1)
                expect(job.maxAttempts).to.equal(1)
                expect(job.lastFailureMessage).to.equal('error message')
            })
        })

    })

    it('should correctly reschedule a job', function() {
        jobqueue.setHandlers({
            rescheduleJob: function(job) {
                return job.reschedule(new Date())
            }
        })

        var job = {
            type: 'rescheduleJob',
        }

        return jobqueue.addJob(job)
        .then(jobqueue.processNextJob)
        .then(jobqueue.processNextJob)
        .then(jobqueue.waitingCount).then(function(count) {
            expect(count).to.equal(1)
        })
    })

    it('should only process jobs with handlers available', function() {
        var job = {
            type: 'sendmail',
        }
        return jobqueue.addJob(job)
        .then(function() {
            // processNextJob should throw the error JobQueueEmpty
            // because we haven't setup any handlers, so it doesn't see the job
            return expect(jobqueue.processNextJob()).to.eventually.be.rejectedWith(jobqueue.errors.JobQueueEmpty)
        })
    })
})

<commit_msg>reuse existing database during tests<commit_after>'use strict'

require('./support/common')
const _ = require('lodash');
const destroyAndCreate = require('./support/db').destroyAndCreate
const jobqueue = require('../lib')

describe('Job Queue', function() {

    before(function() {
        // drop and create the database and install schema
        return destroyAndCreate()
    })

    beforeEach(function() {
        return jobqueue.clearAllJobs()
    })

    describe('addJob should throw an exception when called with invalid arguments', function() {
        this.validJob = {
            type: 'sendMail',
            scheduledFor: new Date(),
            maxAttempts: 1,
            data: {}
        }

        it('should reject non-objects', function() {
            expect(() => jobqueue.addJob(1)).to.throw(TypeError)
        })

        it('should reject scheduledFor specified with non-date', function() {
            var job = _.extend({}, this.validJob, {scheduledFor: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

        it('should reject invalid type', function() {
            var job = _.extend({}, this.validJob, {type: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

        it('should reject invalid maxAttempts', function() {
            var job = _.extend({}, this.validJob, {maxAttempts: 123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)

            job = _.extend({}, this.validJob, {maxAttempts: -123})
            expect(() => jobqueue.addJob(job)).to.throw(TypeError)
        })

    })

    it('should accept a new job and then process it once', function() {

        function jobHandler(job, jobqueue) {
            // send email to job.data.recipient, message=job.data.message
            return job.finish()
        }

        var spy = sinon.spy(jobHandler)

        // setup a single job handler
        jobqueue.setHandlers({
            sendEmail: spy
        })

        var job = {
            type: 'sendEmail',
            data: {
                recipient: 'user@example.com',
                message: 'HELLO'
            }
        }

        // add a job
        return jobqueue.addJob(job).then(function() {
            // process the job
            return jobqueue.processNextJob().then(function() {

                // check the handler was called correctly
                expect(spy.calledOnce).to.be.true
                expect(spy.getCall(0).args[0].data).to.deep.equal(job.data)

                // try and process the job again (should fail)
                return expect(jobqueue.processNextJob()).to.eventually.be.rejected
            })
        })
    })

    it('should mark a job as failed if it throws an exception', function() {
        jobqueue.setHandlers({
            failingJob: function() {
                throw new Error('error message')
            }
        })

        var job = {
            type: 'failingJob',
            maxAttempts: 1
        }

        return jobqueue.addJob(job)
        .then(jobqueue.processNextJob)
        .then(jobqueue.getFailedJobs)
        .then(function(jobs) {

            expect(jobs.length).to.equal(1)
            var job = jobs[0]
            expect(job.failedAttempts).to.equal(1)
            expect(job.lastFailureMessage).to.equal('error message')
        })
    })

    it('should retry a failed job `maxAttempts` times', function() {
        jobqueue.setHandlers({
            failingJob: function() {
                throw new Error('error message')
            }
        })
        var job = {
            type: 'failingJob',
            maxAttempts: 5
        }
        return jobqueue.addJob(job)
        .then(function() {
            var loop = function() {
                return jobqueue.processNextJob().then(function() {
                    return loop()
                })
            }
            return loop()
        }).catch(function(e) {
            if (!(e instanceof jobqueue.errors.JobQueueEmpty)) {
                throw e
            }
        }).then(function() {
            // job has been run many times and should have reached complete failure
            // check that is true
            return jobqueue.getFailedJobs().then(function(jobs) {
                expect(jobs.length).to.equal(1)
                var job = jobs[0]
                expect(job.state).to.equal('failed')
                expect(job.failedAttempts).to.equal(5)
                expect(job.maxAttempts).to.equal(5)
                expect(job.lastFailureMessage).to.equal('error message')
            })
        })
    })

    it('should fail a job when job.fail() is called', function() {
        jobqueue.setHandlers({
            failingJob: function(job) {
                return job.fail('error message')
            }
        })
        var job = {
            type: 'failingJob'
        }
        return jobqueue.addJob(job)
        .then(function() {
            var loop = function() {
                return jobqueue.processNextJob().then(function() {
                    return loop()
                })
            }
            return loop()
        }).catch(function(e) {
            if (!(e instanceof jobqueue.errors.JobQueueEmpty)) {
                throw e
            }
        }).then(function() {
            // job has been run many times and should have reached complete failure
            // check that is true
            return jobqueue.getFailedJobs().then(function(jobs) {
                expect(jobs.length).to.equal(1)
                var job = jobs[0]
                expect(job.state).to.equal('failed')
                expect(job.failedAttempts).to.equal(1)
                expect(job.maxAttempts).to.equal(1)
                expect(job.lastFailureMessage).to.equal('error message')
            })
        })

    })

    it('should correctly reschedule a job', function() {
        jobqueue.setHandlers({
            rescheduleJob: function(job) {
                return job.reschedule(new Date())
            }
        })

        var job = {
            type: 'rescheduleJob',
        }

        return jobqueue.addJob(job)
        .then(jobqueue.processNextJob)
        .then(jobqueue.processNextJob)
        .then(jobqueue.waitingCount).then(function(count) {
            expect(count).to.equal(1)
        })
    })

    it('should only process jobs with handlers available', function() {
        var job = {
            type: 'sendmail',
        }
        return jobqueue.addJob(job)
        .then(function() {
            // processNextJob should throw the error JobQueueEmpty
            // because we haven't setup any handlers, so it doesn't see the job
            return expect(jobqueue.processNextJob()).to.eventually.be.rejectedWith(jobqueue.errors.JobQueueEmpty)
        })
    })
})

<|endoftext|>


<commit_before>import React, { Component } from "react"; // eslint-disable-line no-unused-vars
import Fuse from "fuse.js";

class Search extends Component {
	constructor() {
		super();

		this.state = {
			query: "",
			fuse: false
		};
	}

	componentWillMount() {
		let fuse = new Fuse( this.props.profiles, {
			keys: [
				"name",
				"email",
				"phone",
				"github_user"
			]
		} );

		this.setState( { fuse } );
	}

	get valueChange() {
		return event => this.searchProfiles( event.target.value );
	}

	searchProfiles( query ) {
		let profiles;

		if ( query.trim() !== "" ) {
			profiles = this.state.fuse.search( query );
		} else {
			profiles = this.props.profiles;
		}

		this.setState( {
			query
		} );

		this.props.setVisible( profiles );
	}

	render() {
		return (

				<input name="q" onChange={ this.valueChange } value={ this.state.query } />

		);
	}

	static get propTypes() {
		return {
			profiles: React.PropTypes.array,
			setVisible: React.PropTypes.func,
			query: React.PropTypes.string
		};
	}
}

export default Search;
<commit_msg>remove email from search items<commit_after>import React, { Component } from "react"; // eslint-disable-line no-unused-vars
import Fuse from "fuse.js";

class Search extends Component {
	constructor() {
		super();

		this.state = {
			query: "",
			fuse: false
		};
	}

	componentWillMount() {
		let fuse = new Fuse( this.props.profiles, {
			keys: [
				"name",
				"phone",
				"github_user"
			]
		} );

		this.setState( { fuse } );
	}

	get valueChange() {
		return event => this.searchProfiles( event.target.value );
	}

	searchProfiles( query ) {
		let profiles;

		if ( query.trim() !== "" ) {
			profiles = this.state.fuse.search( query );
		} else {
			profiles = this.props.profiles;
		}

		this.setState( {
			query
		} );

		this.props.setVisible( profiles );
	}

	render() {
		return (

				<input name="q" onChange={ this.valueChange } value={ this.state.query } />

		);
	}

	static get propTypes() {
		return {
			profiles: React.PropTypes.array,
			setVisible: React.PropTypes.func,
			query: React.PropTypes.string
		};
	}
}

export default Search;
<|endoftext|>


<commit_before>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Dominik
// Date:      25.11.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

m_require('core/datastore/validator.js')

/**
 * @class
 *
 * Validates a given date. Validates whether it is possible to create a {@link M.Date} (then valid) or not (then invalid).
 *
 * @extends M.Validator
 */
M.DateValidator = M.Validator.extend(
/** @scope M.DateValidator.prototype */ {

    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.DateValidator',

    /**
     * Validation method. First checks if value is not null, undefined or an empty string and then tries to create a {@link M.Date} with it.
     * Pushes different validation errors depending on where the validator is used: in the view or in the model.
     *
     * @param {Object} obj Parameter object. Contains the value to be validated, the {@link M.ModelAttribute} object of the property and the model record's id.
     * @returns {Boolean} Indicating whether validation passed (YES|true) or not (NO|false).
     */
    validate: function(obj, key) {
        console.log(obj);
        console.log(obj.value.type);
        if(obj.value.type !== 'M.Date' && (obj.value === null || obj.value === undefined || obj.value === '' || !M.Date.create(obj.value))) {
            if(obj.isView) {
                this.validationErrors.push({
                    msg: this.msg ? this.msg : key + ' is not a valid date.',
                    viewId: obj.id,
                    validator: 'DATE',
                    onSuccess: obj.onSuccess,
                    onError: obj.onError
                });
            } else {
                this.validationErrors.push({
                    msg: this.msg ? this.msg : obj.property + ' is not a valid date.',
                    modelId: obj.modelId,
                    property: obj.property,
                    validator: 'DATE',
                    onSuccess: obj.onSuccess,
                    onError: obj.onError
                });
            }
            return NO;
        }
        return YES;
    }

});<commit_msg>check if value type exists<commit_after>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Dominik
// Date:      25.11.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

m_require('core/datastore/validator.js')

/**
 * @class
 *
 * Validates a given date. Validates whether it is possible to create a {@link M.Date} (then valid) or not (then invalid).
 *
 * @extends M.Validator
 */
M.DateValidator = M.Validator.extend(
/** @scope M.DateValidator.prototype */ {

    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.DateValidator',

    /**
     * Validation method. First checks if value is not null, undefined or an empty string and then tries to create a {@link M.Date} with it.
     * Pushes different validation errors depending on where the validator is used: in the view or in the model.
     *
     * @param {Object} obj Parameter object. Contains the value to be validated, the {@link M.ModelAttribute} object of the property and the model record's id.
     * @returns {Boolean} Indicating whether validation passed (YES|true) or not (NO|false).
     */
    validate: function(obj, key) {
        if(obj.value.type && obj.value.type !== 'M.Date' && (obj.value === null || obj.value === undefined || obj.value === '' || !M.Date.create(obj.value))) {
            if(obj.isView) {
                this.validationErrors.push({
                    msg: this.msg ? this.msg : key + ' is not a valid date.',
                    viewId: obj.id,
                    validator: 'DATE',
                    onSuccess: obj.onSuccess,
                    onError: obj.onError
                });
            } else {
                this.validationErrors.push({
                    msg: this.msg ? this.msg : obj.property + ' is not a valid date.',
                    modelId: obj.modelId,
                    property: obj.property,
                    validator: 'DATE',
                    onSuccess: obj.onSuccess,
                    onError: obj.onError
                });
            }
            return NO;
        }
        return YES;
    }

});<|endoftext|>


<commit_before>import React from 'react';
import { Step } from 'semantic-ui-react';

const StepBar = ({ steps, currentStepIndex, goToStep }) => {
  steps.forEach((step, index) => {
    step.completed = index < currentStepIndex
    step.active = index === (currentStepIndex - 1)
    step.onClick = (e) => { goToStep(index + 1) }
    step.key = index
  })

  return (<Step.Group className='six' size='mini' ordered items={steps} />)
}

export default StepBar<commit_msg>Removes non-standard `form` from `<Step.Group>` creation<commit_after>import React from 'react';
import { Step } from 'semantic-ui-react';

const StepBar = ({ steps, currentStepIndex, goToStep }) => {

  var cleanSteps = [];

  steps.forEach((step, index) => {
    var newStep = { title: step.title }
    newStep.completed = index < currentStepIndex
    newStep.active = index === (currentStepIndex - 1)
    newStep.onClick = (e) => { goToStep(index + 1) }
    newStep.key = index
    cleanSteps[ index ] = newStep;
  })

  return (<Step.Group className='six' size='mini' ordered items={cleanSteps} />)
}

export default StepBar<|endoftext|>


<commit_before>import {
  NOTIFICATIONS_MOUNT,
  NOTIFICATIONS_UNMOUNT,
  NOTIFICATIONS_SET_VISIBILITY,
  NOTIFICATIONS_UPDATE,
  NOTIFICATIONS_EXPAND_SUCCESS,
  NOTIFICATIONS_EXPAND_REQUEST,
  NOTIFICATIONS_EXPAND_FAIL,
  NOTIFICATIONS_FILTER_SET,
  NOTIFICATIONS_CLEAR,
  NOTIFICATIONS_SCROLL_TOP,
  NOTIFICATIONS_LOAD_PENDING,
  NOTIFICATIONS_DELETE_MARKED_REQUEST,
  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
  NOTIFICATION_MARK_FOR_DELETE,
  NOTIFICATIONS_DELETE_MARKED_FAIL,
  NOTIFICATIONS_ENTER_CLEARING_MODE,
  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
  NOTIFICATIONS_MARK_AS_READ,
} from 'flavours/glitch/actions/notifications';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
  FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
  FOLLOW_REQUEST_REJECT_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import {
  MARKERS_FETCH_SUCCESS,
} from 'flavours/glitch/actions/markers';
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap({
  pendingItems: ImmutableList(),
  items: ImmutableList(),
  hasMore: true,
  top: false,
  mounted: 0,
  unread: 0,
  lastReadId: '0',
  readMarkerId: '0',
  isLoading: false,
  cleaningMode: false,
  isTabVisible: true,
  // notification removal mark of new notifs loaded whilst cleaningMode is true.
  markNewForDelete: false,
});

const notificationToMap = (state, notification) => ImmutableMap({
  id: notification.id,
  type: notification.type,
  account: notification.account.id,
  markedForDelete: state.get('markNewForDelete'),
  status: notification.status ? notification.status.id : null,
});

const normalizeNotification = (state, notification, usePendingItems) => {
  const top = state.get('top');

  if (usePendingItems || !state.get('pendingItems').isEmpty()) {
    return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1);
  }

  if (shouldCountUnreadNotifications(state)) {
    state = state.update('unread', unread => unread + 1);
  } else {
    state = state.set('lastReadId', notification.id);
  }

  return state.update('items', list => {
    if (top && list.size > 40) {
      list = list.take(20);
    }

    return list.unshift(notificationToMap(state, notification));
  });
};

const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
  const lastReadId = state.get('lastReadId');
  let items = ImmutableList();

  notifications.forEach((n, i) => {
    items = items.set(i, notificationToMap(state, n));
  });

  return state.withMutations(mutable => {
    if (!items.isEmpty()) {
      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());

      mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
        const lastIndex = 1 + list.findLastIndex(
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
        );

        const firstIndex = 1 + list.take(lastIndex).findLastIndex(
          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
        );

        return list.take(firstIndex).concat(items, list.skip(lastIndex));
      });
    }

    if (!next) {
      mutable.set('hasMore', false);
    }

    if (shouldCountUnreadNotifications(state)) {
      mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0));
    } else {
      const mostRecent = items.find(item => item !== null);
      if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
        mutable.set('lastReadId', mostRecent.get('id'));
      }
    }

    mutable.set('isLoading', false);
  });
};

const filterNotifications = (state, accountIds, type) => {
  const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
  return state.update('items', helper).update('pendingItems', helper);
};

const clearUnread = (state) => {
  state = state.set('unread', state.get('pendingItems').size);
  const lastNotification = state.get('items').find(item => item !== null);
  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
};

const updateTop = (state, top) => {
  state = state.set('top', top);

  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }

  return state;
};

const deleteByStatus = (state, statusId) => {
  const lastReadId = state.get('lastReadId');

  if (shouldCountUnreadNotifications(state)) {
    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
    state = state.update('unread', unread => unread - deletedUnread.size);
  }

  const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
  const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
  state = state.update('unread', unread => unread - deletedUnread.size);
  return state.update('items', helper).update('pendingItems', helper);
};

const markForDelete = (state, notificationId, yes) => {
  return state.update('items', list => list.map(item => {
    if(item.get('id') === notificationId) {
      return item.set('markedForDelete', yes);
    } else {
      return item;
    }
  }));
};

const markAllForDelete = (state, yes) => {
  return state.update('items', list => list.map(item => {
    if(yes !== null) {
      return item.set('markedForDelete', yes);
    } else {
      return item.set('markedForDelete', !item.get('markedForDelete'));
    }
  }));
};

const unmarkAllForDelete = (state) => {
  return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
};

const deleteMarkedNotifs = (state) => {
  return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
};

const updateMounted = (state) => {
  state = state.update('mounted', count => count + 1);
  if (!shouldCountUnreadNotifications(state)) {
    state = state.set('readMarkerId', state.get('lastReadId'));
    state = clearUnread(state);
  }
  return state;
};

const updateVisibility = (state, visibility) => {
  state = state.set('isTabVisible', visibility);
  if (!shouldCountUnreadNotifications(state)) {
    state = state.set('readMarkerId', state.get('lastReadId'));
    state = clearUnread(state);
  }
  return state;
};

const shouldCountUnreadNotifications = (state) => {
  const isTabVisible   = state.get('isTabVisible');
  const isOnTop        = state.get('top');
  const isMounted      = state.get('mounted') > 0;
  const lastReadId     = state.get('lastReadId');
  const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0);

  return !(isTabVisible && isOnTop && isMounted && lastItemReached);
};

const recountUnread = (state, last_read_id) => {
  return state.withMutations(mutable => {
    if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
      mutable.set('lastReadId', last_read_id);
    }

    if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
      mutable.set('readMarkerId', last_read_id);
    }

    if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
      mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
    }
  });
};

export default function notifications(state = initialState, action) {
  let st;

  switch(action.type) {
  case MARKERS_FETCH_SUCCESS:
    return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
  case NOTIFICATIONS_MOUNT:
    return updateMounted(state);
  case NOTIFICATIONS_UNMOUNT:
    return state.update('mounted', count => count - 1);
  case NOTIFICATIONS_SET_VISIBILITY:
    return updateVisibility(state, action.visibility);
  case NOTIFICATIONS_LOAD_PENDING:
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
  case NOTIFICATIONS_EXPAND_REQUEST:
  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
    return state.set('isLoading', true);
  case NOTIFICATIONS_DELETE_MARKED_FAIL:
  case NOTIFICATIONS_EXPAND_FAIL:
    return state.set('isLoading', false);
  case NOTIFICATIONS_FILTER_SET:
    return state.set('items', ImmutableList()).set('hasMore', true);
  case NOTIFICATIONS_SCROLL_TOP:
    return updateTop(state, action.top);
  case NOTIFICATIONS_UPDATE:
    return normalizeNotification(state, action.notification, action.usePendingItems);
  case NOTIFICATIONS_EXPAND_SUCCESS:
    return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems);
  case ACCOUNT_BLOCK_SUCCESS:
    return filterNotifications(state, [action.relationship.id]);
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  case DOMAIN_BLOCK_SUCCESS:
    return filterNotifications(state, action.accounts);
  case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
  case FOLLOW_REQUEST_REJECT_SUCCESS:
    return filterNotifications(state, [action.id], 'follow_request');
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  case NOTIFICATIONS_CLEAR:
    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
  case TIMELINE_DELETE:
    return deleteByStatus(state, action.id);
  case TIMELINE_DISCONNECT:
    return action.timeline === 'home' ?
      state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
      state;

  case NOTIFICATION_MARK_FOR_DELETE:
    return markForDelete(state, action.id, action.yes);

  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
    return deleteMarkedNotifs(state).set('isLoading', false);

  case NOTIFICATIONS_ENTER_CLEARING_MODE:
    st = state.set('cleaningMode', action.yes);
    if (!action.yes) {
      return unmarkAllForDelete(st).set('markNewForDelete', false);
    } else {
      return st;
    }

  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
    st = state;
    if (action.yes === null) {
      // Toggle - this is a bit confusing, as it toggles the all-none mode
      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
    } else {
      st = st.set('markNewForDelete', action.yes);
    }
    return markAllForDelete(st, action.yes);

  case NOTIFICATIONS_MARK_AS_READ:
    const lastNotification = state.get('items').find(item => item !== null);
    return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;

  default:
    return state;
  }
};
<commit_msg>Fix issue checking for last unread notification when there are gaps<commit_after>import {
  NOTIFICATIONS_MOUNT,
  NOTIFICATIONS_UNMOUNT,
  NOTIFICATIONS_SET_VISIBILITY,
  NOTIFICATIONS_UPDATE,
  NOTIFICATIONS_EXPAND_SUCCESS,
  NOTIFICATIONS_EXPAND_REQUEST,
  NOTIFICATIONS_EXPAND_FAIL,
  NOTIFICATIONS_FILTER_SET,
  NOTIFICATIONS_CLEAR,
  NOTIFICATIONS_SCROLL_TOP,
  NOTIFICATIONS_LOAD_PENDING,
  NOTIFICATIONS_DELETE_MARKED_REQUEST,
  NOTIFICATIONS_DELETE_MARKED_SUCCESS,
  NOTIFICATION_MARK_FOR_DELETE,
  NOTIFICATIONS_DELETE_MARKED_FAIL,
  NOTIFICATIONS_ENTER_CLEARING_MODE,
  NOTIFICATIONS_MARK_ALL_FOR_DELETE,
  NOTIFICATIONS_MARK_AS_READ,
} from 'flavours/glitch/actions/notifications';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
  FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
  FOLLOW_REQUEST_REJECT_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import {
  MARKERS_FETCH_SUCCESS,
} from 'flavours/glitch/actions/markers';
import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from 'flavours/glitch/actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap({
  pendingItems: ImmutableList(),
  items: ImmutableList(),
  hasMore: true,
  top: false,
  mounted: 0,
  unread: 0,
  lastReadId: '0',
  readMarkerId: '0',
  isLoading: false,
  cleaningMode: false,
  isTabVisible: true,
  // notification removal mark of new notifs loaded whilst cleaningMode is true.
  markNewForDelete: false,
});

const notificationToMap = (state, notification) => ImmutableMap({
  id: notification.id,
  type: notification.type,
  account: notification.account.id,
  markedForDelete: state.get('markNewForDelete'),
  status: notification.status ? notification.status.id : null,
});

const normalizeNotification = (state, notification, usePendingItems) => {
  const top = state.get('top');

  if (usePendingItems || !state.get('pendingItems').isEmpty()) {
    return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))).update('unread', unread => unread + 1);
  }

  if (shouldCountUnreadNotifications(state)) {
    state = state.update('unread', unread => unread + 1);
  } else {
    state = state.set('lastReadId', notification.id);
  }

  return state.update('items', list => {
    if (top && list.size > 40) {
      list = list.take(20);
    }

    return list.unshift(notificationToMap(state, notification));
  });
};

const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
  const lastReadId = state.get('lastReadId');
  let items = ImmutableList();

  notifications.forEach((n, i) => {
    items = items.set(i, notificationToMap(state, n));
  });

  return state.withMutations(mutable => {
    if (!items.isEmpty()) {
      usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());

      mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
        const lastIndex = 1 + list.findLastIndex(
          item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
        );

        const firstIndex = 1 + list.take(lastIndex).findLastIndex(
          item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
        );

        return list.take(firstIndex).concat(items, list.skip(lastIndex));
      });
    }

    if (!next) {
      mutable.set('hasMore', false);
    }

    if (shouldCountUnreadNotifications(state)) {
      mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0));
    } else {
      const mostRecent = items.find(item => item !== null);
      if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
        mutable.set('lastReadId', mostRecent.get('id'));
      }
    }

    mutable.set('isLoading', false);
  });
};

const filterNotifications = (state, accountIds, type) => {
  const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
  return state.update('items', helper).update('pendingItems', helper);
};

const clearUnread = (state) => {
  state = state.set('unread', state.get('pendingItems').size);
  const lastNotification = state.get('items').find(item => item !== null);
  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
};

const updateTop = (state, top) => {
  state = state.set('top', top);

  if (!shouldCountUnreadNotifications(state)) {
    state = clearUnread(state);
  }

  return state;
};

const deleteByStatus = (state, statusId) => {
  const lastReadId = state.get('lastReadId');

  if (shouldCountUnreadNotifications(state)) {
    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
    state = state.update('unread', unread => unread - deletedUnread.size);
  }

  const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
  const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
  state = state.update('unread', unread => unread - deletedUnread.size);
  return state.update('items', helper).update('pendingItems', helper);
};

const markForDelete = (state, notificationId, yes) => {
  return state.update('items', list => list.map(item => {
    if(item.get('id') === notificationId) {
      return item.set('markedForDelete', yes);
    } else {
      return item;
    }
  }));
};

const markAllForDelete = (state, yes) => {
  return state.update('items', list => list.map(item => {
    if(yes !== null) {
      return item.set('markedForDelete', yes);
    } else {
      return item.set('markedForDelete', !item.get('markedForDelete'));
    }
  }));
};

const unmarkAllForDelete = (state) => {
  return state.update('items', list => list.map(item => item.set('markedForDelete', false)));
};

const deleteMarkedNotifs = (state) => {
  return state.update('items', list => list.filterNot(item => item.get('markedForDelete')));
};

const updateMounted = (state) => {
  state = state.update('mounted', count => count + 1);
  if (!shouldCountUnreadNotifications(state)) {
    state = state.set('readMarkerId', state.get('lastReadId'));
    state = clearUnread(state);
  }
  return state;
};

const updateVisibility = (state, visibility) => {
  state = state.set('isTabVisible', visibility);
  if (!shouldCountUnreadNotifications(state)) {
    state = state.set('readMarkerId', state.get('lastReadId'));
    state = clearUnread(state);
  }
  return state;
};

const shouldCountUnreadNotifications = (state) => {
  const isTabVisible   = state.get('isTabVisible');
  const isOnTop        = state.get('top');
  const isMounted      = state.get('mounted') > 0;
  const lastReadId     = state.get('lastReadId');
  const lastItem       = state.get('items').findLast(item => item !== null);
  const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0);

  return !(isTabVisible && isOnTop && isMounted && lastItemReached);
};

const recountUnread = (state, last_read_id) => {
  return state.withMutations(mutable => {
    if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
      mutable.set('lastReadId', last_read_id);
    }

    if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
      mutable.set('readMarkerId', last_read_id);
    }

    if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
      mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
    }
  });
};

export default function notifications(state = initialState, action) {
  let st;

  switch(action.type) {
  case MARKERS_FETCH_SUCCESS:
    return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
  case NOTIFICATIONS_MOUNT:
    return updateMounted(state);
  case NOTIFICATIONS_UNMOUNT:
    return state.update('mounted', count => count - 1);
  case NOTIFICATIONS_SET_VISIBILITY:
    return updateVisibility(state, action.visibility);
  case NOTIFICATIONS_LOAD_PENDING:
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
  case NOTIFICATIONS_EXPAND_REQUEST:
  case NOTIFICATIONS_DELETE_MARKED_REQUEST:
    return state.set('isLoading', true);
  case NOTIFICATIONS_DELETE_MARKED_FAIL:
  case NOTIFICATIONS_EXPAND_FAIL:
    return state.set('isLoading', false);
  case NOTIFICATIONS_FILTER_SET:
    return state.set('items', ImmutableList()).set('hasMore', true);
  case NOTIFICATIONS_SCROLL_TOP:
    return updateTop(state, action.top);
  case NOTIFICATIONS_UPDATE:
    return normalizeNotification(state, action.notification, action.usePendingItems);
  case NOTIFICATIONS_EXPAND_SUCCESS:
    return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems);
  case ACCOUNT_BLOCK_SUCCESS:
    return filterNotifications(state, [action.relationship.id]);
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  case DOMAIN_BLOCK_SUCCESS:
    return filterNotifications(state, action.accounts);
  case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
  case FOLLOW_REQUEST_REJECT_SUCCESS:
    return filterNotifications(state, [action.id], 'follow_request');
  case ACCOUNT_MUTE_SUCCESS:
    return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  case NOTIFICATIONS_CLEAR:
    return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
  case TIMELINE_DELETE:
    return deleteByStatus(state, action.id);
  case TIMELINE_DISCONNECT:
    return action.timeline === 'home' ?
      state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
      state;

  case NOTIFICATION_MARK_FOR_DELETE:
    return markForDelete(state, action.id, action.yes);

  case NOTIFICATIONS_DELETE_MARKED_SUCCESS:
    return deleteMarkedNotifs(state).set('isLoading', false);

  case NOTIFICATIONS_ENTER_CLEARING_MODE:
    st = state.set('cleaningMode', action.yes);
    if (!action.yes) {
      return unmarkAllForDelete(st).set('markNewForDelete', false);
    } else {
      return st;
    }

  case NOTIFICATIONS_MARK_ALL_FOR_DELETE:
    st = state;
    if (action.yes === null) {
      // Toggle - this is a bit confusing, as it toggles the all-none mode
      //st = st.set('markNewForDelete', !st.get('markNewForDelete'));
    } else {
      st = st.set('markNewForDelete', action.yes);
    }
    return markAllForDelete(st, action.yes);

  case NOTIFICATIONS_MARK_AS_READ:
    const lastNotification = state.get('items').find(item => item !== null);
    return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;

  default:
    return state;
  }
};
<|endoftext|>


<commit_before>var EventEmitter = require('./EventEmitter');
var BitSet = require('./BitSet');
var DEFAULT_SYSTEM_PRIORITY = 1000;

/**
 * Represents an game Engine.
 * It's like hub of all the objects - {@link Entity}, {@link Component}, etc.
 * It contains all the Entity in the game, and all the Component used,
 * and all the System.
 * @constructor
 * @extends EventEmitter
 * @see Entity
 * @see Component
 * @see System
 * @see ComponentGroup
 */
function Engine() {
  EventEmitter.call(this);
  this._entities = {};
  this._entitiesArray = [];
  this._entityPos = 0;
  this._components = [];
  this._componentPos = 0;
  this._componentGroups = [];
  this._componentGroupEntities = [];
  /**
   * Array of all the {@link System} in the Engine.
   * You shouldn't edit this array directly, use
   * {@link Engine#addSystem} and {@link Engine#removeSystem} instead.
   * @var {Array}
   */
  this.systems = [];
  this._systemTable = {};
  this._systemPos = 0;
  this._systemsSortRequired = false;
}

Engine.prototype = Object.create(EventEmitter.prototype);
Engine.prototype.constructor = Engine;

/**
 * Registers a {@link Component} type to the Engine.
 * This won't do anything if Component type is already registered.
 * @param component {Function} - {@link Component}'s constructor.
 */
Engine.prototype.registerComponent = function(component) {
  if(this._components.indexOf(component) != -1) return;
  // TODO this variable is for debugging.
  component.bitIndex = this._componentPos;
  this._components.push(component);
  return this._componentPos ++;
}

/**
 * Returns {@link Component} type's unique ID in the Engine.
 * This unique ID is used in {@link BitSet}'s bit position.
 * This will call {@link Engine#registerComponent} if needed.
 * @param component {Function} - {@link Component} constructor.
 * @return the Component's unique ID.
 */
Engine.prototype.getComponentBit = function(component) {
  var bitPos = this._components.indexOf(component);
  if(bitPos == -1) {
    return this.registerComponent(component);
  } else {
    return bitPos;
  }
}

/**
 * Returns a BitSet holding combination of {@link Component}s type's unique ID.
 * @param components {Array} - An array holding {@link Component} constructors.
 * @return {BitSet} A BitSet holding combination of Components.
 */
Engine.prototype.getComponentsBitSet = function(components) {
  var bits = new BitSet();
  for(var i = 0; i < components.length; ++i) {
    bits.set(this.getComponentBit(components[i]), true);
  }
  return bits;
}

/**
 * Returns a new Entity ID to use.
 * This changes its value each time it's called.
 * @return {Number} An integer that is used for Entity's ID
 * @see Entity
 */
Engine.prototype.obtainEntityId = function() {
  return this._entityPos ++;
}

/**
 * Adds an Entity to the Engine.
 * @param entity {Entity} - An Entity to add
 * @fires Engine#entityAdded
 * @fires ComponentGroup#entityAdded
 */
Engine.prototype.addEntity = function(entity) {
  if(entity.id != null) return;
  entity.id = this.obtainEntityId();
  entity._engine = this;
  this._entities[entity.id] = entity;
  this._entitiesArray.push(entity);
  /**
   * This event is fired when a Entity has added to {@link Engine}.
   *
   * @event Engine#entityAdded
   * @type {Entity}
   */
  this.emit('entityAdded', entity);
  this.updateComponentGroup(entity);
  entity.on('componentAdded', this.updateComponentGroup, this);
  entity.on('componentRemoved', this.updateComponentGroup, this);
}

/**
 * Removes an Entity from the Engine.
 * @param entity {Entity} - An Entity to remove
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeEntity = function(entity) {
  var entityPos = this._entitiesArray.indexOf(entity);
  if(entityPos == -1) return;
  delete this._entities[entity.id];
  this._entitiesArray.splice(entityPos, 1);
  /**
   * This event is fired when a Entity has removed from {@link Engine}.
   *
   * @event Engine#entityRemoved
   * @type {Entity}
   */
  this.emit('entityRemoved', entity);
  entity.removeListener('componentAdded', this.updateComponentGroup);
  entity.removeListener('componentRemoved', this.updateComponentGroup);
  // TODO Optimiziation
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(entity.componentGroupBits.get(componentGroup.id)) {
      var componentEntities = this._componentGroupEntities[i];
      componentEntities.splice(componentEntities.indexOf(entity), 1);
      componentGroup.emit('entityRemoved', entity);
    }
  }
}

/**
 * Removes all {@link Entity} from the Engine.
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeAllEntities = function(entity) {
  while(this._entitiesArray.length > 0) {
    this.removeEntity(this._entitiesArray[0]);
  }
}

/**
 * Returns an Entity with given ID.
 * This will return null if it can't be found.
 * @param id {Number} - An ID to find the Entity
 * @returns {Entity} The entity with given ID
 */
Engine.prototype.getEntity = function(id) {
  return this._entities[id];
}

/**
 * Returns an array of Entity.
 * It shouldn't be edited since the Engine uses it directly.
 * @returns {Array} An array holding {@link Entity}
 */
Engine.prototype.getEntities = function() {
  return this._entitiesArray;
}

/**
 * Updates the Entity to find if it matches {@link ComponentGroup}s' criteria.
 * @param entity {Entity} - the Entity to update
 * @private
 * @see ComponentGroup
 */
Engine.prototype.updateComponentGroup = function(entity) {
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(componentGroup.matches(entity)) {
      if(!entity.componentGroupBits.get(componentGroup.id)) {
        // 추가
        entity.componentGroupBits.set(componentGroup.id, true);
        this._componentGroupEntities[i].push(entity);
        componentGroup.emit('entityAdded', entity);
      }
    } else {
      if(entity.componentGroupBits.get(componentGroup.id)) {
        // 삭제
        entity.componentGroupBits.set(componentGroup.id, false);
        var componentEntities = this._componentGroupEntities[i];
        componentEntities.splice(componentEntities.indexOf(entity), 1);
        componentGroup.emit('entityRemoved', entity);
      }
    }
  }
}

/**
 * Registers the ComponentGroup to the Engine.
 * Engine will check if an Entity matches ComponentGroup's criteria every time
 * when a Entity is added or removed.
 * @param componentGroup {ComponentGroup} - The ComponentGroup to register
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Entity
 */
Engine.prototype.registerComponentGroup = function(componentGroup) {
  for(var i = 0 ; i < this._componentGroups.length; ++i) {
    if(this._componentGroups[i].equals(componentGroup)) {
      return this._componentGroupEntities[i];
    }
  }
  if(componentGroup.id != null &&
      this._componentGroups[componentGroup.id] == componentGroup) {
    return this._componentGroupEntities[componentGroup.id];
  }
  componentGroup.id = this._componentGroups.length;
  componentGroup._engine = this;
  this._componentGroups.push(componentGroup);
  var componentGroupEntity = [];
  this._componentGroupEntities.push(componentGroupEntity);
  // initialize componentGroup array
  this.getEntities().forEach(function(entity) {
    if(componentGroup.matches(entity)) {
      // 추가
      entity.componentGroupBits.set(componentGroup.id, true);
      componentGroup.push(entity);
      componentGroup.emit('entityAdded', entity);
    }
  });
  return componentGroupEntity;
}

/**
 * Returns an array having {@link Entity} matching ComponentGroup's criteria.
 * The array will keep updated, so you can call this method only once
 * and check its contents if needed.
 * It's an alias function to {@link Engine#registerComponentGroup}
 * @param componentGroup {ComponentGroup} - The ComponentGroup
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Engine#registerComponentGroup
 */
Engine.prototype.getEntitiesFor = function(componentGroup) {
  return this.registerComponentGroup(componentGroup);
}

/**
 * Searches ComponentGroup by its array.
 * @param entities {Array} An array of ComponentGroup
 * @returns {ComponentGroup} The ComponentGroup linked with the array
 */
Engine.prototype.getComponentGroup = function(entities) {
  return this._componentGroups[this._componentGroupEntities.indexOf(entities)];
}

/**
 * Adds the System to the Engine.
 * The System will be triggered when the {@link Engine#update} is called.
 * This will trigger {@link System#onAddedToEngine}.
 * @param system {System} - The System to add
 */
Engine.prototype.addSystem = function(system) {
  if(this.systems.indexOf(system) != -1) return;
  this._systemTable[system.constructor] = system;
  system._id = this._systemPos ++;
  this.systems.push(system);
  if(typeof system.onAddedToEngine == 'function') {
    system.onAddedToEngine(this);
  }
  if(system.priority == null) {
    system.priority = DEFAULT_SYSTEM_PRIORITY;
  }
  system.engine = this;
  this._systemsSortRequired = true;
}

/**
 * Returns the System registered to the Engine.
 * @param system {Function} - The System's constructor
 * @returns {System} The system registered to the Engine
 */
Engine.prototype.getSystem = function(system) {
  return this._systemTable[system];
}

/**
 * Removes the System from the Engine.
 * This will trigger {@link System#onRemovedFromEngine}.
 * @param system {System} - The System to remove.
 */
Engine.prototype.removeSystem = function(system) {
  var systemPos = this.systems.indexOf(system);
  if(systemPos == -1) return;
  delete this._systemTable[system.constructor];
  this.systems.splice(systemPos, 1);
  if(typeof system.onRemovedFromEngine == 'function') {
    system.onRemovedFromEngine(this);
  }
}

/**
 * Sorts the System array to match their priority.
 * @private
 * @see System
 */
Engine.prototype.sortSystems = function() {
  if(this._systemsSortRequired) {
    this.systems.sort(function(a, b) {
      if(a.priority > b.priority) {
        return 1;
      } else if(a.priority < b.priority) {
        return -1;
      } else {
        if(a._id > b._id) {
          return 1;
        } else if(a._id < b._id) {
          return -1;
        } else {
          return 0;
        }
      }
    });
    this._systemsSortRequired = false;
  }
}

/**
 * An update function called by every tick of the game.
 */
Engine.prototype.update = function() {
  this.sortSystems();
  this.systems.forEach(function(system) {
    if(system.update) {
      system.update();
    }
  })
}

if(typeof module !== 'undefined') {
  module.exports = Engine;
}
<commit_msg>Provide a way to get list of entities. Fixes #2 You can get list of entities by using engine.getEntitiesFor(components...).<commit_after>var EventEmitter = require('./EventEmitter');
var ComponentGroup = require('./ComponentGroup');
var BitSet = require('./BitSet');
var DEFAULT_SYSTEM_PRIORITY = 1000;

/**
 * Represents an game Engine.
 * It's like hub of all the objects - {@link Entity}, {@link Component}, etc.
 * It contains all the Entity in the game, and all the Component used,
 * and all the System.
 * @constructor
 * @extends EventEmitter
 * @see Entity
 * @see Component
 * @see System
 * @see ComponentGroup
 */
function Engine() {
  EventEmitter.call(this);
  this._entities = {};
  this._entitiesArray = [];
  this._entityPos = 0;
  this._components = [];
  this._componentPos = 0;
  this._componentGroups = [];
  this._componentGroupEntities = [];
  /**
   * Array of all the {@link System} in the Engine.
   * You shouldn't edit this array directly, use
   * {@link Engine#addSystem} and {@link Engine#removeSystem} instead.
   * @var {Array}
   */
  this.systems = [];
  this._systemTable = {};
  this._systemPos = 0;
  this._systemsSortRequired = false;
}

Engine.prototype = Object.create(EventEmitter.prototype);
Engine.prototype.constructor = Engine;

/**
 * Registers a {@link Component} type to the Engine.
 * This won't do anything if Component type is already registered.
 * @param component {Function} - {@link Component}'s constructor.
 */
Engine.prototype.registerComponent = function(component) {
  if(this._components.indexOf(component) != -1) return;
  // TODO this variable is for debugging.
  component.bitIndex = this._componentPos;
  this._components.push(component);
  return this._componentPos ++;
}

/**
 * Returns {@link Component} type's unique ID in the Engine.
 * This unique ID is used in {@link BitSet}'s bit position.
 * This will call {@link Engine#registerComponent} if needed.
 * @param component {Function} - {@link Component} constructor.
 * @return the Component's unique ID.
 */
Engine.prototype.getComponentBit = function(component) {
  var bitPos = this._components.indexOf(component);
  if(bitPos == -1) {
    return this.registerComponent(component);
  } else {
    return bitPos;
  }
}

/**
 * Returns a BitSet holding combination of {@link Component}s type's unique ID.
 * @param components {Array} - An array holding {@link Component} constructors.
 * @return {BitSet} A BitSet holding combination of Components.
 */
Engine.prototype.getComponentsBitSet = function(components) {
  var bits = new BitSet();
  for(var i = 0; i < components.length; ++i) {
    bits.set(this.getComponentBit(components[i]), true);
  }
  return bits;
}

/**
 * Returns a new Entity ID to use.
 * This changes its value each time it's called.
 * @return {Number} An integer that is used for Entity's ID
 * @see Entity
 */
Engine.prototype.obtainEntityId = function() {
  return this._entityPos ++;
}

/**
 * Adds an Entity to the Engine.
 * @param entity {Entity} - An Entity to add
 * @fires Engine#entityAdded
 * @fires ComponentGroup#entityAdded
 */
Engine.prototype.addEntity = function(entity) {
  if(entity.id != null) return;
  entity.id = this.obtainEntityId();
  entity._engine = this;
  this._entities[entity.id] = entity;
  this._entitiesArray.push(entity);
  /**
   * This event is fired when a Entity has added to {@link Engine}.
   *
   * @event Engine#entityAdded
   * @type {Entity}
   */
  this.emit('entityAdded', entity);
  this.updateComponentGroup(entity);
  entity.on('componentAdded', this.updateComponentGroup, this);
  entity.on('componentRemoved', this.updateComponentGroup, this);
}

/**
 * Removes an Entity from the Engine.
 * @param entity {Entity} - An Entity to remove
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeEntity = function(entity) {
  var entityPos = this._entitiesArray.indexOf(entity);
  if(entityPos == -1) return;
  delete this._entities[entity.id];
  this._entitiesArray.splice(entityPos, 1);
  /**
   * This event is fired when a Entity has removed from {@link Engine}.
   *
   * @event Engine#entityRemoved
   * @type {Entity}
   */
  this.emit('entityRemoved', entity);
  entity.removeListener('componentAdded', this.updateComponentGroup);
  entity.removeListener('componentRemoved', this.updateComponentGroup);
  // TODO Optimiziation
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(entity.componentGroupBits.get(componentGroup.id)) {
      var componentEntities = this._componentGroupEntities[i];
      componentEntities.splice(componentEntities.indexOf(entity), 1);
      componentGroup.emit('entityRemoved', entity);
    }
  }
}

/**
 * Removes all {@link Entity} from the Engine.
 * @fires Engine#entityRemoved
 * @fires ComponentGroup#entityRemoved
 */
Engine.prototype.removeAllEntities = function(entity) {
  while(this._entitiesArray.length > 0) {
    this.removeEntity(this._entitiesArray[0]);
  }
}

/**
 * Returns an Entity with given ID.
 * This will return null if it can't be found.
 * @param id {Number} - An ID to find the Entity
 * @returns {Entity} The entity with given ID
 */
Engine.prototype.getEntity = function(id) {
  return this._entities[id];
}

/**
 * Returns an array of Entity.
 * It shouldn't be edited since the Engine uses it directly.
 * @returns {Array} An array holding {@link Entity}
 */
Engine.prototype.getEntities = function() {
  return this._entitiesArray;
}

/**
 * Updates the Entity to find if it matches {@link ComponentGroup}s' criteria.
 * @param entity {Entity} - the Entity to update
 * @private
 * @see ComponentGroup
 */
Engine.prototype.updateComponentGroup = function(entity) {
  for(var i = 0; i < this._componentGroups.length; ++i) {
    var componentGroup = this._componentGroups[i];
    if(componentGroup.matches(entity)) {
      if(!entity.componentGroupBits.get(componentGroup.id)) {
        // 추가
        entity.componentGroupBits.set(componentGroup.id, true);
        this._componentGroupEntities[i].push(entity);
        componentGroup.emit('entityAdded', entity);
      }
    } else {
      if(entity.componentGroupBits.get(componentGroup.id)) {
        // 삭제
        entity.componentGroupBits.set(componentGroup.id, false);
        var componentEntities = this._componentGroupEntities[i];
        componentEntities.splice(componentEntities.indexOf(entity), 1);
        componentGroup.emit('entityRemoved', entity);
      }
    }
  }
}

/**
 * Registers the ComponentGroup to the Engine.
 * Engine will check if an Entity matches ComponentGroup's criteria every time
 * when a Entity is added or removed.
 * @param componentGroup {ComponentGroup} - The ComponentGroup to register
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Entity
 */
Engine.prototype.registerComponentGroup = function(componentGroup) {
  for(var i = 0 ; i < this._componentGroups.length; ++i) {
    if(this._componentGroups[i].equals(componentGroup)) {
      return this._componentGroupEntities[i];
    }
  }
  if(componentGroup.id != null &&
      this._componentGroups[componentGroup.id] == componentGroup) {
    return this._componentGroupEntities[componentGroup.id];
  }
  componentGroup.id = this._componentGroups.length;
  componentGroup._engine = this;
  this._componentGroups.push(componentGroup);
  var componentGroupEntity = [];
  this._componentGroupEntities.push(componentGroupEntity);
  // initialize componentGroup array
  this.getEntities().forEach(function(entity) {
    if(componentGroup.matches(entity)) {
      // 추가
      entity.componentGroupBits.set(componentGroup.id, true);
      componentGroup.push(entity);
      componentGroup.emit('entityAdded', entity);
    }
  });
  return componentGroupEntity;
}

/**
 * Returns an array having {@link Entity} matching ComponentGroup's criteria.
 * The array will keep updated, so you can call this method only once
 * and check its contents if needed.
 * It's an alias function to {@link Engine#registerComponentGroup}
 * @param componentGroup {ComponentGroup} - The ComponentGroup
 * @returns {Array} An array holding {@link Entity} matching its criteria
 * @fires ComponentGroup#entityAdded
 * @see Engine#registerComponentGroup
 */
Engine.prototype.getEntitiesFor = function(componentGroup) {
  if(componentGroup instanceof ComponentGroup) {
    return this.registerComponentGroup(componentGroup);
  } else {
    // Build ComponentGroup with arguments
    var builder = ComponentGroup.createBuilder(this);
    builder.contain.apply(builder, arguments);
    var componentGroup = builer.build();
    return this.registerComponentGroup(componentGroup);
  }
}

/**
 * Searches ComponentGroup by its array.
 * @param entities {Array} An array of ComponentGroup
 * @returns {ComponentGroup} The ComponentGroup linked with the array
 */
Engine.prototype.getComponentGroup = function(entities) {
  return this._componentGroups[this._componentGroupEntities.indexOf(entities)];
}

/**
 * Adds the System to the Engine.
 * The System will be triggered when the {@link Engine#update} is called.
 * This will trigger {@link System#onAddedToEngine}.
 * @param system {System} - The System to add
 */
Engine.prototype.addSystem = function(system) {
  if(this.systems.indexOf(system) != -1) return;
  this._systemTable[system.constructor] = system;
  system._id = this._systemPos ++;
  this.systems.push(system);
  if(typeof system.onAddedToEngine == 'function') {
    system.onAddedToEngine(this);
  }
  if(system.priority == null) {
    system.priority = DEFAULT_SYSTEM_PRIORITY;
  }
  system.engine = this;
  this._systemsSortRequired = true;
}

/**
 * Returns the System registered to the Engine.
 * @param system {Function} - The System's constructor
 * @returns {System} The system registered to the Engine
 */
Engine.prototype.getSystem = function(system) {
  return this._systemTable[system];
}

/**
 * Removes the System from the Engine.
 * This will trigger {@link System#onRemovedFromEngine}.
 * @param system {System} - The System to remove.
 */
Engine.prototype.removeSystem = function(system) {
  var systemPos = this.systems.indexOf(system);
  if(systemPos == -1) return;
  delete this._systemTable[system.constructor];
  this.systems.splice(systemPos, 1);
  if(typeof system.onRemovedFromEngine == 'function') {
    system.onRemovedFromEngine(this);
  }
}

/**
 * Sorts the System array to match their priority.
 * @private
 * @see System
 */
Engine.prototype.sortSystems = function() {
  if(this._systemsSortRequired) {
    this.systems.sort(function(a, b) {
      if(a.priority > b.priority) {
        return 1;
      } else if(a.priority < b.priority) {
        return -1;
      } else {
        if(a._id > b._id) {
          return 1;
        } else if(a._id < b._id) {
          return -1;
        } else {
          return 0;
        }
      }
    });
    this._systemsSortRequired = false;
  }
}

/**
 * An update function called by every tick of the game.
 */
Engine.prototype.update = function() {
  this.sortSystems();
  this.systems.forEach(function(system) {
    if(system.update) {
      system.update();
    }
  })
}

if(typeof module !== 'undefined') {
  module.exports = Engine;
}
<|endoftext|>


<commit_before>!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var i=e();for(var s in i)("object"==typeof exports?exports:t)[s]=i[s]}}(this,function(){return function(t){function e(s){if(i[s])return i[s].exports;var n=i[s]={exports:{},id:s,loaded:!1};return t[s].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n=function(){function t(t,e){for(var i=0;i<e.length;i++){var s=e[i];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(t,s.key,s)}}return function(e,i,s){return i&&t(e.prototype,i),s&&t(e,s),e}}(),a="dual-listbox",l="dual-listbox__container",o="dual-listbox__available",d="dual-listbox__selected",u="dual-listbox__title",c="dual-listbox__item",r="dual-listbox__buttons",h="dual-listbox__button",v="dual-listbox__search",f="dual-listbox__item--selected",b=function(){function t(e){var s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};i(this,t),this.setDefaults(),this.selected=[],this.available=[],t.isDomElement(e)?this.select=e:this.select=document.querySelector(e),this._initOptions(s),this._initReusableElements(),this._splitOptions(this.select.options),void 0!==s.options&&this._splitOptions(s.options),this._buildDualListbox(this.select.parentNode),this._addActions(),this.redraw()}return n(t,[{key:"setDefaults",value:function(){this.addEvent=null,this.removeEvent=null,this.availableTitle="Available options",this.selectedTitle="Selected options",this.addButtonText="add",this.removeButtonText="remove",this.addAllButtonText="add all",this.removeAllButtonText="remove all",this.searchPlaceholder="Search"}},{key:"addEventListener",value:function(t,e){this.dualListbox.addEventListener(t,e)}},{key:"addSelected",value:function(t){var e=this.available.indexOf(t);if(e>-1){this.available.splice(e,1),this.selected.push(t),this._selectOption(t.dataset.id),this.redraw();var i=document.createEvent("HTMLEvents");i.initEvent("added",!1,!0),i.addedElement=t,this.dualListbox.dispatchEvent(i)}}},{key:"redraw",value:function(){this.updateAvailableListbox(),this.updateSelectedListbox()}},{key:"removeSelected",value:function(t){var e=this.selected.indexOf(t);if(e>-1){this.selected.splice(e,1),this.available.push(t),this._deselectOption(t.dataset.id),this.redraw();var i=document.createEvent("HTMLEvents");i.initEvent("removed",!1,!0),i.removedElement=t,this.dualListbox.dispatchEvent(i)}}},{key:"searchLists",value:function(t,e){for(var i=e.querySelectorAll("."+c),s=t.toLowerCase(),n=0;n<i.length;n++){var a=i[n];a.textContent.toLowerCase().indexOf(s)===-1?a.style.display="none":a.style.display="list-item"}}},{key:"updateAvailableListbox",value:function(){this._updateListbox(this.availableList,this.available)}},{key:"updateSelectedListbox",value:function(){this._updateListbox(this.selectedList,this.selected)}},{key:"_actionAllSelected",value:function(t){var e=this;t.preventDefault(),this.available.filter(function(t){return"none"!==t.style.display}).forEach(function(t){return e.addSelected(t)})}},{key:"_updateListbox",value:function(t,e){t.innerHTML="";for(var i=0;i<e.length;i++){var s=e[i];t.appendChild(s)}}},{key:"_actionItemSelected",value:function(t){t.preventDefault();var e=this.dualListbox.querySelector("."+f);e&&this.addSelected(e)}},{key:"_actionAllDeselected",value:function(t){var e=this;t.preventDefault(),this.selected.filter(function(t){return"none"!==t.style.display}).forEach(function(t){return e.removeSelected(t)})}},{key:"_actionItemDeselected",value:function(t){t.preventDefault();var e=this.dualListbox.querySelector("."+f);e&&this.removeSelected(e)}},{key:"_actionItemDoubleClick",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;e&&(e.preventDefault(),e.stopPropagation()),this.selected.indexOf(t)>-1?this.removeSelected(t):this.addSelected(t)}},{key:"_actionItemClick",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;i&&i.preventDefault();for(var s=e.querySelectorAll("."+c),n=0;n<s.length;n++){var a=s[n];a!==t&&a.classList.remove(f)}t.classList.contains(f)?t.classList.remove(f):t.classList.add(f)}},{key:"_addActions",value:function(){this._addButtonActions(),this._addSearchActions()}},{key:"_addButtonActions",value:function(){var t=this;this.add_all_button.addEventListener("click",function(e){return t._actionAllSelected(e)}),this.add_button.addEventListener("click",function(e){return t._actionItemSelected(e)}),this.remove_button.addEventListener("click",function(e){return t._actionItemDeselected(e)}),this.remove_all_button.addEventListener("click",function(e){return t._actionAllDeselected(e)})}},{key:"_addClickActions",value:function(t){var e=this;return t.addEventListener("dblclick",function(i){return e._actionItemDoubleClick(t,i)}),t.addEventListener("click",function(i){return e._actionItemClick(t,e.dualListbox,i)}),t}},{key:"_addSearchActions",value:function(){var t=this;this.search.addEventListener("change",function(e){return t.searchLists(e.target.value,t.dualListbox)}),this.search.addEventListener("keyup",function(e){return t.searchLists(e.target.value,t.dualListbox)})}},{key:"_buildDualListbox",value:function(t){this.select.style.display="none",this.dualListBoxContainer.appendChild(this._createList(this.availableListTitle,this.availableList)),this.dualListBoxContainer.appendChild(this.buttons),this.dualListBoxContainer.appendChild(this._createList(this.selectedListTitle,this.selectedList)),this.dualListbox.appendChild(this.search),this.dualListbox.appendChild(this.dualListBoxContainer),t.insertBefore(this.dualListbox,this.select)}},{key:"_createList",value:function(t,e){var i=document.createElement("div");return i.appendChild(t),i.appendChild(e),i}},{key:"_createButtons",value:function(){this.buttons=document.createElement("div"),this.buttons.classList.add(r),this.add_all_button=document.createElement("button"),this.add_all_button.classList.add(h),this.add_all_button.innerHTML=this.addAllButtonText,this.add_button=document.createElement("button"),this.add_button.classList.add(h),this.add_button.innerHTML=this.addButtonText,this.remove_button=document.createElement("button"),this.remove_button.classList.add(h),this.remove_button.innerHTML=this.removeButtonText,this.remove_all_button=document.createElement("button"),this.remove_all_button.classList.add(h),this.remove_all_button.innerHTML=this.removeAllButtonText,this.buttons.appendChild(this.add_all_button),this.buttons.appendChild(this.add_button),this.buttons.appendChild(this.remove_button),this.buttons.appendChild(this.remove_all_button)}},{key:"_createListItem",value:function(t){var e=document.createElement("li");return e.classList.add(c),e.innerHTML=t.text,e.dataset.id=t.value,this._addClickActions(e),e}},{key:"_createSearch",value:function(){this.search=document.createElement("input"),this.search.classList.add(v),this.search.placeholder=this.searchPlaceholder}},{key:"_deselectOption",value:function(t){for(var e=this.select.options,i=0;i<e.length;i++){var s=e[i];s.value===t&&(s.selected=!1)}this.removeEvent&&this.removeEvent(t)}},{key:"_initOptions",value:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e])}},{key:"_initReusableElements",value:function(){this.dualListbox=document.createElement("div"),this.dualListbox.classList.add(a),this.select.id&&this.dualListbox.classList.add(this.select.id),this.dualListBoxContainer=document.createElement("div"),this.dualListBoxContainer.classList.add(l),this.availableList=document.createElement("ul"),this.availableList.classList.add(o),this.selectedList=document.createElement("ul"),this.selectedList.classList.add(d),this.availableListTitle=document.createElement("div"),this.availableListTitle.classList.add(u),this.availableListTitle.innerText=this.availableTitle,this.selectedListTitle=document.createElement("div"),this.selectedListTitle.classList.add(u),this.selectedListTitle.innerText=this.selectedTitle,this._createButtons(),this._createSearch()}},{key:"_selectOption",value:function(t){for(var e=this.select.options,i=0;i<e.length;i++){var s=e[i];s.value===t&&(s.selected=!0)}this.addEvent&&this.addEvent(t)}},{key:"_splitOptions",value:function(e){for(var i=0;i<e.length;i++){var s=e[i];t.isDomElement(s)?this._addOption({text:s.innerHTML,value:s.value,selected:s.attributes.selected}):this._addOption(s)}}},{key:"_addOption",value:function(t){var e=this._createListItem(t);t.selected?this.selected.push(e):this.available.push(e)}}],[{key:"isDomElement",value:function(t){return"object"===("undefined"==typeof HTMLElement?"undefined":s(HTMLElement))?t instanceof HTMLElement:t&&"object"===(void 0===t?"undefined":s(t))&&null!==t&&1===t.nodeType&&"string"==typeof t.nodeName}}]),t}();e.default=b,e.DualListbox=b}])});<commit_msg>Updated dist js<commit_after>!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var i=e();for(var s in i)("object"==typeof exports?exports:t)[s]=i[s]}}(this,function(){return function(t){function e(s){if(i[s])return i[s].exports;var n=i[s]={exports:{},id:s,loaded:!1};return t[s].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e){"use strict";function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n=function(){function t(t,e){for(var i=0;i<e.length;i++){var s=e[i];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(t,s.key,s)}}return function(e,i,s){return i&&t(e.prototype,i),s&&t(e,s),e}}(),a="dual-listbox",l="dual-listbox__container",o="dual-listbox__available",d="dual-listbox__selected",u="dual-listbox__title",c="dual-listbox__item",r="dual-listbox__buttons",h="dual-listbox__button",v="dual-listbox__search",f="dual-listbox__item--selected",b=function(){function t(e){var s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};i(this,t),this.setDefaults(),this.selected=[],this.available=[],t.isDomElement(e)?this.select=e:this.select=document.querySelector(e),this._initOptions(s),this._initReusableElements(),this._splitOptions(this.select.options),void 0!==s.options&&this._splitOptions(s.options),this._buildDualListbox(this.select.parentNode),this._addActions(),this.redraw()}return n(t,[{key:"setDefaults",value:function(){this.addEvent=null,this.removeEvent=null,this.availableTitle="Available options",this.selectedTitle="Selected options",this.addButtonText="add",this.removeButtonText="remove",this.addAllButtonText="add all",this.removeAllButtonText="remove all",this.searchPlaceholder="Search"}},{key:"addEventListener",value:function(t,e){this.dualListbox.addEventListener(t,e)}},{key:"addSelected",value:function(t){var e=this.available.indexOf(t);if(e>-1){this.available.splice(e,1),this.selected.push(t),this._selectOption(t.dataset.id),this.redraw();var i=document.createEvent("HTMLEvents");i.initEvent("added",!1,!0),i.target=t,this.dualListbox.dispatchEvent(i)}}},{key:"redraw",value:function(){this.updateAvailableListbox(),this.updateSelectedListbox()}},{key:"removeSelected",value:function(t){var e=this.selected.indexOf(t);if(e>-1){this.selected.splice(e,1),this.available.push(t),this._deselectOption(t.dataset.id),this.redraw();var i=document.createEvent("HTMLEvents");i.initEvent("removed",!1,!0),i.target=t,this.dualListbox.dispatchEvent(i)}}},{key:"searchLists",value:function(t,e){for(var i=e.querySelectorAll("."+c),s=t.toLowerCase(),n=0;n<i.length;n++){var a=i[n];a.textContent.toLowerCase().indexOf(s)===-1?a.style.display="none":a.style.display="list-item"}}},{key:"updateAvailableListbox",value:function(){this._updateListbox(this.availableList,this.available)}},{key:"updateSelectedListbox",value:function(){this._updateListbox(this.selectedList,this.selected)}},{key:"_actionAllSelected",value:function(t){var e=this;t.preventDefault(),this.available.filter(function(t){return"none"!==t.style.display}).forEach(function(t){return e.addSelected(t)})}},{key:"_updateListbox",value:function(t,e){t.innerHTML="";for(var i=0;i<e.length;i++){var s=e[i];t.appendChild(s)}}},{key:"_actionItemSelected",value:function(t){t.preventDefault();var e=this.dualListbox.querySelector("."+f);e&&this.addSelected(e)}},{key:"_actionAllDeselected",value:function(t){var e=this;t.preventDefault(),this.selected.filter(function(t){return"none"!==t.style.display}).forEach(function(t){return e.removeSelected(t)})}},{key:"_actionItemDeselected",value:function(t){t.preventDefault();var e=this.dualListbox.querySelector("."+f);e&&this.removeSelected(e)}},{key:"_actionItemDoubleClick",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;e&&(e.preventDefault(),e.stopPropagation()),this.selected.indexOf(t)>-1?this.removeSelected(t):this.addSelected(t)}},{key:"_actionItemClick",value:function(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;i&&i.preventDefault();for(var s=e.querySelectorAll("."+c),n=0;n<s.length;n++){var a=s[n];a!==t&&a.classList.remove(f)}t.classList.contains(f)?t.classList.remove(f):t.classList.add(f)}},{key:"_addActions",value:function(){this._addButtonActions(),this._addSearchActions()}},{key:"_addButtonActions",value:function(){var t=this;this.add_all_button.addEventListener("click",function(e){return t._actionAllSelected(e)}),this.add_button.addEventListener("click",function(e){return t._actionItemSelected(e)}),this.remove_button.addEventListener("click",function(e){return t._actionItemDeselected(e)}),this.remove_all_button.addEventListener("click",function(e){return t._actionAllDeselected(e)})}},{key:"_addClickActions",value:function(t){var e=this;return t.addEventListener("dblclick",function(i){return e._actionItemDoubleClick(t,i)}),t.addEventListener("click",function(i){return e._actionItemClick(t,e.dualListbox,i)}),t}},{key:"_addSearchActions",value:function(){var t=this;this.search.addEventListener("change",function(e){return t.searchLists(e.target.value,t.dualListbox)}),this.search.addEventListener("keyup",function(e){return t.searchLists(e.target.value,t.dualListbox)})}},{key:"_buildDualListbox",value:function(t){this.select.style.display="none",this.dualListBoxContainer.appendChild(this._createList(this.availableListTitle,this.availableList)),this.dualListBoxContainer.appendChild(this.buttons),this.dualListBoxContainer.appendChild(this._createList(this.selectedListTitle,this.selectedList)),this.dualListbox.appendChild(this.search),this.dualListbox.appendChild(this.dualListBoxContainer),t.insertBefore(this.dualListbox,this.select)}},{key:"_createList",value:function(t,e){var i=document.createElement("div");return i.appendChild(t),i.appendChild(e),i}},{key:"_createButtons",value:function(){this.buttons=document.createElement("div"),this.buttons.classList.add(r),this.add_all_button=document.createElement("button"),this.add_all_button.classList.add(h),this.add_all_button.innerHTML=this.addAllButtonText,this.add_button=document.createElement("button"),this.add_button.classList.add(h),this.add_button.innerHTML=this.addButtonText,this.remove_button=document.createElement("button"),this.remove_button.classList.add(h),this.remove_button.innerHTML=this.removeButtonText,this.remove_all_button=document.createElement("button"),this.remove_all_button.classList.add(h),this.remove_all_button.innerHTML=this.removeAllButtonText,this.buttons.appendChild(this.add_all_button),this.buttons.appendChild(this.add_button),this.buttons.appendChild(this.remove_button),this.buttons.appendChild(this.remove_all_button)}},{key:"_createListItem",value:function(t){var e=document.createElement("li");return e.classList.add(c),e.innerHTML=t.text,e.dataset.id=t.value,this._addClickActions(e),e}},{key:"_createSearch",value:function(){this.search=document.createElement("input"),this.search.classList.add(v),this.search.placeholder=this.searchPlaceholder}},{key:"_deselectOption",value:function(t){for(var e=this.select.options,i=0;i<e.length;i++){var s=e[i];s.value===t&&(s.selected=!1)}this.removeEvent&&this.removeEvent(t)}},{key:"_initOptions",value:function(t){for(var e in t)t.hasOwnProperty(e)&&(this[e]=t[e])}},{key:"_initReusableElements",value:function(){this.dualListbox=document.createElement("div"),this.dualListbox.classList.add(a),this.select.id&&this.dualListbox.classList.add(this.select.id),this.dualListBoxContainer=document.createElement("div"),this.dualListBoxContainer.classList.add(l),this.availableList=document.createElement("ul"),this.availableList.classList.add(o),this.selectedList=document.createElement("ul"),this.selectedList.classList.add(d),this.availableListTitle=document.createElement("div"),this.availableListTitle.classList.add(u),this.availableListTitle.innerText=this.availableTitle,this.selectedListTitle=document.createElement("div"),this.selectedListTitle.classList.add(u),this.selectedListTitle.innerText=this.selectedTitle,this._createButtons(),this._createSearch()}},{key:"_selectOption",value:function(t){for(var e=this.select.options,i=0;i<e.length;i++){var s=e[i];s.value===t&&(s.selected=!0)}this.addEvent&&this.addEvent(t)}},{key:"_splitOptions",value:function(e){for(var i=0;i<e.length;i++){var s=e[i];t.isDomElement(s)?this._addOption({text:s.innerHTML,value:s.value,selected:s.attributes.selected}):this._addOption(s)}}},{key:"_addOption",value:function(t){var e=this._createListItem(t);t.selected?this.selected.push(e):this.available.push(e)}}],[{key:"isDomElement",value:function(t){return"object"===("undefined"==typeof HTMLElement?"undefined":s(HTMLElement))?t instanceof HTMLElement:t&&"object"===(void 0===t?"undefined":s(t))&&null!==t&&1===t.nodeType&&"string"==typeof t.nodeName}}]),t}();e.default=b,e.DualListbox=b}])});<|endoftext|>


<commit_before>(function($) {
	$.fn.countdown = function(options, callback) {

		//custom 'this' selector
		thisEl = $(this);

		//array of custom settings
		var settings = {
			'date': null,
			'format': null
		};

		//append the settings array to options
		if(options) {
			$.extend(settings, options);
		}

		//main countdown function
		function countdown_proc() {

			eventDate = Date.parse(settings['date']) / 1000;
			currentDate = Math.floor($.now() / 1000);

			if(eventDate <= currentDate) {
				callback.call(this);
				clearInterval(interval);
			}

			seconds = eventDate - currentDate;

			days = Math.floor(seconds / (60 * 60 * 24)); //calculate the number of days
			seconds -= days * 60 * 60 * 24; //update the seconds variable with no. of days removed

			hours = Math.floor(seconds / (60 * 60));
			seconds -= hours * 60 * 60; //update the seconds variable with no. of hours removed

			minutes = Math.floor(seconds / 60);
			seconds -= minutes * 60; //update the seconds variable with no. of minutes removed

			//conditional Ss
			if (days == 1) { thisEl.find(".timeRefDays").text("día"); } else { thisEl.find(".timeRefDays").text("días"); }
			if (hours == 1) { thisEl.find(".timeRefHours").text("hora"); } else { thisEl.find(".timeRefHours").text("horas"); }
			if (minutes == 1) { thisEl.find(".timeRefMinutes").text("minuto"); } else { thisEl.find(".timeRefMinutes").text("minutos"); }
			if (seconds == 1) { thisEl.find(".timeRefSeconds").text("segundo"); } else { thisEl.find(".timeRefSeconds").text("segundo"); }

			//logic for the two_digits ON setting
			if(settings['format'] == "on") {
				days = (String(days).length >= 2) ? days : "0" + days;
				hours = (String(hours).length >= 2) ? hours : "0" + hours;
				minutes = (String(minutes).length >= 2) ? minutes : "0" + minutes;
				seconds = (String(seconds).length >= 2) ? seconds : "0" + seconds;
			}

			//update the countdown's html values.
			if(!isNaN(eventDate)) {
				thisEl.find(".days").text(days);
				thisEl.find(".hours").text(hours);
				thisEl.find(".minutes").text(minutes);
				thisEl.find(".seconds").text(seconds);
			} else {
				alert("Invalid date. Here's an example: 12 Tuesday 2012 17:30:00");
				clearInterval(interval);
			}
		}

		//run the function
		countdown_proc();

		//loop the function
		interval = setInterval(countdown_proc, 1000);

	}
}) (jQuery);
<commit_msg>Update countdown.js<commit_after>(function($) {
	$.fn.countdown = function(options, callback) {

		//custom 'this' selector
		thisEl = $(this);

		//array of custom settings
		var settings = {
			'date': null,
			'format': null
		};

		//append the settings array to options
		if(options) {
			$.extend(settings, options);
		}

		//main countdown function
		function countdown_proc() {

			eventDate = Date.parse(settings['date']) / 1000;
			currentDate = Math.floor($.now() / 1000);

			if(eventDate <= currentDate) {
				callback.call(this);
				clearInterval(interval);
			}

			seconds = eventDate - currentDate;

			days = Math.floor(seconds / (60 * 60 * 24)); //calculate the number of days
			seconds -= days * 60 * 60 * 24; //update the seconds variable with no. of days removed

			hours = Math.floor(seconds / (60 * 60));
			seconds -= hours * 60 * 60; //update the seconds variable with no. of hours removed

			minutes = Math.floor(seconds / 60);
			seconds -= minutes * 60; //update the seconds variable with no. of minutes removed

			//conditional Ss
			if (days == 1) { thisEl.find(".timeRefDays").text("día"); } else { thisEl.find(".timeRefDays").text("días"); }
			if (hours == 1) { thisEl.find(".timeRefHours").text("hora"); } else { thisEl.find(".timeRefHours").text("horas"); }
			if (minutes == 1) { thisEl.find(".timeRefMinutes").text("minuto"); } else { thisEl.find(".timeRefMinutes").text("minutos"); }
			if (seconds == 1) { thisEl.find(".timeRefSeconds").text("segundo"); } else { thisEl.find(".timeRefSeconds").text("segundos"); }

			//logic for the two_digits ON setting
			if(settings['format'] == "on") {
				days = (String(days).length >= 2) ? days : "0" + days;
				hours = (String(hours).length >= 2) ? hours : "0" + hours;
				minutes = (String(minutes).length >= 2) ? minutes : "0" + minutes;
				seconds = (String(seconds).length >= 2) ? seconds : "0" + seconds;
			}

			//update the countdown's html values.
			if(!isNaN(eventDate)) {
				thisEl.find(".days").text(days);
				thisEl.find(".hours").text(hours);
				thisEl.find(".minutes").text(minutes);
				thisEl.find(".seconds").text(seconds);
			} else {
				alert("Invalid date. Here's an example: 12 Tuesday 2012 17:30:00");
				clearInterval(interval);
			}
		}

		//run the function
		countdown_proc();

		//loop the function
		interval = setInterval(countdown_proc, 1000);

	}
}) (jQuery);
<|endoftext|>