<commit_before>'kiwi public';

import { trim } from 'lodash';

import state from '@/libs/state';
import formatIrcMessage, { createNewBlock } from '@/libs/MessageFormatter';
import { urlRegex, channelRegex } from '@/helpers/TextFormatting';

/**
 * Receives a message, parses its irc blocks, and then finds urls, users, channels and emoji. Each
 * content is extracted to a separate block.
 * E.g. the message:
 *   "this is a message www.google.com and #kiwiirc"
 * will be split into the blocks:
 *   ["this is a message "]["www.google.com"][" and "]["#kiwiirc"]
 * The special content blocks will also contain additional info about their content according to
 * their type, such as the url, nick colour, emoji code...
 * @param {Array} blocks Array of style blocks from MessageFormatter
 * @param {Array} userList List of users to find within the message
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
export default function parseMessage(message, formatOpts = {}, userList = null) {
    const emojiList = state.setting('emojis');

    const blocks = formatIrcMessage(message, formatOpts);
    let formatedBlocks = blocks.reduce(
        (acc, block, i) => acc.concat(processBlock(block, userList, emojiList)),
        []
    );

    return formatedBlocks;
}

/**
 * Receives a block, splits it into words and tries finding channels, urls, nicks, and emoji.
 * @param {Object} block A block that came from MessageFormatter.formatIrcMessage()
 * @param {Object} userList List of users to find within the message
 * @param {Object} emojiList List of emoji to find within the message
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
function processBlock(block, userList, emojiList) {
    const wordsRegex = /\S+/g;

    let wordMatch;
    let word;
    const specialMatches = [];
    // Array containing the special matches. Each `specialMatch` is an object with:
    // {
    //    index: <index of the match>
    //    match: <match, i.e. the text that will be extracted into a new block>
    //    block: <the block that will replace the match>
    // }

    // eslint-disable-next-line no-cond-assign
    while ((wordMatch = wordsRegex.exec(block.content)) !== null) {
        // `wordMatch` is an array with the match and the index of the match. We need that so
        // we can re-construct the original message.

        word = wordMatch[0];

        const match =
            matchChannel(word) ||
            matchUrl(word) ||
            matchUser(word, userList) ||
            matchEmoji(word, emojiList);

        if (match) {
            specialMatches.push({
                index: wordMatch.index + match.index,
                match: match.match,
                block: createNewBlock(
                    match.match,
                    block.styles,
                    match.type,
                    match.meta
                ),
            });
        }
    }

    // if there are no special matches, return the original block as is.
    if (specialMatches.length === 0) {
        return [block];
    }

    // split block with special matches.
    return mergeMatches(block, specialMatches);
}

/**
 * Finds a channel in the word match.
 * @param {String} word Word to be searched for channels.
 * @returns {object} Object with the matched channel, index within the word, and block.
 */
function matchChannel(word) {
    const channelMatch = channelRegex.exec(word);
    // matches the groups (spaces before)(prefix)(channel)(suffix punctuation)

    if (channelMatch === null) {
        return false;
    }

    return {
        index: channelMatch[1].length + channelMatch[2].length,
        match: channelMatch[3],
        type: 'channel',
        meta: {
            channel: channelMatch[3],
        },
    };
}

/**
 * Finds an url in the word match.
 * @param {String} word Word to be searched for urls.
 * @returns {Object} Object with the index of the url match in the block content, the
 * url match itself, and the new url text to be placed where the first url was.
 */
function matchUrl(word) {
    const urlMatch = urlRegex.exec(word);

    if (urlMatch === null) {
        return false;
    }

    let url = urlMatch[0];

    // Don't allow javascript execution
    if (url.match(/^javascript:/i)) {
        return false;
    }

    // Links almost always contain an opening bracket if the last character is a closing
    // bracket and should be part of the URL.
    // If there isn't an opening bracket but the URL ends in a closing bracket, consider the
    // closing bracket as punctuation outside of the URL.
    if (url.indexOf('(') === -1 && url[url.length - 1] === ')') {
        url = url.substr(0, url.length - 1);
    }

    // Add the http if no protocol was found
    let urlText = url;
    if (urlText.match(/^www\./i)) {
        urlText = 'http://' + url;
    }

    return {
        index: urlMatch.index,
        match: url,
        matchText: urlText,
        type: 'url',
        meta: {
            url: urlText,
        },
    };
}

/**
 * Finds an user in the word match.
 * @param {String} word Word to be searched for users.
 * @returns {Object} Object with the index of the user match in the block content, the
 * user match itself, and the user colour.
 */
function matchUser(word, userList) {
    if (!userList) {
        return false;
    }

    let user = null;
    let punc = ',.!:;-+)]?¿\\/<>@';
    let hasProp = Object.prototype.hasOwnProperty;
    let nickIdx = 0;

    const trimWord = trim(word, punc);
    let normWord = trimWord.toLowerCase();

    if (hasProp.call(userList, normWord)) {
        user = userList[normWord];
        nickIdx = word.indexOf(trimWord);
    } else {
        return false;
    }

    return {
        index: nickIdx,
        match: trimWord,
        type: 'user',
        meta: {
            user: trimWord,
            colour: user.colour,
        },
    };
}

/**
 * Finds an emoji in the word match.
 * @param {String} word Word to be searched for emoji.
 * @returns {Object} Object with the index of the emoji match in the block content, the
 * emoji match itself, and the emoji code.
 */
function matchEmoji(word, emojiList) {
    if (emojiList.hasOwnProperty && !emojiList.hasOwnProperty(word)) {
        return false;
    }

    return {
        index: 0,
        match: word,
        type: 'emoji',
        meta: {
            emoji: emojiList[word],
        },
    };
}

/**
 * Merges the special matches into the block.
 * @param {Object} block A block that came from MessageFormatter.formatIrcMessage()
 * @param {Array} specialMatches Array of special matches. See definition above
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
function mergeMatches(block, specialMatches) {
    const resultBlocks = [];
    let lastProcessedIndex = 0;

    // for each special content, creat a block with the text up to the match, and a block with the
    // special content itself.
    specialMatches.forEach((specialMatch, index) => {
        const beforeMatchContent = block.content.substr(
            lastProcessedIndex,
            specialMatch.index - lastProcessedIndex
        );

        if (beforeMatchContent) {
            resultBlocks.push({
                content: beforeMatchContent,
                styles: { ...block.styles },
                containsContent: !!beforeMatchContent,
            });
        }

        resultBlocks.push(specialMatch.block);

        lastProcessedIndex = specialMatch.index + specialMatch.match.length;
    });

    // remaining content after the last special content.
    if (lastProcessedIndex < block.content.length) {
        resultBlocks.push({
            content: block.content.substr(lastProcessedIndex),
            styles: { ...block.styles },
            containsContent: !!block.content.substr(lastProcessedIndex),
        });
    }

    return resultBlocks;
}
<commit_msg>Link detection: trim only trailing punctuation #1285<commit_after>'kiwi public';

import { trim } from 'lodash';

import state from '@/libs/state';
import formatIrcMessage, { createNewBlock } from '@/libs/MessageFormatter';
import { urlRegex, channelRegex } from '@/helpers/TextFormatting';

/**
 * Receives a message, parses its irc blocks, and then finds urls, users, channels and emoji. Each
 * content is extracted to a separate block.
 * E.g. the message:
 *   "this is a message www.google.com and #kiwiirc"
 * will be split into the blocks:
 *   ["this is a message "]["www.google.com"][" and "]["#kiwiirc"]
 * The special content blocks will also contain additional info about their content according to
 * their type, such as the url, nick colour, emoji code...
 * @param {Array} blocks Array of style blocks from MessageFormatter
 * @param {Array} userList List of users to find within the message
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
export default function parseMessage(message, formatOpts = {}, userList = null) {
    const emojiList = state.setting('emojis');

    const blocks = formatIrcMessage(message, formatOpts);
    let formatedBlocks = blocks.reduce(
        (acc, block, i) => acc.concat(processBlock(block, userList, emojiList)),
        []
    );

    return formatedBlocks;
}

/**
 * Receives a block, splits it into words and tries finding channels, urls, nicks, and emoji.
 * @param {Object} block A block that came from MessageFormatter.formatIrcMessage()
 * @param {Object} userList List of users to find within the message
 * @param {Object} emojiList List of emoji to find within the message
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
function processBlock(block, userList, emojiList) {
    const wordsRegex = /\S+/g;

    let wordMatch;
    let word;
    const specialMatches = [];
    // Array containing the special matches. Each `specialMatch` is an object with:
    // {
    //    index: <index of the match>
    //    match: <match, i.e. the text that will be extracted into a new block>
    //    block: <the block that will replace the match>
    // }

    // eslint-disable-next-line no-cond-assign
    while ((wordMatch = wordsRegex.exec(block.content)) !== null) {
        // `wordMatch` is an array with the match and the index of the match. We need that so
        // we can re-construct the original message.

        word = wordMatch[0];

        const match =
            matchChannel(word) ||
            matchUrl(word) ||
            matchUser(word, userList) ||
            matchEmoji(word, emojiList);

        if (match) {
            specialMatches.push({
                index: wordMatch.index + match.index,
                match: match.match,
                block: createNewBlock(
                    match.match,
                    block.styles,
                    match.type,
                    match.meta
                ),
            });
        }
    }

    // if there are no special matches, return the original block as is.
    if (specialMatches.length === 0) {
        return [block];
    }

    // split block with special matches.
    return mergeMatches(block, specialMatches);
}

/**
 * Finds a channel in the word match.
 * @param {String} word Word to be searched for channels.
 * @returns {object} Object with the matched channel, index within the word, and block.
 */
function matchChannel(word) {
    const channelMatch = channelRegex.exec(word);
    // matches the groups (spaces before)(prefix)(channel)(suffix punctuation)

    if (channelMatch === null) {
        return false;
    }

    return {
        index: channelMatch[1].length + channelMatch[2].length,
        match: channelMatch[3],
        type: 'channel',
        meta: {
            channel: channelMatch[3],
        },
    };
}

/**
 * Finds an url in the word match.
 * @param {String} word Word to be searched for urls.
 * @returns {Object} Object with the index of the url match in the block content, the
 * url match itself, and the new url text to be placed where the first url was.
 */
function matchUrl(word) {
    const urlMatch = urlRegex.exec(word);

    if (urlMatch === null) {
        return false;
    }

    let url = urlMatch[0];

    // Don't allow javascript execution
    if (url.match(/^javascript:/i)) {
        return false;
    }

    // Trim common punctuation from the end of a link. End of scentences etc.
    let punctuation = '.,;:';
    while (punctuation.indexOf(url[url.length - 1]) > -1) {
        url = url.substr(0, url.length - 1);
    }

    // Links almost always contain an opening bracket if the last character is a closing
    // bracket and should be part of the URL.
    // If there isn't an opening bracket but the URL ends in a closing bracket, consider the
    // closing bracket as punctuation outside of the URL.
    if (url.indexOf('(') === -1 && url[url.length - 1] === ')') {
        url = url.substr(0, url.length - 1);
    }

    // Add the http if no protocol was found
    let urlText = url;
    if (urlText.match(/^www\./i)) {
        urlText = 'http://' + url;
    }

    return {
        index: urlMatch.index,
        match: url,
        matchText: urlText,
        type: 'url',
        meta: {
            url: urlText,
        },
    };
}

/**
 * Finds an user in the word match.
 * @param {String} word Word to be searched for users.
 * @returns {Object} Object with the index of the user match in the block content, the
 * user match itself, and the user colour.
 */
function matchUser(word, userList) {
    if (!userList) {
        return false;
    }

    let user = null;
    let punc = ',.!:;-+)]?¿\\/<>@';
    let hasProp = Object.prototype.hasOwnProperty;
    let nickIdx = 0;

    const trimWord = trim(word, punc);
    let normWord = trimWord.toLowerCase();

    if (hasProp.call(userList, normWord)) {
        user = userList[normWord];
        nickIdx = word.indexOf(trimWord);
    } else {
        return false;
    }

    return {
        index: nickIdx,
        match: trimWord,
        type: 'user',
        meta: {
            user: trimWord,
            colour: user.colour,
        },
    };
}

/**
 * Finds an emoji in the word match.
 * @param {String} word Word to be searched for emoji.
 * @returns {Object} Object with the index of the emoji match in the block content, the
 * emoji match itself, and the emoji code.
 */
function matchEmoji(word, emojiList) {
    if (emojiList.hasOwnProperty && !emojiList.hasOwnProperty(word)) {
        return false;
    }

    return {
        index: 0,
        match: word,
        type: 'emoji',
        meta: {
            emoji: emojiList[word],
        },
    };
}

/**
 * Merges the special matches into the block.
 * @param {Object} block A block that came from MessageFormatter.formatIrcMessage()
 * @param {Array} specialMatches Array of special matches. See definition above
 * @returns An array of blocks, where each special content will be extracted into a separate block.
 */
function mergeMatches(block, specialMatches) {
    const resultBlocks = [];
    let lastProcessedIndex = 0;

    // for each special content, creat a block with the text up to the match, and a block with the
    // special content itself.
    specialMatches.forEach((specialMatch, index) => {
        const beforeMatchContent = block.content.substr(
            lastProcessedIndex,
            specialMatch.index - lastProcessedIndex
        );

        if (beforeMatchContent) {
            resultBlocks.push({
                content: beforeMatchContent,
                styles: { ...block.styles },
                containsContent: !!beforeMatchContent,
            });
        }

        resultBlocks.push(specialMatch.block);

        lastProcessedIndex = specialMatch.index + specialMatch.match.length;
    });

    // remaining content after the last special content.
    if (lastProcessedIndex < block.content.length) {
        resultBlocks.push({
            content: block.content.substr(lastProcessedIndex),
            styles: { ...block.styles },
            containsContent: !!block.content.substr(lastProcessedIndex),
        });
    }

    return resultBlocks;
}
<|endoftext|>


<commit_before>import {GUTTER, FLEX, CYCLE, calcDimension, addFlex} from './defaults';

export default function column(columnVal, cycle=CYCLE, gutter=GUTTER, flex=FLEX) {
  let style = {},
    cycleVal = cycle;

  if (columnVal !== 'none') {
    if (cycle === 'auto' || flex === 'flex' || flex === 'no-flex') {
      cycleVal = columnVal.split('/')[1];
    } else {
      cycleVal = cycle;
    }

    style.width = calcDimension(columnVal, gutter);

    if (flex === 'flex') {
      style = addFlex(flex, style);

      style['&:nth-child(n)'] = {
        marginRight: gutter
      };

      style['&:last-child'] = {
        marginRight: '0'
      };

      style[`&:nth-child(${cycleVal}n)`] = {
        float: 'right'
      };

      if (cycle !== 0) {
        style[`&:nth-child(${cycleVal}n)`] = {
          marginRight: '0'
        };
      }
    } else {
      style['&:nth-child(n)'] = {
        float: 'left',
        marginRight: gutter,
        clear: 'none'
      };

      style['&:last-child'] = {
        marginRight: '0'
      };

      style[`&:nth-child(${cycleVal}n)`] = {
        float: 'right'
      };

      if (cycle !== 0) {
        style[`&:nth-child(${cycleVal}n)`] = {
          marginRight: '0'
        };

        style[`&:nth-child(${cycleVal}n + 1)`] = {
          clear: 'left'
        };
      }
    }
  } else {
    style.width = 'auto';

    style['&:last-child'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(n)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(1n +1)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(1n)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };
  }

  return style;
}
<commit_msg>Correct column cycling.<commit_after>import {GUTTER, FLEX, CYCLE, calcDimension, addFlex} from './defaults';

export default function column(columnVal, cycle=CYCLE, gutter=GUTTER, flex=FLEX) {
  let style = {},
    cycleVal = cycle;

  if (columnVal !== 'none') {
    if (cycle === 'auto') {
      cycleVal = columnVal.split('/')[1];
    } else {
      cycleVal = cycle;
    }

    style.width = calcDimension(columnVal, gutter);

    if (flex === 'flex') {
      style = addFlex(flex, style);

      style['&:nth-child(n)'] = {
        marginRight: gutter
      };

      style['&:last-child'] = {
        marginRight: '0'
      };

      style[`&:nth-child(${cycleVal}n)`] = {
        float: 'right'
      };

      if (cycle !== 0) {
        style[`&:nth-child(${cycleVal}n)`] = {
          marginRight: '0'
        };
      }
    } else {
      style['&:nth-child(n)'] = {
        float: 'left',
        marginRight: gutter,
        clear: 'none'
      };

      style['&:last-child'] = {
        marginRight: '0'
      };

      style[`&:nth-child(${cycleVal}n)`] = {
        float: 'right'
      };

      if (cycle !== 0) {
        style[`&:nth-child(${cycleVal}n)`] = {
          marginRight: '0'
        };

        style[`&:nth-child(${cycleVal}n + 1)`] = {
          clear: 'left'
        };
      }
    }
  } else {
    style.width = 'auto';

    style['&:last-child'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(n)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(1n +1)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };

    style['&:nth-child(1n)'] = {
      float: 'none',
      clear: 'none',
      marginRight: '0',
      width: 'auto'
    };
  }

  return style;
}
<|endoftext|>


<commit_before>const cloudinary = require('cloudinary');
const {get, last, random, sum, sample, uniq} = require('lodash');
const levenshtein = require('fast-levenshtein');
const {google} = require('googleapis');
const {Mutex} = require('async-mutex');
const {xml2js} = require('xml-js');
const axios = require('axios');
const {Deferred} = require('../lib/utils.ts');
const {unlock, increment} = require('../achievements');

const animesDeferred = new Deferred();
const mutex = new Mutex();

const loadSheet = async () => {
	const auth = await new google.auth.GoogleAuth({
		scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
	}).getClient();
	const sheets = google.sheets({version: 'v4', auth});
	const {data: {values}} = await new Promise((resolve, reject) => {
		sheets.spreadsheets.values.get({
			spreadsheetId: '12YLDm-YqzWO3kL0ehZPKr9zF5WbYwLj31B_XsRIPb58',
			range: 'A:F',
		}, (error, response) => {
			if (error) {
				reject(error);
			} else {
				resolve(response);
			}
		});
	});
	const animes = values.map(([type, id, title, channel, animeTitle, count]) => ({
		type, id, title, channel, animeTitle, count: parseInt(count),
	}));

	animesDeferred.resolve(animes);
	return animesDeferred;
};

const getUrl = (publicId, options = {}) => (
	cloudinary.url(`${publicId}.jpg`, {
		private_cdn: false,
		secure: true,
		secure_distribution: 'res.cloudinary.com',
		...options,
	})
);

const getRandomThumb = async (answer) => {
	const animes = await animesDeferred.promise;
	const videos = animes.filter(({animeTitle}) => animeTitle === answer);
	const totalThumbs = sum(videos.map(({count}) => count));
	const thumbIndex = random(totalThumbs);
	let offset = 0;
	const video = videos.find(({count}) => {
		offset += count;
		return thumbIndex < offset;
	});

	const {data: filesXml} = await axios.get('https://hakata-thumbs.s3.amazonaws.com/', {
		params: {
			'list-type': 2,
			prefix: `${video.type}/${video.id}/`,
		},
	});
	const filesData = get(xml2js(filesXml, {compact: true}), ['ListBucketResult', 'Contents'], []);
	const filePath = get(sample(filesData), ['Key', '_text'], '');
	const {data: imageData} = await axios.get(`https://hakata-thumbs.s3.amazonaws.com/${filePath}`, {responseType: 'arraybuffer'});

	const cloudinaryDatum = await new Promise((resolve, reject) => {
		cloudinary.v2.uploader
			.upload_stream({resource_type: 'image'}, (error, data) => {
				if (error) {
					reject(error);
				} else {
					resolve(data);
				}
			})
			.end(imageData);
	});

	return {publicId: cloudinaryDatum.public_id, video, filename: last(filePath.split('/'))};
};

const getVideoInfo = (video, filename) => {
	const fileIndex = parseInt(filename.split('.')[0]);
	const seekTime = Math.floor((fileIndex + 0.5) * (video.type === 'niconico' ? 15 : 30));
	const hours = Math.floor(seekTime / 60 / 60);
	const minutes = Math.floor(seekTime / 60) % 60;
	const seconds = seekTime % 60;
	const timeText = hours === 0 ? `${minutes}分${seconds}秒～` : `${hours}時間${minutes}分${seconds}秒～`;

	if (video.type === 'lives') {
		return {
			title: `${video.title} (${timeText}) - ニコニコ生放送`,
			url: `https://live.nicovideo.jp/gate/${video.id}`,
		};
	}

	if (video.type === 'niconico') {
		return {
			title: `${video.title} (${timeText}) - ニコニコ動画`,
			url: `https://www.nicovideo.jp/watch/${video.id}?from=${seekTime}`,
		};
	}

	if (video.type === 'youtube') {
		return {
			title: `${video.title} (${timeText}) - YouTube`,
			url: `https://www.youtube.com/watch?v=${video.id}&t=${seekTime}`,
		};
	}

	return {
		title: '',
		url: '',
	};
};

const getHintText = (n) => {
	if (n <= 1) {
		return 'しょうがないにゃあ、ヒントだよ';
	}
	if (n <= 2) {
		return 'もう一つヒントだよ、早く答えてね';
	}
	if (n <= 3) {
		return 'まだわからないの？ヒント追加するからね';
	}
	return '最後のヒントだよ！もうわかるよね？';
};

const getHintOptions = (n) => {
	if (n <= 0) {
		return {
			transformation: [
				{width: 150},
				{effect: 'pixelate:6'},
			],
		};
	}
	if (n <= 1) {
		return {
			transformation: [
				{effect: 'pixelate:8'},
			],
		};
	}
	return {};
};

module.exports = ({rtmClient: rtm, webClient: slack}) => {
	const state = {
		answer: null,
		previousTick: 0,
		previousHint: 0,
		hints: [],
		thread: null,
	};

	const onTick = () => {
		mutex.runExclusive(async () => {
			const now = Date.now();
			const nextHint = state.previousHint + (state.hints.length === 5 ? 30 : 15) * 1000;

			if (state.answer !== null && nextHint <= now) {
				state.previousHint = now;
				if (state.hints.length < 5) {
					const {publicId, video, filename} = await getRandomThumb(state.answer);
					const hintText = getHintText(state.hints.length);

					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: hintText,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: [{
							image_url: getUrl(publicId, getHintOptions(state.hints.length)),
							fallback: hintText,
						}],
					});

					state.hints.push({publicId, video, filename});
				} else {
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: `もう、しっかりして！\n答えは＊${state.answer}＊だよ:anger:\nこれくらい常識だよね？`,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						reply_broadcast: true,
					});
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: '今回のヒント一覧だよ:anger:',
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: state.hints.map((hint) => {
							const info = getVideoInfo(hint.video, hint.filename);
							return {
								title: info.title,
								title_link: info.url,
								image_url: getUrl(hint.publicId),
								fallback: info.title,
							};
						}),
					});
					state.answer = null;
					state.previousHint = 0;
					state.hints = [];
					state.thread = null;
				}
			}
			state.previousTick = now;
		});
	};

	setInterval(onTick, 1000);

	rtm.on('message', (message) => {
		if (message.channel !== process.env.CHANNEL_SANDBOX) {
			return;
		}

		if (message.text === 'アニメ当てクイズ' && state.answer === null) {
			mutex.runExclusive(async () => {
				if (!animesDeferred.isResolved) {
					loadSheet();
				}
				const animes = await animesDeferred.promise;
				const animeTitles = uniq(animes.map(({animeTitle}) => animeTitle).filter((title) => title));
				const answer = sample(animeTitles);

				const {publicId, video, filename} = await getRandomThumb(answer);

				const {ts} = await slack.chat.postMessage({
					channel: process.env.CHANNEL_SANDBOX,
					text: 'このアニメなーんだ',
					username: 'anime',
					icon_emoji: ':tv:',
					attachments: [{
						image_url: getUrl(publicId, getHintOptions(0)),
						fallback: 'このアニメなーんだ',
					}],
				});

				state.thread = ts;
				state.hints.push({publicId, video, filename});
				state.previousHint = Date.now();

				await slack.chat.postMessage({
					channel: process.env.CHANNEL_SANDBOX,
					text: '15秒経過でヒントを出すよ♫',
					username: 'anime',
					icon_emoji: ':tv:',
					thread_ts: ts,
				});

				state.answer = answer;
			});
		}

		if (state.answer !== null && message.text && message.thread_ts === state.thread && message.username !== 'anime') {
			mutex.runExclusive(async () => {
				const distance = levenshtein.get(
					state.answer.replace(/\P{Letter}/gu, '').toLowerCase(),
					message.text.replace(/\P{Letter}/gu, '').toLowerCase(),
				);

				if (distance <= state.answer.replace(/\P{Letter}/gu, '').length / 3) {
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: `<@${message.user}> 正解:tada:\n答えは＊${state.answer}＊だよ:muscle:`,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						reply_broadcast: true,
					});
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: '今回のヒント一覧だよ',
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: state.hints.map((hint) => {
							const info = getVideoInfo(hint.video, hint.filename);
							return {
								title: info.title,
								title_link: info.url,
								image_url: getUrl(hint.publicId),
								fallback: info.title,
							};
						}),
					});

					await increment(message.user, 'anime-answer');
					if (state.hints.length === 1) {
						await increment(message.user, 'anime-answer-first-hint');
						await unlock(message.user, 'anime-answer-second-hint');
						await unlock(message.user, 'anime-answer-third-hint');
					} else if (state.hints.length === 2) {
						await unlock(message.user, 'anime-answer-second-hint');
						await unlock(message.user, 'anime-answer-third-hint');
					} else if (state.hints.length === 3) {
						await unlock(message.user, 'anime-answer-third-hint');
					}

					state.answer = null;
					state.previousHint = 0;
					state.hints = [];
					state.thread = null;
				} else {
					await slack.reactions.add({
						name: 'no_good',
						channel: message.channel,
						timestamp: message.ts,
					});
				}
			});
		}
	});
};
<commit_msg>Seperate conditional statements<commit_after>const cloudinary = require('cloudinary');
const {get, last, random, sum, sample, uniq} = require('lodash');
const levenshtein = require('fast-levenshtein');
const {google} = require('googleapis');
const {Mutex} = require('async-mutex');
const {xml2js} = require('xml-js');
const axios = require('axios');
const {Deferred} = require('../lib/utils.ts');
const {unlock, increment} = require('../achievements');

const animesDeferred = new Deferred();
const mutex = new Mutex();

const loadSheet = async () => {
	const auth = await new google.auth.GoogleAuth({
		scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
	}).getClient();
	const sheets = google.sheets({version: 'v4', auth});
	const {data: {values}} = await new Promise((resolve, reject) => {
		sheets.spreadsheets.values.get({
			spreadsheetId: '12YLDm-YqzWO3kL0ehZPKr9zF5WbYwLj31B_XsRIPb58',
			range: 'A:F',
		}, (error, response) => {
			if (error) {
				reject(error);
			} else {
				resolve(response);
			}
		});
	});
	const animes = values.map(([type, id, title, channel, animeTitle, count]) => ({
		type, id, title, channel, animeTitle, count: parseInt(count),
	}));

	animesDeferred.resolve(animes);
	return animesDeferred;
};

const getUrl = (publicId, options = {}) => (
	cloudinary.url(`${publicId}.jpg`, {
		private_cdn: false,
		secure: true,
		secure_distribution: 'res.cloudinary.com',
		...options,
	})
);

const getRandomThumb = async (answer) => {
	const animes = await animesDeferred.promise;
	const videos = animes.filter(({animeTitle}) => animeTitle === answer);
	const totalThumbs = sum(videos.map(({count}) => count));
	const thumbIndex = random(totalThumbs);
	let offset = 0;
	const video = videos.find(({count}) => {
		offset += count;
		return thumbIndex < offset;
	});

	const {data: filesXml} = await axios.get('https://hakata-thumbs.s3.amazonaws.com/', {
		params: {
			'list-type': 2,
			prefix: `${video.type}/${video.id}/`,
		},
	});
	const filesData = get(xml2js(filesXml, {compact: true}), ['ListBucketResult', 'Contents'], []);
	const filePath = get(sample(filesData), ['Key', '_text'], '');
	const {data: imageData} = await axios.get(`https://hakata-thumbs.s3.amazonaws.com/${filePath}`, {responseType: 'arraybuffer'});

	const cloudinaryDatum = await new Promise((resolve, reject) => {
		cloudinary.v2.uploader
			.upload_stream({resource_type: 'image'}, (error, data) => {
				if (error) {
					reject(error);
				} else {
					resolve(data);
				}
			})
			.end(imageData);
	});

	return {publicId: cloudinaryDatum.public_id, video, filename: last(filePath.split('/'))};
};

const getVideoInfo = (video, filename) => {
	const fileIndex = parseInt(filename.split('.')[0]);
	const seekTime = Math.floor((fileIndex + 0.5) * (video.type === 'niconico' ? 15 : 30));
	const hours = Math.floor(seekTime / 60 / 60);
	const minutes = Math.floor(seekTime / 60) % 60;
	const seconds = seekTime % 60;
	const timeText = hours === 0 ? `${minutes}分${seconds}秒～` : `${hours}時間${minutes}分${seconds}秒～`;

	if (video.type === 'lives') {
		return {
			title: `${video.title} (${timeText}) - ニコニコ生放送`,
			url: `https://live.nicovideo.jp/gate/${video.id}`,
		};
	}

	if (video.type === 'niconico') {
		return {
			title: `${video.title} (${timeText}) - ニコニコ動画`,
			url: `https://www.nicovideo.jp/watch/${video.id}?from=${seekTime}`,
		};
	}

	if (video.type === 'youtube') {
		return {
			title: `${video.title} (${timeText}) - YouTube`,
			url: `https://www.youtube.com/watch?v=${video.id}&t=${seekTime}`,
		};
	}

	return {
		title: '',
		url: '',
	};
};

const getHintText = (n) => {
	if (n <= 1) {
		return 'しょうがないにゃあ、ヒントだよ';
	}
	if (n <= 2) {
		return 'もう一つヒントだよ、早く答えてね';
	}
	if (n <= 3) {
		return 'まだわからないの？ヒント追加するからね';
	}
	return '最後のヒントだよ！もうわかるよね？';
};

const getHintOptions = (n) => {
	if (n <= 0) {
		return {
			transformation: [
				{width: 150},
				{effect: 'pixelate:6'},
			],
		};
	}
	if (n <= 1) {
		return {
			transformation: [
				{effect: 'pixelate:8'},
			],
		};
	}
	return {};
};

module.exports = ({rtmClient: rtm, webClient: slack}) => {
	const state = {
		answer: null,
		previousTick: 0,
		previousHint: 0,
		hints: [],
		thread: null,
	};

	const onTick = () => {
		mutex.runExclusive(async () => {
			const now = Date.now();
			const nextHint = state.previousHint + (state.hints.length === 5 ? 30 : 15) * 1000;

			if (state.answer !== null && nextHint <= now) {
				state.previousHint = now;
				if (state.hints.length < 5) {
					const {publicId, video, filename} = await getRandomThumb(state.answer);
					const hintText = getHintText(state.hints.length);

					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: hintText,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: [{
							image_url: getUrl(publicId, getHintOptions(state.hints.length)),
							fallback: hintText,
						}],
					});

					state.hints.push({publicId, video, filename});
				} else {
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: `もう、しっかりして！\n答えは＊${state.answer}＊だよ:anger:\nこれくらい常識だよね？`,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						reply_broadcast: true,
					});
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: '今回のヒント一覧だよ:anger:',
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: state.hints.map((hint) => {
							const info = getVideoInfo(hint.video, hint.filename);
							return {
								title: info.title,
								title_link: info.url,
								image_url: getUrl(hint.publicId),
								fallback: info.title,
							};
						}),
					});
					state.answer = null;
					state.previousHint = 0;
					state.hints = [];
					state.thread = null;
				}
			}
			state.previousTick = now;
		});
	};

	setInterval(onTick, 1000);

	rtm.on('message', (message) => {
		if (message.channel !== process.env.CHANNEL_SANDBOX) {
			return;
		}

		if (message.text === 'アニメ当てクイズ' && state.answer === null) {
			mutex.runExclusive(async () => {
				if (!animesDeferred.isResolved) {
					loadSheet();
				}
				const animes = await animesDeferred.promise;
				const animeTitles = uniq(animes.map(({animeTitle}) => animeTitle).filter((title) => title));
				const answer = sample(animeTitles);

				const {publicId, video, filename} = await getRandomThumb(answer);

				const {ts} = await slack.chat.postMessage({
					channel: process.env.CHANNEL_SANDBOX,
					text: 'このアニメなーんだ',
					username: 'anime',
					icon_emoji: ':tv:',
					attachments: [{
						image_url: getUrl(publicId, getHintOptions(0)),
						fallback: 'このアニメなーんだ',
					}],
				});

				state.thread = ts;
				state.hints.push({publicId, video, filename});
				state.previousHint = Date.now();

				await slack.chat.postMessage({
					channel: process.env.CHANNEL_SANDBOX,
					text: '15秒経過でヒントを出すよ♫',
					username: 'anime',
					icon_emoji: ':tv:',
					thread_ts: ts,
				});

				state.answer = answer;
			});
		}

		if (state.answer !== null && message.text && message.thread_ts === state.thread && message.username !== 'anime') {
			mutex.runExclusive(async () => {
				const distance = levenshtein.get(
					state.answer.replace(/\P{Letter}/gu, '').toLowerCase(),
					message.text.replace(/\P{Letter}/gu, '').toLowerCase(),
				);

				if (distance <= state.answer.replace(/\P{Letter}/gu, '').length / 3) {
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: `<@${message.user}> 正解:tada:\n答えは＊${state.answer}＊だよ:muscle:`,
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						reply_broadcast: true,
					});
					await slack.chat.postMessage({
						channel: process.env.CHANNEL_SANDBOX,
						text: '今回のヒント一覧だよ',
						username: 'anime',
						icon_emoji: ':tv:',
						thread_ts: state.thread,
						attachments: state.hints.map((hint) => {
							const info = getVideoInfo(hint.video, hint.filename);
							return {
								title: info.title,
								title_link: info.url,
								image_url: getUrl(hint.publicId),
								fallback: info.title,
							};
						}),
					});

					await increment(message.user, 'anime-answer');
					if (state.hints.length === 1) {
						await increment(message.user, 'anime-answer-first-hint');
					}
					if (state.hints.length <= 2) {
						await unlock(message.user, 'anime-answer-second-hint');
					}
					if (state.hints.length <= 3) {
						await unlock(message.user, 'anime-answer-third-hint');
					}

					state.answer = null;
					state.previousHint = 0;
					state.hints = [];
					state.thread = null;
				} else {
					await slack.reactions.add({
						name: 'no_good',
						channel: message.channel,
						timestamp: message.ts,
					});
				}
			});
		}
	});
};
<|endoftext|>


<commit_before>import Ember from 'ember';
import layout from '../templates/components/sl-grid';

/**
 * Valid values for the column definitions' `align` property
 *
 * @memberof module:components/sl-grid
 * @enum {String}
 */
export const ColumnAlign = Object.freeze({
    LEFT: 'left',
    RIGHT: 'right'
});

/**
 * Valid values for the column definitions' `size` property
 *
 * @memberof module:components/sl-grid
 * @enum {String}
 */
export const ColumnSize = Object.freeze({
    LARGE: 'large',
    MEDIUM: 'medium',
    SMALL: 'small'
});

/**
 * @module
 * @augments ember/Component
 */
export default Ember.Component.extend({

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNameBindings: [
        'detailPaneOpen:details-open',
        'loading:sl-loading'
    ],

    /** @type {String[]} */
    classNames: [
        'sl-grid'
    ],

    /** @type {Object} */
    layout,

    /** @type {String} */
    tagName: 'div',

    // -------------------------------------------------------------------------
    // Actions

    /** @type {Object} */
    actions: {

        /**
         * Handle changing pages
         *
         * @function actions:changePage
         * @param {Number} page - The page number being changed to
         * @returns {undefined}
         */
        changePage( page ) {
            if ( this.get( 'loading' ) ) {
                return;
            }

            const limit = this.get( 'pageSize' );
            const offset = limit * ( page - 1 );

            this.set( 'loading', true );
            this.sendAction( 'requestData', limit, offset );
        },

        /**
         * Close the detail-pane
         *
         * @function
         * @returns {undefined}
         */
        closeDetailPane() {
            const activeRecord = this.get( 'activeRecord' );

            if ( activeRecord ) {
                Ember.set( activeRecord, 'active', false );
                this.set( 'activeRecord', null );
            }

            this.set( 'detailPaneOpen', false );
            this.updateHeight();
        },

         /**
         * Handles drop button selection
         *
         * @function actions:dropButtonSelect
         * @param {Object} row - An object representing the row on which the drop button was selected
         * @param {String} actionName - The action to be sent
         * @returns {undefined}
         */
        dropButtonSelect( row, actionName ) {
            this.sendAction( actionName, row );
        },

        /**
         * Open the detail-pane with a specific row object
         *
         * @function
         * @param {Object} row - An object representing the row to make active
         * @returns {undefined}
         */
        openDetailPane( row ) {
            const activeRecord = this.get( 'activeRecord' );

            if ( activeRecord ) {
                Ember.set( activeRecord, 'active', false );
            }

            Ember.set( row, 'active', true );
            this.setProperties({
                activeRecord: row,
                detailPaneOpen: true
            });

            Ember.run.scheduleOnce( 'afterRender', () => {
                this.updateHeight();
            });
        },

        /**
         * Handle a list item's row click
         *
         * If an action is bound to the `rowClick` property, then it will be
         * called when this is triggered. Otherwise, the detail-pane will be
         * opened for the triggering row's model record, unless no detailPath is
         * defined.
         *
         * @function actions:rowClick
         * @param {Object} row - The object that the clicked row represents
         * @returns {undefined}
         */
        rowClick( row ) {
            if ( this.get( 'rowClick' ) ) {
                this.sendAction( 'rowClick', row );
            } else if ( this.get( 'detailComponent' ) ) {
                this.send( 'openDetailPane', row );
            }
        },

        /**
         * Toggle sorting of the selected column, and send the "sortAction"
         * bound action the column and direction to sort
         *
         * @function actions:sortColumn
         * @param {Object} column - The column definition for the triggered
         *        header's column
         * @returns {undefined}
         */
        sortColumn( column ) {
            if ( this.get( 'loading' ) ) {
                return;
            }

            const columnTitle = Ember.get( column, 'title' );
            const sortedColumn = this.get( 'sortedColumn' );
            const sortedColumnTitle = this.get( 'sortedColumnTitle' );
            let sortDirection = this.get( 'sortDirection' );

            if ( sortedColumnTitle === columnTitle ) {
                sortDirection = !sortDirection;
            } else {
                if ( sortedColumn ) {
                    Ember.set( sortedColumn, 'sortAscending', null );
                }

                this.set( 'sortedColumnTitle', columnTitle );
                sortDirection = true;
            }

            this.set( 'sortDirection', sortDirection );
            Ember.set( column, 'sortAscending', sortDirection );

            this.sendAction( 'sortColumn', column, sortDirection );
        },

        /**
         * Opens/closes the filter pane
         *
         * @function actions:toggleFilterPane
         * @returns {undefined}
         */
        toggleFilterPane() {
            this.toggleProperty( 'filterPaneOpen' );
            this.updateHeight();
        }
    },

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    /**
     * The text label for the rows' actions buttons
     *
     * @type {String}
     */
    actionsButtonLabel: 'Actions',

    /**
     * The row record that is currently active in the detail pane
     *
     * @type {?Object}
     */
    activeRecord: null,

    /**
     * @typedef ColumnDefinition
     * @type {Object}
     * @property {ColumnAlign} [align] - Which direction to align the
     *           column's content
     * @property {Boolean} [primary] - Whether the column is always shown
     * @property {Number|ColumnSize} [size] - The width of the column; either a
     *           number of pixels, or a ColumnSize value
     * @property {Boolean} [sortable] - Whether the column is able to be sorted
     * @property {String} [template] - Template name to use for the cell value;
     *           uses the `rowController` as its controller
     * @property {String} title - The displayed title of the column
     * @property {String} [valuePath] - Name of a property to lookup on the
     *           rows to populate the cell with
     */

    /**
     * @type {ColumnDefinition[]}
     */
    columns: [],

    /**
     * @type {?Object[]}
     */
    content: null,

    /**
     * Whether the grid's data should be handled by continuous-scrolling
     *
     * When this is false (default), then the grid will have pagination enabled.
     *
     * @type {Boolean}
     */
    continuous: false,

    /**
     * The current page, valid for a non-`continuous` grid
     *
     * @type {Number}
     */
    currentPage: 1,

    /**
     * The name of the component to render for the detail pane
     *
     * @type {?String}
     */
    detailComponent: null,

    /**
     * The name of the component to render for the detail-pane footer
     *
     * @type {?String}
     */
    detailFooterComponent: null,

    /**
     * The name of the component to render for the detail-pane header
     *
     * @type {?String}
     */
    detailHeaderComponent: null,

    /**
     * Indicates when the detail-pane is open
     *
     * @type {Boolean}
     */
    detailPaneOpen: false,

    /**
     * The text to display on the filter panel toggle button
     *
     * @type {String}
     */
    filterButtonLabel: 'Filter',

    /**
     * Indicates when the filter pane is open
     *
     * @type {Boolean}
     */
    filterPaneOpen: false,

    /**
     * The name of a component to use for the filter panel
     *
     * @type {?String}
     */
    filterComponent: null,

    /**
     * The path for the template to use for the footer of the list pane
     *
     * @type {?String}
     */
    footerPath: null,

    /**
     * The height of the overall grid
     *
     * When the value is set to "auto" (the default), then the sl-grid will size
     * its content to take up the maximum valid vertical space for the
     * current viewport.
     *
     * @type {Number|String}
     */
    height: 'auto',

    /**
     * When true, the split-grid is in a loading state
     *
     * @type {Boolean}
     */
    loading: false,

    /**
     * The "top" value for the table scroll to request a new page at
     *
     * @type {Number}
     */
    nextPageScrollPoint: 0,

    /**
     * The number of records to request for each page
     *
     * @type {Number}
     */
    pageSize: 25,

    /**
     * An array of action definitions to use for individual row actions
     *
     * Each item in this array should have the following properties:
     * - {String} action - The name of the action to trigger when this option
     *   is called
     * - {String} label - The displayed text for the option
     *
     * @type {?Object[]}
     */
    rowActions: null,

    /**
     * Bound action to call when a row is clicked
     *
     * When this value is not set, the detail pane will be opened whenever a row
     * is clicked.
     *
     * @type {?String}
     */
    rowClick: null,

    /**
     * Whether the currently sorted column is ascending or not
     *
     * @type {Boolean}
     */
    sortAscending: true,

    /**
     * The title of the column that is currently being sorted
     *
     * @type {?Object}
     */
    sortedColumnTitle: null,

    // -------------------------------------------------------------------------
    // Observers

    /**
     * Does cleanup for internal state when content length has changed
     *
     * @function
     * @returns {undefined}
     */
    handleNewContent: Ember.observer(
        'content.[]',
        function() {
            this.set( 'loading', false );

            if ( !this.get( 'hasMoreData' ) ) {
                this.disableContinuousPaging();
            }
        }
    ),

    /**
     * Setup the viewport-based auto sizing when `height` is "auto"
     *
     * @function
     * @returns {undefined}
     */
    setupAutoHeight: Ember.on(
        'didInsertElement',
        function() {
            if ( 'auto' === this.get( 'height' ) ) {
                Ember.$( window ).bind( 'resize', () => {
                    this.updateHeight();
                });
            }
        }
    ),

    /**
     * Setup the widths for column headers that were not given widths
     *
     * @function
     * @returns {undefined}
     */
    setupColumnHeaderWidths: Ember.on(
        'didInsertElement',
        function() {
            const colHeaders = $( '.list-pane .column-headers tr:first th' );
            this.$( '.list-pane .content > table tr:first td' ).each( function( index ) {
                colHeaders.eq( index ).width( $( this ).width() );
            });
        }
    ),

    /**
     * Setup the "continuous paging" functionality, if the data set is
     * not complete
     *
     * @function
     * @returns {undefined}
     */
    setupContinuousPaging: Ember.on(
        'didInsertElement',
        function() {
            if ( this.get( 'continuous' ) && this.get( 'hasMoreData' ) ) {
                this.enableContinuousPaging();
            }
        }
    ),

    /**
     * Update the panes' heights according to `height` property value
     *
     * @function
     * @returns {undefined}
     */
    updateHeight: Ember.on(
        'didInsertElement',
        function() {
            if ( !this.$() ) {
                return;
            }

            const componentHeight = this.get( 'height' );
            const gridHeader = this.$( '.grid-header' );
            const detailHeader = this.$( '.detail-pane header' );
            const detailFooter = this.$( '.detail-pane footer' );
            const listHeader = this.$( '.list-pane .column-headers' );
            const listFooter = this.$( '.list-pane footer' );

            const detailHeaderHeight = detailHeader ?
                parseInt( detailHeader.css( 'height' ) ) : 0;

            const detailFooterHeight = detailFooter ?
                parseInt( detailFooter.css( 'height' ) ) : 0;

            const gridHeaderHeight = gridHeader ?
                parseInt( gridHeader.css( 'height' ) ) : 0;

            const listHeaderHeight = listHeader ?
                parseInt( listHeader.css( 'height' ) ) : 0;

            const listFooterHeight = listFooter ?
                parseInt( listFooter.css( 'height' ) ) : 0;

            let maxHeight = componentHeight;
            if ( 'auto' === componentHeight ) {
                maxHeight = Ember.$( window ).innerHeight() -
                    this.$().position().top;
            }

            let detailContentHeight = maxHeight - gridHeaderHeight -
                detailHeaderHeight - detailFooterHeight;

            let listContentHeight = maxHeight - gridHeaderHeight -
                listHeaderHeight - listFooterHeight;

            if ( this.get( 'filterPaneOpen' ) ) {
                const filterPaneHeight = parseInt(
                    this.$( '.filter-pane' ).css( 'height' )
                );

                detailContentHeight -= filterPaneHeight;
                listContentHeight -= filterPaneHeight;
            }

            this.$( '.detail-pane .content' ).height( detailContentHeight );
            this.$( '.list-pane .content' ).height( listContentHeight );
        }
    ),

    // -------------------------------------------------------------------------
    // Methods

    /**
     * Whether to show the pagination in the list-pane footer
     *
     * @function
     * @returns {Boolean}
     */
    showPagination: Ember.computed(
        'continuous',
        'totalPages',
        function() {
            const totalPages = this.get( 'totalPages' );

            return !this.get( 'continuous' ) && totalPages && totalPages > 1;
        }
    ),

    /**
     * The currently sorted column definition
     *
     * @function
     * @returns {?Object} The definition for the currently sorted column
     */
    sortedColumn: Ember.computed(
        'columns',
        'sortedColumnTitle',
        function() {
            const sortedColumnTitle = this.get( 'sortedColumnTitle' );

            if ( sortedColumnTitle ) {
                const columns = this.get( 'columns' );

                for ( let i = 0; i < columns.length; i++ ) {
                    if (
                        Ember.get( columns[ i ], 'title' ) === sortedColumnTitle
                    ) {
                        return columns[ i ];
                    }
                }
            }

            return null;
        }
    ),

    /**
     * The total number of pages of bound content, based on pageSize
     *
     * @function
     * @returns {?Number}
     */
    totalPages: Ember.computed(
        'continuous',
        'pageSize',
        'totalCount',
        function() {
            if (
                !this.get( 'continuous' ) &&
                this.get( 'totalCount' ) &&
                this.get( 'pageSize' )
            ) {
                return Math.ceil(
                    this.get( 'totalCount' ) / this.get( 'pageSize' )
                );
            }

            return null;
        }
    ),

    /**
     * Disables the scroll event handling for continuous paging
     *
     * @function
     * @returns {undefined}
     */
    disableContinuousPaging() {
        this.$( '.list-pane .content' ).unbind( 'scroll' );
    },

    /**
     * Enables the scroll event handling for continuous paging
     *
     * @function
     * @returns {undefined}
     */
    enableContinuousPaging() {
        this.$( '.list-pane .content' ).bind( 'scroll', ( event ) => {
            this.handleListContentScroll( event );
        });
    },

    /**
     * For `continuous` grids; callback to the list content scrolling, which is
     * responsible for determining when triggering requestData is necessary by
     * checking the scroll location of the content
     *
     * @function
     * @param {jQuery.Event} event - The scroll trigger event
     * @returns {undefined}
     */
    handleListContentScroll( event ) {
        const listContent = this.$( event.target );
        const loading = this.get( 'loading' );
        const nextPageScrollPoint = this.get( 'nextPageScrollPoint' );
        const scrollBottom = listContent.scrollTop() + listContent.height();

        if ( scrollBottom >= nextPageScrollPoint && !loading ) {
            this.requestMoreData();
        }
    },

    /**
     * Whether the content has more available data to page in
     *
     * @function
     * @returns {Boolean} - True if more content pages are available
     */
    hasMoreData: Ember.computed(
        'content.length',
        'totalCount',
        function() {
            const contentLength = this.get( 'content.length' );
            const totalCount = this.get( 'totalCount' );

            if ( contentLength && totalCount ) {
                return contentLength < totalCount;
            }

            return false;
        }
    ),

    /**
     * Trigger the bound `requestData` action for more content data
     *
     * @function
     * @returns {undefined}
     */
    requestMoreData() {
        if ( this.get( 'hasMoreData' ) ) {
            const nextPageScrollPoint = this.$( '.list-pane .content' )[ 0 ]
                .scrollHeight;

            this.setProperties({
                'loading': true,
                nextPageScrollPoint
            });

            this.sendAction( 'requestData' );
        }
    }

});
<commit_msg>Closes softlayer/sl-ember-components#1019<commit_after>import Ember from 'ember';
import layout from '../templates/components/sl-grid';

/**
 * Valid values for the column definitions' `align` property
 *
 * @memberof module:components/sl-grid
 * @enum {String}
 */
export const ColumnAlign = Object.freeze({
    LEFT: 'left',
    RIGHT: 'right'
});

/**
 * Valid values for the column definitions' `size` property
 *
 * @memberof module:components/sl-grid
 * @enum {String}
 */
export const ColumnSize = Object.freeze({
    LARGE: 'large',
    MEDIUM: 'medium',
    SMALL: 'small'
});

/**
 * @module
 * @augments ember/Component
 */
export default Ember.Component.extend({

    // -------------------------------------------------------------------------
    // Dependencies

    // -------------------------------------------------------------------------
    // Attributes

    /** @type {String[]} */
    classNameBindings: [
        'detailPaneOpen:details-open',
        'loading:sl-loading'
    ],

    /** @type {String[]} */
    classNames: [
        'sl-grid'
    ],

    /** @type {Object} */
    layout,

    /** @type {String} */
    tagName: 'div',

    // -------------------------------------------------------------------------
    // Actions

    /** @type {Object} */
    actions: {

        /**
         * Handle changing pages
         *
         * @function actions:changePage
         * @param {Number} page - The page number being changed to
         * @returns {undefined}
         */
        changePage( page ) {
            if ( this.get( 'loading' ) ) {
                return;
            }

            const limit = this.get( 'pageSize' );
            const offset = limit * ( page - 1 );

            this.set( 'loading', true );
            this.sendAction( 'requestData', limit, offset );
        },

        /**
         * Close the detail-pane
         *
         * @function
         * @returns {undefined}
         */
        closeDetailPane() {
            const activeRecord = this.get( 'activeRecord' );

            if ( activeRecord ) {
                Ember.set( activeRecord, 'active', false );
                this.set( 'activeRecord', null );
            }

            this.set( 'detailPaneOpen', false );
            this.updateHeight();
        },

         /**
         * Handles drop button selection
         *
         * @function actions:dropButtonSelect
         * @param {Object} row - An object representing the row on which the drop button was selected
         * @param {String} actionName - The action to be sent
         * @returns {undefined}
         */
        dropButtonSelect( row, actionName ) {
            this.sendAction( actionName, row );
        },

        /**
         * Open the detail-pane with a specific row object
         *
         * @function
         * @param {Object} row - An object representing the row to make active
         * @returns {undefined}
         */
        openDetailPane( row ) {
            const activeRecord = this.get( 'activeRecord' );

            if ( activeRecord ) {
                Ember.set( activeRecord, 'active', false );
            }

            Ember.set( row, 'active', true );
            this.setProperties({
                activeRecord: row,
                detailPaneOpen: true
            });

            Ember.run.scheduleOnce( 'afterRender', () => {
                this.updateHeight();
            });
        },

        /**
         * Handle a list item's row click
         *
         * If an action is bound to the `rowClick` property, then it will be
         * called when this is triggered. Otherwise, the detail-pane will be
         * opened for the triggering row's model record, unless no detailPath is
         * defined.
         *
         * @function actions:rowClick
         * @param {Object} row - The object that the clicked row represents
         * @returns {undefined}
         */
        rowClick( row ) {
            if ( this.get( 'rowClick' ) ) {
                this.sendAction( 'rowClick', row );
            } else if ( this.get( 'detailComponent' ) ) {
                this.send( 'openDetailPane', row );
            }
        },

        /**
         * Toggle sorting of the selected column, and send the "sortAction"
         * bound action the column and direction to sort
         *
         * @function actions:sortColumn
         * @param {Object} column - The column definition for the triggered
         *        header's column
         * @returns {undefined}
         */
        sortColumn( column ) {
            if ( this.get( 'loading' ) ) {
                return;
            }

            const columnTitle = Ember.get( column, 'title' );
            const sortedColumn = this.get( 'sortedColumn' );
            const sortedColumnTitle = this.get( 'sortedColumnTitle' );
            let sortDirection = this.get( 'sortDirection' );

            if ( sortedColumnTitle === columnTitle ) {
                sortDirection = !sortDirection;
            } else {
                if ( sortedColumn ) {
                    Ember.set( sortedColumn, 'sortAscending', null );
                }

                this.set( 'sortedColumnTitle', columnTitle );
                sortDirection = true;
            }

            this.set( 'sortDirection', sortDirection );
            Ember.set( column, 'sortAscending', sortDirection );

            this.sendAction( 'sortColumn', column, sortDirection );
        },

        /**
         * Opens/closes the filter pane
         *
         * @function actions:toggleFilterPane
         * @returns {undefined}
         */
        toggleFilterPane() {
            this.toggleProperty( 'filterPaneOpen' );
            this.updateHeight();
        }
    },

    // -------------------------------------------------------------------------
    // Events

    // -------------------------------------------------------------------------
    // Properties

    /**
     * The text label for the rows' actions buttons
     *
     * @type {String}
     */
    actionsButtonLabel: 'Actions',

    /**
     * The row record that is currently active in the detail pane
     *
     * @type {?Object}
     */
    activeRecord: null,

    /**
     * @typedef ColumnDefinition
     * @type {Object}
     * @property {ColumnAlign} [align] - Which direction to align the
     *           column's content
     * @property {Boolean} [primary] - Whether the column is always shown
     * @property {Number|ColumnSize} [size] - The width of the column; either a
     *           number of pixels, or a ColumnSize value
     * @property {Boolean} [sortable] - Whether the column is able to be sorted
     * @property {String} [template] - Template name to use for the cell value;
     *           uses the `rowController` as its controller
     * @property {String} title - The displayed title of the column
     * @property {String} [valuePath] - Name of a property to lookup on the
     *           rows to populate the cell with
     */

    /**
     * @type {ColumnDefinition[]}
     */
    columns: [],

    /**
     * @type {?Object[]}
     */
    content: null,

    /**
     * Whether the grid's data should be handled by continuous-scrolling
     *
     * When this is false (default), then the grid will have pagination enabled.
     *
     * @type {Boolean}
     */
    continuous: false,

    /**
     * The current page, valid for a non-`continuous` grid
     *
     * @type {Number}
     */
    currentPage: 1,

    /**
     * The name of the component to render for the detail pane
     *
     * @type {?String}
     */
    detailComponent: null,

    /**
     * The name of the component to render for the detail-pane footer
     *
     * @type {?String}
     */
    detailFooterComponent: null,

    /**
     * The name of the component to render for the detail-pane header
     *
     * @type {?String}
     */
    detailHeaderComponent: null,

    /**
     * Indicates when the detail-pane is open
     *
     * @type {Boolean}
     */
    detailPaneOpen: false,

    /**
     * The text to display on the filter panel toggle button
     *
     * @type {String}
     */
    filterButtonLabel: 'Filter',

    /**
     * Indicates when the filter pane is open
     *
     * @type {Boolean}
     */
    filterPaneOpen: false,

    /**
     * The name of a component to use for the filter panel
     *
     * @type {?String}
     */
    filterComponent: null,

    /**
     * The path for the template to use for the footer of the list pane
     *
     * @type {?String}
     */
    footerPath: null,

    /**
     * The height of the overall grid
     *
     * When the value is set to "auto" (the default), then the sl-grid will size
     * its content to take up the maximum valid vertical space for the
     * current viewport.
     *
     * @type {Number|String}
     */
    height: 'auto',

    /**
     * When true, the split-grid is in a loading state
     *
     * @type {Boolean}
     */
    loading: false,

    /**
     * The "top" value for the table scroll to request a new page at
     *
     * @type {Number}
     */
    nextPageScrollPoint: 0,

    /**
     * The number of records to request for each page
     *
     * @type {Number}
     */
    pageSize: 25,

    /**
     * An array of action definitions to use for individual row actions
     *
     * Each item in this array should have the following properties:
     * - {String} action - The name of the action to trigger when this option
     *   is called
     * - {String} label - The displayed text for the option
     *
     * @type {?Object[]}
     */
    rowActions: null,

    /**
     * Bound action to call when a row is clicked
     *
     * When this value is not set, the detail pane will be opened whenever a row
     * is clicked.
     *
     * @type {?String}
     */
    rowClick: null,

    /**
     * Whether the currently sorted column is ascending or not
     *
     * @type {Boolean}
     */
    sortAscending: true,

    /**
     * The title of the column that is currently being sorted
     *
     * @type {?Object}
     */
    sortedColumnTitle: null,

    // -------------------------------------------------------------------------
    // Observers

    /**
     * Does cleanup for internal state when content length has changed
     *
     * @function
     * @returns {undefined}
     */
    handleNewContent: Ember.observer(
        'content.[]',
        function() {
            this.set( 'loading', false );

            if ( !this.get( 'hasMoreData' ) ) {
                this.disableContinuousPaging();
            }
        }
    ),

    /**
     * Setup the viewport-based auto sizing when `height` is "auto"
     *
     * @function
     * @returns {undefined}
     */
    setupAutoHeight: Ember.on(
        'didInsertElement',
        function() {
            if ( 'auto' === this.get( 'height' ) ) {
                Ember.$( window ).bind( 'resize', () => {
                    this.updateHeight();
                });
            }
        }
    ),

    /**
     * Setup the widths for column headers that were not given widths
     *
     * @function
     * @returns {undefined}
     */
    setupColumnHeaderWidths: Ember.on(
        'didInsertElement',
        function() {
            const colHeaders = this.$( '.list-pane .column-headers tr:first th' );
            this.$( '.list-pane .content > table tr:first td' ).each( function( index ) {
                colHeaders.eq( index ).width( $( this ).width() );
            });
        }
    ),

    /**
     * Setup the "continuous paging" functionality, if the data set is
     * not complete
     *
     * @function
     * @returns {undefined}
     */
    setupContinuousPaging: Ember.on(
        'didInsertElement',
        function() {
            if ( this.get( 'continuous' ) && this.get( 'hasMoreData' ) ) {
                this.enableContinuousPaging();
            }
        }
    ),

    /**
     * Update the panes' heights according to `height` property value
     *
     * @function
     * @returns {undefined}
     */
    updateHeight: Ember.on(
        'didInsertElement',
        function() {
            if ( !this.$() ) {
                return;
            }

            const componentHeight = this.get( 'height' );
            const gridHeader = this.$( '.grid-header' );
            const detailHeader = this.$( '.detail-pane header' );
            const detailFooter = this.$( '.detail-pane footer' );
            const listHeader = this.$( '.list-pane .column-headers' );
            const listFooter = this.$( '.list-pane footer' );

            const detailHeaderHeight = detailHeader ?
                parseInt( detailHeader.css( 'height' ) ) : 0;

            const detailFooterHeight = detailFooter ?
                parseInt( detailFooter.css( 'height' ) ) : 0;

            const gridHeaderHeight = gridHeader ?
                parseInt( gridHeader.css( 'height' ) ) : 0;

            const listHeaderHeight = listHeader ?
                parseInt( listHeader.css( 'height' ) ) : 0;

            const listFooterHeight = listFooter ?
                parseInt( listFooter.css( 'height' ) ) : 0;

            let maxHeight = componentHeight;
            if ( 'auto' === componentHeight ) {
                maxHeight = Ember.$( window ).innerHeight() -
                    this.$().position().top;
            }

            let detailContentHeight = maxHeight - gridHeaderHeight -
                detailHeaderHeight - detailFooterHeight;

            let listContentHeight = maxHeight - gridHeaderHeight -
                listHeaderHeight - listFooterHeight;

            if ( this.get( 'filterPaneOpen' ) ) {
                const filterPaneHeight = parseInt(
                    this.$( '.filter-pane' ).css( 'height' )
                );

                detailContentHeight -= filterPaneHeight;
                listContentHeight -= filterPaneHeight;
            }

            this.$( '.detail-pane .content' ).height( detailContentHeight );
            this.$( '.list-pane .content' ).height( listContentHeight );
        }
    ),

    // -------------------------------------------------------------------------
    // Methods

    /**
     * Whether to show the pagination in the list-pane footer
     *
     * @function
     * @returns {Boolean}
     */
    showPagination: Ember.computed(
        'continuous',
        'totalPages',
        function() {
            const totalPages = this.get( 'totalPages' );

            return !this.get( 'continuous' ) && totalPages && totalPages > 1;
        }
    ),

    /**
     * The currently sorted column definition
     *
     * @function
     * @returns {?Object} The definition for the currently sorted column
     */
    sortedColumn: Ember.computed(
        'columns',
        'sortedColumnTitle',
        function() {
            const sortedColumnTitle = this.get( 'sortedColumnTitle' );

            if ( sortedColumnTitle ) {
                const columns = this.get( 'columns' );

                for ( let i = 0; i < columns.length; i++ ) {
                    if (
                        Ember.get( columns[ i ], 'title' ) === sortedColumnTitle
                    ) {
                        return columns[ i ];
                    }
                }
            }

            return null;
        }
    ),

    /**
     * The total number of pages of bound content, based on pageSize
     *
     * @function
     * @returns {?Number}
     */
    totalPages: Ember.computed(
        'continuous',
        'pageSize',
        'totalCount',
        function() {
            if (
                !this.get( 'continuous' ) &&
                this.get( 'totalCount' ) &&
                this.get( 'pageSize' )
            ) {
                return Math.ceil(
                    this.get( 'totalCount' ) / this.get( 'pageSize' )
                );
            }

            return null;
        }
    ),

    /**
     * Disables the scroll event handling for continuous paging
     *
     * @function
     * @returns {undefined}
     */
    disableContinuousPaging() {
        this.$( '.list-pane .content' ).unbind( 'scroll' );
    },

    /**
     * Enables the scroll event handling for continuous paging
     *
     * @function
     * @returns {undefined}
     */
    enableContinuousPaging() {
        this.$( '.list-pane .content' ).bind( 'scroll', ( event ) => {
            this.handleListContentScroll( event );
        });
    },

    /**
     * For `continuous` grids; callback to the list content scrolling, which is
     * responsible for determining when triggering requestData is necessary by
     * checking the scroll location of the content
     *
     * @function
     * @param {jQuery.Event} event - The scroll trigger event
     * @returns {undefined}
     */
    handleListContentScroll( event ) {
        const listContent = this.$( event.target );
        const loading = this.get( 'loading' );
        const nextPageScrollPoint = this.get( 'nextPageScrollPoint' );
        const scrollBottom = listContent.scrollTop() + listContent.height();

        if ( scrollBottom >= nextPageScrollPoint && !loading ) {
            this.requestMoreData();
        }
    },

    /**
     * Whether the content has more available data to page in
     *
     * @function
     * @returns {Boolean} - True if more content pages are available
     */
    hasMoreData: Ember.computed(
        'content.length',
        'totalCount',
        function() {
            const contentLength = this.get( 'content.length' );
            const totalCount = this.get( 'totalCount' );

            if ( contentLength && totalCount ) {
                return contentLength < totalCount;
            }

            return false;
        }
    ),

    /**
     * Trigger the bound `requestData` action for more content data
     *
     * @function
     * @returns {undefined}
     */
    requestMoreData() {
        if ( this.get( 'hasMoreData' ) ) {
            const nextPageScrollPoint = this.$( '.list-pane .content' )[ 0 ]
                .scrollHeight;

            this.setProperties({
                'loading': true,
                nextPageScrollPoint
            });

            this.sendAction( 'requestData' );
        }
    }

});
<|endoftext|>


<commit_before>// takes an IR function object and returns a list of Scratch blocks

module.exports.ffi = {};

module.exports.generateFunctionHat = function(func) {
	var spec = func.funcName;
	var inputs = [];
	var defaults = [];

	for(var i = 0; i < func.paramList.length; ++i) {
		if(func.paramList[i][1])
			inputs.push(func.paramList[i][1]);
		else
			inputs.push("param"+i);

		defaults.push(defaultForType(func.paramList[i][0]));
		spec += " "+specifierForType(func.paramList[i][0]);
	}

	return ["procDef", spec, inputs, defaults, false];

}

module.exports.compileFunction = function(func) {
	console.log("Compiling "+JSON.stringify(func)+"...");

	var blockList = [module.exports.generateFunctionHat(func)]
					.concat(initLocal());

	for(var i = 0; i < func.code.length; ++i) {
		console.log(func.code[i]);
		if(func.code[i].type == "call") {
			// calling a (potentially foreign) function
			// stub

			console.log(func.code[i]);

			var spec = func.code[i].funcName;
			var args = [];

			for(var a = 0; a < func.code[i].paramList.length; ++a) {
				args.push(formatValue(func.code[i].paramList[a][0], func.code[i].paramList[a][1]));
				spec += " "+specifierForType(func.code[i].paramList[a][0]);
			}

			blockList.push(
				[
					"call",
					spec
				].concat(args)
			)
		} else if(func.code[i].type == "ffi") {
			// FFI block
			// load the code from the options
			blockList = blockList.concat(module.exports.ffi[func.code[i].ffiBlock]);
		}
	}

	blockList.concat(returnBlock());

	return blockList;
}

// fixme: stub
function defaultForType(type) {
	console.log(type);
	return 0;
}

// fixme: stub
function specifierForType(type) {
	return "%s";
}

// fixme: stub
function formatValue(type, value) {
	return value;
}

// higher-level code generation
function initLocal() {
	return [
		["append:toList:", "0", "# of locals"]
	]
}

function allocateLocal(val) {
	return [
		["setLine:ofList:to:", "last", "# of locals", ["+", ["getLine:ofList:", "last", "# of locals"], 1]],
		["append:toList:", val, "Stack"]
	];
}

function freeLocals() {
	return [
		["doRepeat", ["getLine:ofList:", "last", "# of locals"], [["deleteLine:ofList:", "last", "Stack"]]],
		["deleteLine:ofList:", "last", "# of locals"]
	];
}

function returnBlock(val) {
	var proc = freeLocals();

	if(val) {
		proc.push(["setVar:to:", "return value", val]);
	}

	proc.push(["stopScripts", "this script"]);

	return proc;
}<commit_msg>Bugfix with return block<commit_after>// takes an IR function object and returns a list of Scratch blocks

module.exports.ffi = {};

module.exports.generateFunctionHat = function(func) {
	var spec = func.funcName;
	var inputs = [];
	var defaults = [];

	for(var i = 0; i < func.paramList.length; ++i) {
		if(func.paramList[i][1])
			inputs.push(func.paramList[i][1]);
		else
			inputs.push("param"+i);

		defaults.push(defaultForType(func.paramList[i][0]));
		spec += " "+specifierForType(func.paramList[i][0]);
	}

	return ["procDef", spec, inputs, defaults, false];

}

module.exports.compileFunction = function(func) {
	console.log("Compiling "+JSON.stringify(func)+"...");

	var blockList = [module.exports.generateFunctionHat(func)]
					.concat(initLocal());

	for(var i = 0; i < func.code.length; ++i) {
		console.log(func.code[i]);
		if(func.code[i].type == "call") {
			// calling a (potentially foreign) function
			// stub

			console.log(func.code[i]);

			var spec = func.code[i].funcName;
			var args = [];

			for(var a = 0; a < func.code[i].paramList.length; ++a) {
				args.push(formatValue(func.code[i].paramList[a][0], func.code[i].paramList[a][1]));
				spec += " "+specifierForType(func.code[i].paramList[a][0]);
			}

			blockList.push(
				[
					"call",
					spec
				].concat(args)
			)
		} else if(func.code[i].type == "ffi") {
			// FFI block
			// load the code from the options
			blockList = blockList.concat(module.exports.ffi[func.code[i].ffiBlock]);
		}
	}

	blockList = blockList.concat(returnBlock());

	return blockList;
}

// fixme: stub
function defaultForType(type) {
	console.log(type);
	return 0;
}

// fixme: stub
function specifierForType(type) {
	return "%s";
}

// fixme: stub
function formatValue(type, value) {
	return value;
}

// higher-level code generation
function initLocal() {
	return [
		["append:toList:", "0", "# of locals"]
	]
}

function allocateLocal(val) {
	return [
		["setLine:ofList:to:", "last", "# of locals", ["+", ["getLine:ofList:", "last", "# of locals"], 1]],
		["append:toList:", val, "Stack"]
	];
}

function freeLocals() {
	return [
		["doRepeat", ["getLine:ofList:", "last", "# of locals"], [["deleteLine:ofList:", "last", "Stack"]]],
		["deleteLine:ofList:", "last", "# of locals"]
	];
}

function returnBlock(val) {
	var proc = freeLocals();

	if(val) {
		proc.push(["setVar:to:", "return value", val]);
	}

	proc.push(["stopScripts", "this script"]);

	return proc;
}<|endoftext|>


<commit_before>$(document).ready(function() {
    $('.draftbutt').on('click', function() {
	var dnum = $(this).parent().parent().next('.secondaryitems').attr('id');
	var mark = $(this).parent().parent().find('td :first').next().text().trim();
	var langSelect = whatLanguage(mark);

	$('#lansel').append('
'+dnum+'
');

	for (var langKey in langSelect) {
	    if (langSelect.hasOwnProperty(langKey)) {
		var langValue = langSelect[langKey];
		$('#lansel').append('<option val='+langKey+'>'+langValue+'</option>')
	    };
	};

	$('#modalreview2').modal();

	$('#loaddraft').on('click', function() {
	    window.location.replace('/mail/'+dnum+'/'+$('#lansel option:selected').attr('val')+'/');
	});
    });

    $(".glyphicon-ok").on("click", function(event) {
	event.stopPropagation();
	var itemNumber = $(this).parent().parent().attr("class").substr(10)
	var secItems = $(document).find(".secondaryitems."+itemNumber)
	var secondaryIdArray = new Array()

	$.each(secItems, function() {
	    secondaryIdArray.push(this.id)
	});

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'done',
		idarray: JSON.stringify(secondaryIdArray)
	    },
	    success: function(response) {
		if ( response.success === true ) {
		    window.location.replace("/overview/");
		} else {
		    alert(response.error);
		};
	    },
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });

    $(".glyphicon-remove").on("click", function(event) {
	event.stopPropagation();
	var itemNumber = $(this).parent().parent().attr("class").substr(10)
	var secItems = $(document).find(".secondaryitems."+itemNumber)
	var secondaryIdArray = new Array()

	$.each(secItems, function() {
	    secondaryIdArray.push(this.id)
	});

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'del_item',
		idarray: JSON.stringify(secondaryIdArray)
	    },
	    success: function(response) {
		if ( response.success === true ) {
		    window.location.replace("/overview/");
		} else {
		    alert(response.error);
		};
	    },
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });

    $('.mainitems, .secondaryitems').each(function() {
	var cleanclass = $(this).children('td:first').text().replace('/','').trim();
	$(this).addClass(cleanclass);
    });

    $('tr.mainitems').on('click', function() {
	var num = $(this).children("td:first").text().replace('/', '').trim();
	$('.secondaryitems.' + num).toggle();
    });

    $(document).on('click', '.editbutton', function() {
	var objId = parseInt($(this).parent().parent().attr('id'));

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'edit',
		id: objId
	    },
	    success: function(json_data) {
		modalEdit(json_data);
		},
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });
});

function modalEdit(dataEdit) {
    //populate the field with the values from DB
    changedItem = dataEdit;
    changedStuff = 0;
    var iid = dataEdit.itemid;

    $("#id_paidon").datepicker({dateFormat: 'yyyy-mm-dd'});
    $('h4').append(''+iid+'');
    $('#id_market').val(dataEdit.market);
    $('#id_ccode').show();
    $('#id_ccode').val(dataEdit.ccode);
    $('#dln').val(dataEdit.remindernumber);
    $('#invn').val(dataEdit.invoicenumber);
    $('#id_invoicestatus').val(dataEdit.invoicestatus);
    $('#id_rejectreason').val(dataEdit.rejectreason);
    $('#id_paidon').val(dataEdit.paidon);
    $("#id_amount").val(dataEdit.amount);
    $("#id_currency").val(dataEdit.currency);
    $('#vendm').val(dataEdit.mailvendor);
    $('#vendn').val(dataEdit.vendor);
    $('#id_level').val(dataEdit.level);
    $('#dld').val(dataEdit.reminderdate);
    $('#modalreview').modal();
    $('#dld').datepicker();

    $('#save').on('click', function() {
	var obj = {
	    itemid: $('#itemid').text(),
	    mailvendor: $('#vendm').val(),
	    paidon: $("#id_paidon").val(),
	    vendor: $('#vendn').val(),
	    ccode: $('#id_ccode').val(),
	    market: $('#id_market').val(),
	    remindernumber: $('#dln').val(),
	    invoicenumber: $('#invn').val(),
	    invoicestatus: $('#id_invoicestatus').val(),
	    amount: $("#id_amount").val(),
	    currency: $("#id_currency").val(),
	    //actiontaken: $('#id_actiontaken').val(),
	    rejectreason: $('#id_rejectreason').val(),
	    level: $('#id_level').val(),
	    reminderdate: $('#dld').val(),
	    };

	$.each(obj, function(key, value) {
	    if (key !== "success" || key !== "error") {
		if (obj[key] !== changedItem[key]) {
		    changedStuff++;
		};
	    };
	});
	if (changedStuff != 0) {Update(obj)};
    });
};

function Update(item) {
    $.ajaxSetup({
	type: 'POST',
	dataType: 'json'
    });

    //the real AJAX request
    $.ajax({
	url: '/ajax/',
	data: {
	    form_type: 'update',
	    mass_data: JSON.stringify(item)
	},
	success: window.location.replace("/overview/"),
	error: function (ajaxObj, textStatus, error) {
	    alert(error);
	}
    });
    return true;
}

function whatLanguage(market) {

    switch(market)
    {
	case "IT":
	var lObj = {EN: "English", IT: "Italian"};
	break;
	case "DE":
	var lObj = {DE: "German"};
	break;
	case "FI":
	var lObj = {EN: "English", FI: "Finnish"};
	break;
	case "SE":
	var lObj = {EN: "English", SE: "Swedish"};
	break;
	case "DK":
	var lObj = {EN: "English"};
	break;
	case "CH":
	var lObj = {DE: "German"};
	break;
	case "NO":
	var lObj = {EN: "English"};
	break;
	case "BE":
	var lObj = {NL: "Dutch"};
	break;
	case "NL":
	var lObj = {NL: "Dutch"};
	break;
    };

    return lObj;
};
<commit_msg>Fixed appending ID to modal box.<commit_after>$(document).ready(function() {
    $('.draftbutt').on('click', function() {
	var dnum = $(this).parent().parent().next('.secondaryitems').attr('id');
	var mark = $(this).parent().parent().find('td :first').next().text().trim();
	var langSelect = whatLanguage(mark);

	$('#lansel').append('
'+dnum+'
');

	for (var langKey in langSelect) {
	    if (langSelect.hasOwnProperty(langKey)) {
		var langValue = langSelect[langKey];
		$('#lansel').append('<option val='+langKey+'>'+langValue+'</option>')
	    };
	};

	$('#modalreview2').modal();

	$('#loaddraft').on('click', function() {
	    window.location.replace('/mail/'+dnum+'/'+$('#lansel option:selected').attr('val')+'/');
	});
    });

    $(".glyphicon-ok").on("click", function(event) {
	event.stopPropagation();
	var itemNumber = $(this).parent().parent().attr("class").substr(10)
	var secItems = $(document).find(".secondaryitems."+itemNumber)
	var secondaryIdArray = new Array()

	$.each(secItems, function() {
	    secondaryIdArray.push(this.id)
	});

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'done',
		idarray: JSON.stringify(secondaryIdArray)
	    },
	    success: function(response) {
		if ( response.success === true ) {
		    window.location.replace("/overview/");
		} else {
		    alert(response.error);
		};
	    },
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });

    $(".glyphicon-remove").on("click", function(event) {
	event.stopPropagation();
	var itemNumber = $(this).parent().parent().attr("class").substr(10)
	var secItems = $(document).find(".secondaryitems."+itemNumber)
	var secondaryIdArray = new Array()

	$.each(secItems, function() {
	    secondaryIdArray.push(this.id)
	});

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'del_item',
		idarray: JSON.stringify(secondaryIdArray)
	    },
	    success: function(response) {
		if ( response.success === true ) {
		    window.location.replace("/overview/");
		} else {
		    alert(response.error);
		};
	    },
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });

    $('.mainitems, .secondaryitems').each(function() {
	var cleanclass = $(this).children('td:first').text().replace('/','').trim();
	$(this).addClass(cleanclass);
    });

    $('tr.mainitems').on('click', function() {
	var num = $(this).children("td:first").text().replace('/', '').trim();
	$('.secondaryitems.' + num).toggle();
    });

    $(document).on('click', '.editbutton', function() {
	var objId = parseInt($(this).parent().parent().attr('id'));

	//setup the AJAX request
	$.ajaxSetup({
	    type: 'POST',
	    dataType: 'json'
	});

	//the real AJAX request
	$.ajax({
	    url: '/ajax/',
	    data: {
		form_type: 'edit',
		id: objId
	    },
	    success: function(json_data) {
		modalEdit(json_data);
		},
	    error: function (ajaxObj, textStatus, error) {
		alert(error);
	    }
	});
	return true;
    });
});

function modalEdit(dataEdit) {
    //populate the field with the values from DB
    changedItem = dataEdit;
    changedStuff = 0;
    var iid = dataEdit.itemid;

    $('h4:first').children().remove();
    $("#id_paidon").datepicker({dateFormat: 'yyyy-mm-dd'});
    $('h4:first').append(''+iid+'');
    $('#id_market').val(dataEdit.market);
    $('#id_ccode').show();
    $('#id_ccode').val(dataEdit.ccode);
    $('#dln').val(dataEdit.remindernumber);
    $('#invn').val(dataEdit.invoicenumber);
    $('#id_invoicestatus').val(dataEdit.invoicestatus);
    $('#id_rejectreason').val(dataEdit.rejectreason);
    $('#id_paidon').val(dataEdit.paidon);
    $("#id_amount").val(dataEdit.amount);
    $("#id_currency").val(dataEdit.currency);
    $('#vendm').val(dataEdit.mailvendor);
    $('#vendn').val(dataEdit.vendor);
    $('#id_level').val(dataEdit.level);
    $('#dld').val(dataEdit.reminderdate);
    $('#modalreview').modal();
    $('#dld').datepicker();

    $('#save').on('click', function() {
	var obj = {
	    itemid: $('#itemid').text(),
	    mailvendor: $('#vendm').val(),
	    paidon: $("#id_paidon").val(),
	    vendor: $('#vendn').val(),
	    ccode: $('#id_ccode').val(),
	    market: $('#id_market').val(),
	    remindernumber: $('#dln').val(),
	    invoicenumber: $('#invn').val(),
	    invoicestatus: $('#id_invoicestatus').val(),
	    amount: $("#id_amount").val(),
	    currency: $("#id_currency").val(),
	    //actiontaken: $('#id_actiontaken').val(),
	    rejectreason: $('#id_rejectreason').val(),
	    level: $('#id_level').val(),
	    reminderdate: $('#dld').val(),
	    };

	$.each(obj, function(key, value) {
	    if (key !== "success" || key !== "error") {
		if (obj[key] !== changedItem[key]) {
		    changedStuff++;
		};
	    };
	});
	if (changedStuff != 0) {Update(obj)};
    });
};

function Update(item) {
    $.ajaxSetup({
	type: 'POST',
	dataType: 'json'
    });

    //the real AJAX request
    $.ajax({
	url: '/ajax/',
	data: {
	    form_type: 'update',
	    mass_data: JSON.stringify(item)
	},
	success: window.location.replace("/overview/"),
	error: function (ajaxObj, textStatus, error) {
	    alert(error);
	}
    });
    return true;
}

function whatLanguage(market) {

    switch(market)
    {
	case "IT":
	var lObj = {EN: "English", IT: "Italian"};
	break;
	case "DE":
	var lObj = {DE: "German"};
	break;
	case "FI":
	var lObj = {EN: "English", FI: "Finnish"};
	break;
	case "SE":
	var lObj = {EN: "English", SE: "Swedish"};
	break;
	case "DK":
	var lObj = {EN: "English"};
	break;
	case "CH":
	var lObj = {DE: "German"};
	break;
	case "NO":
	var lObj = {EN: "English"};
	break;
	case "BE":
	var lObj = {NL: "Dutch"};
	break;
	case "NL":
	var lObj = {NL: "Dutch"};
	break;
    };

    return lObj;
};
<|endoftext|>


<commit_before>export default class DateRange {
  constructor() {
    this.now = new Date();
    this.startMinute = this.now.getMinutes();
    this.startHour = this.now.getHours();
    this.startDay = this.now.getDate();
    this.startMonth = this.now.getMonth();
    this.startYear = this.now.getFullYear();
    this.endMinute = this.startMinute;
    this.endHour = this.startHour;
    this.endDay = this.startDay;
    this.endMonth = this.startMonth;
    this.endYear = this.startYear + 1;
    this.stepValue = 1;
    this.stepType = 'month';
  }

  withStartMinute(minute) {
    this.startMinute = minute;
    return this;
  }

  withStartHour(hour) {
    this.startHour = hour;
    return this;
  }

  withStartDay(day) {
    this.startDay = day;
    return this;
  }

  withStartMonth(month) {
    this.startMonth = month;
    return this;
  }

  withStartYear(startYear) {
    this.startYear = startYear;
    return this;
  }

  withEndMinute(minute) {
    this.endMinute = minute;
    return this;
  }

  withEndHour(hour) {
    this.endHour = hour;
    return this;
  }

  withEndMonth(month) {
    this.endMonth = month;
    return this;
  }

  withEndYear(year) {
    this.endYear = year;
    return this;
  }

  stepEvery(stepValue, stepType) {
    this.stepValue = stepValue;
    this.stepType = stepType;
    return this;
  }

  get() {
    if (this.stepValue <= 0) {
      throw new Error('Step value should be more then 0');
    }
    const startDate = new Date(this.startYear, this.startMonth,
      this.startDay, this.startHour, this.startMinute);

    const endDate = new Date(this.endYear, this.endMonth, this.endDay,
      this.endHour, this.endMinute);
    const datesRange = [];
    while (startDate < endDate) {
      switch (this.stepType) {
        case 'minute' : {
          const newMinuteDate = new Date(startDate.setMinutes(startDate.getMinutes() + 1));
          if (newMinuteDate < endDate) {
            datesRange.push(newMinuteDate);
          }
          break;
        }
        case 'hour' : {
          const newHourDate = new Date(startDate.setHours(startDate.getHours() + 1));
          if (newHourDate < endDate) {
            datesRange.push(newHourDate);
          }
          break;
        }
        case 'day' : {
          const newDayDate = new Date(startDate.setDate(startDate.getDay() + 1));
          if (newDayDate < endDate) {
            datesRange.push(newDayDate);
          }
          break;
        }

        case 'month' : {
          const newMonthDate = new Date(startDate.setMonth(startDate.getMonth() + 1));
          if (newMonthDate < endDate) {
            datesRange.push(newMonthDate);
          }
          break;
        }
        case 'year' : {
          const newYearDate = new Date(startDate.setYear(startDate.getYear() + 1));
          if (newYearDate < endDate) {
            datesRange.push(newYearDate);
          }
          break;
        }
        default : {
          throw new Error('Please provide correct range type. ' +
            'Available options are : minute, hour, day, month, year');
        }
      }
    }

    return datesRange;
  }
}
<commit_msg>add possibility to get date range in string format<commit_after>import moment from 'moment';
export default class DateRange {
  constructor() {
    this.now = new Date();
    this.startMinute = this.now.getMinutes();
    this.startHour = this.now.getHours();
    this.startDay = this.now.getDate();
    this.startMonth = this.now.getMonth();
    this.startYear = this.now.getFullYear();
    this.endMinute = this.startMinute;
    this.endHour = this.startHour;
    this.endDay = this.startDay;
    this.endMonth = this.startMonth;
    this.endYear = this.startYear + 1;
    this.stepValue = 1;
    this.stepType = 'month';
    this.returnAsString = false;
    this.stringFormat = 'DD-MM-YYYY';
  }

  withStartMinute(minute) {
    this.startMinute = minute;
    return this;
  }

  withStartHour(hour) {
    this.startHour = hour;
    return this;
  }

  withStartDay(day) {
    this.startDay = day;
    return this;
  }

  withStartMonth(month) {
    this.startMonth = month;
    return this;
  }

  withStartYear(startYear) {
    this.startYear = startYear;
    return this;
  }

  withEndMinute(minute) {
    this.endMinute = minute;
    return this;
  }

  withEndHour(hour) {
    this.endHour = hour;
    return this;
  }

  withEndMonth(month) {
    this.endMonth = month;
    return this;
  }

  withEndYear(year) {
    this.endYear = year;
    return this;
  }

  stepEvery(stepValue, stepType) {
    this.stepValue = stepValue;
    this.stepType = stepType;
    return this;
  }

  asString(format) {
    this.returnAsString = true;
    if (format) {
      this.stringFormat = format;
    }
    return this;
  }

  get() {
    if (this.stepValue <= 0) {
      throw new Error('Step value should be more then 0');
    }
    const startDate = new Date(this.startYear, this.startMonth,
      this.startDay, this.startHour, this.startMinute);

    const endDate = new Date(this.endYear, this.endMonth, this.endDay,
      this.endHour, this.endMinute);
    const datesRange = [];
    while (startDate < endDate) {
      switch (this.stepType) {
        case 'minute' : {
          const newMinuteDate = new Date(startDate.setMinutes(startDate.getMinutes() + 1));
          if (newMinuteDate < endDate) {
            datesRange.push(newMinuteDate);
          }
          break;
        }
        case 'hour' : {
          const newHourDate = new Date(startDate.setHours(startDate.getHours() + 1));
          if (newHourDate < endDate) {
            datesRange.push(newHourDate);
          }
          break;
        }
        case 'day' : {
          const newDayDate = new Date(startDate.setDate(startDate.getDay() + 1));
          if (newDayDate < endDate) {
            datesRange.push(newDayDate);
          }
          break;
        }

        case 'month' : {
          const newMonthDate = new Date(startDate.setMonth(startDate.getMonth() + 1));
          if (newMonthDate < endDate) {
            datesRange.push(newMonthDate);
          }
          break;
        }
        case 'year' : {
          const newYearDate = new Date(startDate.setYear(startDate.getYear() + 1));
          if (newYearDate < endDate) {
            datesRange.push(newYearDate);
          }
          break;
        }
        default : {
          throw new Error('Please provide correct range type. ' +
            'Available options are : minute, hour, day, month, year');
        }
      }
    }
    if (this.returnAsString) {
      return datesRange.map(date => moment(date).format(this.stringFormat));
    }
    return datesRange;
  }
}
<|endoftext|>