<commit_before>'use strict';

import Promise from 'bluebird';

import Ui from './ui';
import Post from './post';
import Esa from './esa';

export default class Popup {
  constructor() {
    this.ui = new Ui();
    this.post = new Post();
    this.esa = undefined;
  }

  setPreviousPost() {
    Post.load((loadedPost) => {
      this.post = loadedPost;
      this.syncSaveButtonWithPost();

      this.ui.post = this.post;
      if (this.ui.title != '') {
        // Move cursor at previous position
        this.ui.cursorPosition = this.post.cursorPosition;
      }
    });
  }

  setPreviousState() {
    return new Promise((resolve, reject) => {
      let defaultConfig = {
        teamName: '',
        token: '',
        teamIcon: '',
      };
      chrome.storage.sync.get(defaultConfig, (config) => {
        if (!config.teamName || !config.token) {
          this.ui.toggleDisplayOptionLink(true);
          this.ui.team = { teamName: 'Configuration failed' };

          reject('Please configure options (\\( ˘⊖˘)/)');
        }

        this.esa = new Esa(config);

        this.ui.toggleDisplayOptionLink(false);
        this.ui.team = config;

        resolve();
      });
    });
  }

  setPreviousSavedPostLink() {
    return new Promise((resolve, reject) => {
      let defaultLinkData = {
        savedPostLink: '',
        postId: '',
      };
      chrome.storage.sync.get(defaultLinkData, (data) => {
        if (data.savedPostLink) {
          this.ui.savedPostLink = data.savedPostLink;
        } else {
          // NOTE: 0.2.1以前の後方互換性のための対応
          // TODO: 次々回のリリースで削除する
          // 合わせてこの関数で行っているsavedPostLinkの設定をsetPreviousPost()に統合する（Postの持つデータとしてsavedPostLinkを使う）
          this.ui.savedPostLink = `https://${this.ui.teamName}.esa.io/posts/${
            data.postId
          }`;
        }
        resolve();
      });
    });
  }

  setHooks(targetName, hooks) {
    let targetDom;
    switch (targetName) {
      case 'title':
        targetDom = this.ui.titleDom;
        break;
      case 'body':
        targetDom = this.ui.bodyDom;
        break;
      case 'post-button':
        targetDom = this.ui.postButtonDom;
        break;

      default:
        console.error(`error: invalid target '${targetName}' is specified`);
        return;
    }

    Object.keys(hooks).forEach((key) => {
      targetDom.on(key, hooks[key]);
    });
  }

  save() {
    this.ui.toggleDisabledSaveButton(true);
    this.ui.toggleUploadingStatus(true);

    this.searchTargetPostInEsa()
      .then(this.uploadPost.bind(this))
      .then(this.syncPostWithEsaResponse.bind(this))
      .then(this.syncUIWithPost.bind(this))
      .then(this.notifySuccess.bind(this))
      .catch(this.notifyError.bind(this))
      .finally(() => {
        this.ui.toggleUploadingStatus(false);
      });
  }

  // private
  // TODO: privateっぽい名前にする？
  searchTargetPostInEsa() {
    return new Promise((resolve, reject) => {
      let [category, title] = Post.splitCategory(this.ui.title);

      let q = `name:${title}`;
      if (category.length != 0) q = `${q} category:${category}`;

      let filterPostsFunc = (response) => {
        // nameによる検索は部分一致のため、完全一致させるために検索結果から更に絞り込んでいる
        let hitPost = Post.filterPosts(title, category, response);

        let postId = hitPost ? hitPost.number : undefined;
        resolve(postId);
      };

      this.esa.search(q, filterPostsFunc, reject);
    });
  }

  uploadPost(postId) {
    return new Promise((resolve, reject) => {
      let [category, title] = Post.splitCategory(this.ui.title);

      let postData = {
        id: postId,
        name: title,
        category: category,
        body_md: this.ui.body,
        message: 'from tsuibami',
      };

      this.esa.save(postData, resolve, reject);
    });
  }

  syncPostWithEsaResponse(response) {
    this.post.saved = true;
    this.post.savedPostLink = response.url;
    if (this.ui.isClearCheckBoxChecked) {
      this.post.title = '';
      this.post.body = '';
      this.post.cursorPosition = 0;
    } else {
      // 保存したタイトルによっては末尾に "(2)" などの表記がesaによってつけられていたり、タイトル無しで保存した場合にカテゴリが変わっている可能性があるため、内容を更新する
      this.post.title = response.full_name;
    }

    return new Promise((resolve, reject) => {
      this.post.store(
        () => {
          this.syncSaveButtonWithPost();
          resolve(response);
        },
        () => {
          reject(response);
        },
      );
    });
  }

  syncUIWithPost(response) {
    return new Promise((resolve, _reject) => {
      this.ui.post = this.post;

      this.ui.savedPostLink = this.post.savedPostLink;
      resolve(response);
    });
  }

  syncSaveButtonWithPost() {
    this.ui.toggleDisabledSaveButton(this.post.saved);
  }

  notifySuccess(response) {
    // 最初の保存の時だけメッセージを変える
    let message = response.revision_number == 1 ? 'created!' : 'updated!';
    this.showMessage(message + ' (\\( ⁰⊖⁰)/)', true);
  }

  notifyError(msg) {
    console.log(msg);
    let message = msg.hasOwnProperty('statusText') ? msg.statusText : msg;
    message = msg.hasOwnProperty('status')
      ? `${message} (${msg.status})`
      : message;
    this.showMessage(message);
  }

  showMessage(message, succeeded) {
    if (succeeded) {
      this.ui.messageArea.showMessageOnSuccess(message);
    } else {
      this.ui.messageArea.showMessageOnFailure(message);
    }
  }
}
<commit_msg>Use async/await for Popup#save<commit_after>'use strict';

import Promise from 'bluebird';

import Ui from './ui';
import Post from './post';
import Esa from './esa';

export default class Popup {
  constructor() {
    this.ui = new Ui();
    this.post = new Post();
    this.esa = undefined;
  }

  setPreviousPost() {
    Post.load((loadedPost) => {
      this.post = loadedPost;
      this.syncSaveButtonWithPost();

      this.ui.post = this.post;
      if (this.ui.title != '') {
        // Move cursor at previous position
        this.ui.cursorPosition = this.post.cursorPosition;
      }
    });
  }

  setPreviousState() {
    return new Promise((resolve, reject) => {
      let defaultConfig = {
        teamName: '',
        token: '',
        teamIcon: '',
      };
      chrome.storage.sync.get(defaultConfig, (config) => {
        if (!config.teamName || !config.token) {
          this.ui.toggleDisplayOptionLink(true);
          this.ui.team = { teamName: 'Configuration failed' };

          reject('Please configure options (\\( ˘⊖˘)/)');
        }

        this.esa = new Esa(config);

        this.ui.toggleDisplayOptionLink(false);
        this.ui.team = config;

        resolve();
      });
    });
  }

  setPreviousSavedPostLink() {
    return new Promise((resolve, reject) => {
      let defaultLinkData = {
        savedPostLink: '',
        postId: '',
      };
      chrome.storage.sync.get(defaultLinkData, (data) => {
        if (data.savedPostLink) {
          this.ui.savedPostLink = data.savedPostLink;
        } else {
          // NOTE: 0.2.1以前の後方互換性のための対応
          // TODO: 次々回のリリースで削除する
          // 合わせてこの関数で行っているsavedPostLinkの設定をsetPreviousPost()に統合する（Postの持つデータとしてsavedPostLinkを使う）
          this.ui.savedPostLink = `https://${this.ui.teamName}.esa.io/posts/${
            data.postId
          }`;
        }
        resolve();
      });
    });
  }

  setHooks(targetName, hooks) {
    let targetDom;
    switch (targetName) {
      case 'title':
        targetDom = this.ui.titleDom;
        break;
      case 'body':
        targetDom = this.ui.bodyDom;
        break;
      case 'post-button':
        targetDom = this.ui.postButtonDom;
        break;

      default:
        console.error(`error: invalid target '${targetName}' is specified`);
        return;
    }

    Object.keys(hooks).forEach((key) => {
      targetDom.on(key, hooks[key]);
    });
  }

  save() {
    this.ui.toggleDisabledSaveButton(true);
    this.ui.toggleUploadingStatus(true);

    (async () => {
      let postId = await this.searchTargetPostInEsa();
      let response = await this.uploadPost(postId);

      await this.syncPostWithEsaResponse(response);
      await this.syncUIWithPost(response);

      await this.notifySuccess(response);
    })()
      .catch((error) => this.notifyError(error))
      .finally(() => {
        this.ui.toggleUploadingStatus(false);
      });
  }

  // private
  // TODO: privateっぽい名前にする？
  searchTargetPostInEsa() {
    return new Promise((resolve, reject) => {
      let [category, title] = Post.splitCategory(this.ui.title);

      let q = `name:${title}`;
      if (category.length != 0) q = `${q} category:${category}`;

      let filterPostsFunc = (response) => {
        // nameによる検索は部分一致のため、完全一致させるために検索結果から更に絞り込んでいる
        let hitPost = Post.filterPosts(title, category, response);

        let postId = hitPost ? hitPost.number : undefined;
        resolve(postId);
      };

      this.esa.search(q, filterPostsFunc, reject);
    });
  }

  uploadPost(postId) {
    return new Promise((resolve, reject) => {
      let [category, title] = Post.splitCategory(this.ui.title);

      let postData = {
        id: postId,
        name: title,
        category: category,
        body_md: this.ui.body,
        message: 'from tsuibami',
      };

      this.esa.save(postData, resolve, reject);
    });
  }

  syncPostWithEsaResponse(response) {
    this.post.saved = true;
    this.post.savedPostLink = response.url;
    if (this.ui.isClearCheckBoxChecked) {
      this.post.title = '';
      this.post.body = '';
      this.post.cursorPosition = 0;
    } else {
      // 保存したタイトルによっては末尾に "(2)" などの表記がesaによってつけられていたり、タイトル無しで保存した場合にカテゴリが変わっている可能性があるため、内容を更新する
      this.post.title = response.full_name;
    }

    return new Promise((resolve, reject) => {
      this.post.store(
        () => {
          this.syncSaveButtonWithPost();
          resolve(response);
        },
        () => {
          reject(response);
        },
      );
    });
  }

  syncUIWithPost(response) {
    return new Promise((resolve, _reject) => {
      this.ui.post = this.post;

      this.ui.savedPostLink = this.post.savedPostLink;
      resolve(response);
    });
  }

  syncSaveButtonWithPost() {
    this.ui.toggleDisabledSaveButton(this.post.saved);
  }

  notifySuccess(response) {
    // 最初の保存の時だけメッセージを変える
    let message = response.revision_number == 1 ? 'created!' : 'updated!';
    this.showMessage(message + ' (\\( ⁰⊖⁰)/)', true);
  }

  notifyError(msg) {
    console.log(msg);
    let message = msg.hasOwnProperty('statusText') ? msg.statusText : msg;
    message = msg.hasOwnProperty('status')
      ? `${message} (${msg.status})`
      : message;
    this.showMessage(message);
  }

  showMessage(message, succeeded) {
    if (succeeded) {
      this.ui.messageArea.showMessageOnSuccess(message);
    } else {
      this.ui.messageArea.showMessageOnFailure(message);
    }
  }
}
<|endoftext|>


<commit_before>(() => {

  "use strict";

  let settings = {
    domain: "",
    token: {},
    draft: "",
    visibility: "public",
  };

  chrome.storage.local.get(settings, function(storage) {
    settings.domain = storage.domain;
    settings.token = storage.token;
    settings.draft = storage.draft;
    settings.visibility = storage.visibility;

    document.querySelector("#content").value = settings.draft;
    document.querySelector("#visibility").value = settings.visibility;
    init();
  });

  function init() {
    count();
    validateCount();

    document.querySelector("#toot").addEventListener("click", toot);
    document.querySelector("#cw").addEventListener("change", toggleSpoilerField);
    document.addEventListener("keyup", function(event) {
      count();
      validateCount();
      saveDraft();
    });
    document.addEventListener("keydown", function(event) {
      if (((event.ctrlKey && !event.metaKey) || (event.metaKey && !event.ctrlKey)) && event.key === 'Enter' && tootable()) {
        toot();
      }
      else if (event.metaKey && event.ctrlKey && event.keyCode === 32) {
        event.preventDefault();
        document.querySelector(".wdt-emoji-picker").click();
        document.querySelector("#wdt-emoji-search").focus();
      }

      refleshStatus();
    });
  }

  function count() {
    let content = stringToArray(document.querySelector("#content").value);
    let spoiler = stringToArray(document.querySelector("#spoiler-text").value);
    let left = 500 - (content.length + spoiler.length);

    document.querySelector("#words").textContent = left;
  }

  function validateCount() {
    let content = document.querySelector("#content").value.length;
    let spoiler = document.querySelector("#spoiler-text").value.length;
    let count = content + spoiler;

    if (count <= 0 || count > 500) {
      disableTootButton();
      warningCount();
      return false;
    }
    else if (content <= 0) {
      disableTootButton();
      return false;
    }
    else {
      enableTootButton();
      clearWarningCount();
      return true;
    }
  }

  function toot() {
    let xhr = new XMLHttpRequest();
    let url = settings.domain + "/api/v1/statuses";
    let token = settings.token;
    let content = document.querySelector("#content").value;
    let visibility = document.querySelector("#visibility").value;
    let spoiler = document.querySelector("#spoiler-text").value;

    xhr.open("POST", url, true);
    xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
    xhr.setRequestHeader("Authorization", "Bearer " + token.access_token);

    xhr.onreadystatechange = function(event) {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        document.querySelector("#content").value = "";
        saveDraft();
        document.querySelector("#spoiler-text").value = "";
        document.querySelector("#cw").checked = false;
        hideSpoiler();

        let status = document.querySelector("#status");
        status.classList.remove("text-danger");
        status.classList.add("text-success");
        status.textContent = "Tooted successfully!";

        validateCount();

        setTimeout(function() {
          status.textContent = "";
          status.classList.remove("text-success");
        }, 1500);
      }
      else if (xhr.readyState === XMLHttpRequest.DONE && xhr.status >= 400 && xhr.status < 600) {
        let status = document.querySelector("#status");
        status.classList.remove("text-success");
        status.classList.add("text-danger");

        if (settings.token.access_token) {
          status.textContent = "Toot failed";
        }
        else {
          status.innerHTML = 'Auth this app from options page';
        }

        validateCount();
      }
      else {
        // this may be OPTIONS method
      }
    }

    let isChecked = document.querySelector("#cw").checked;
    if (isChecked && spoiler === "") {
      document.querySelector("#spoiler-text").style.border = "1px solid #d9534f";
      return false;
    }
    else if (isChecked && spoiler !== "") {
      disableTootButton();
      xhr.send("status=" + encodeURIComponent(content) + "&visibility=" + visibility + "&spoiler_text=" + encodeURIComponent(spoiler));
    }
    else {
      disableTootButton();
      xhr.send("status=" + encodeURIComponent(content) + "&visibility=" + visibility);
    }

  }

  function saveDraft() {
    let content = document.querySelector("#content").value;
    chrome.storage.local.set({
      draft: content
    });
  }

  function validateFocus() {
    let active = document.activeElement;
    let content = document.querySelector("#content");
    let spoiler = document.querySelector("#spoiler-text");

    if (active === content || active === spoiler) {
      return true;
    }
    else {
      return false;
    }
  }

  function toggleSpoilerField() {
    let isChecked = document.querySelector("#cw").checked;
    if (isChecked) {
      showSpoiler();
    }
    else {
      hideSpoiler();
    }
  }

  function showSpoiler() {
    document.querySelector("#spoiler-text").style.display = "block";
    document.querySelector("#cw-hide-area").style.height = "0";
  }

  function hideSpoiler() {
    document.querySelector("#spoiler-text").value = "";
    count();
    validateCount();
    document.querySelector("#spoiler-text").style.display = "none";
    document.querySelector("#spoiler-text").style.border = "";
    document.querySelector("#cw-hide-area").style.height = "48px";
  }

  function disableTootButton() {
    let tootButton = document.querySelector("#toot");
    tootButton.setAttribute("disabled", "");
  }

  function enableTootButton() {
    let tootButton = document.querySelector("#toot");
    tootButton.removeAttribute("disabled");
  }

  function tootable() {
    let tootButton = document.querySelector("#toot");

    if (validateCount === false) {
      return false;
    }
    else if (validateFocus === false) {
      return false;
    }
    else if (tootButton.getAttribute("disabled") !== null) {
      return false;
    }
    else {
      return true;
    }
  }

  function warningCount() {
    let words = document.querySelector("#words");

    if (words.textContent !== "500") {
      words.style.color = "#d9534f";
    }
  }

  function clearWarningCount() {
    let words = document.querySelector("#words");
    words.style.color = "";
  }

  function refleshStatus() {
    let status = document.querySelector("#status");
    status.textContent = "";
    status.classList.remove("text-success");
    status.classList.remove("text-danger");
  }

  function stringToArray(str) {
    return str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
  }

})();
<commit_msg>:pencil2: Focus emoji search form when clicking emoji picker<commit_after>(() => {

  "use strict";

  let settings = {
    domain: "",
    token: {},
    draft: "",
    visibility: "public",
  };

  chrome.storage.local.get(settings, function(storage) {
    settings.domain = storage.domain;
    settings.token = storage.token;
    settings.draft = storage.draft;
    settings.visibility = storage.visibility;

    document.querySelector("#content").value = settings.draft;
    document.querySelector("#visibility").value = settings.visibility;
    init();
  });

  function init() {
    count();
    validateCount();

    document.querySelector("#toot").addEventListener("click", toot);
    document.querySelector("#cw").addEventListener("change", toggleSpoilerField);
    document.addEventListener("keyup", function(event) {
      count();
      validateCount();
      saveDraft();
    });
    document.addEventListener("keydown", function(event) {
      if (((event.ctrlKey && !event.metaKey) || (event.metaKey && !event.ctrlKey)) && event.key === 'Enter' && tootable()) {
        toot();
      }
      else if (event.metaKey && event.ctrlKey && event.keyCode === 32) {
        event.preventDefault();
        document.querySelector(".wdt-emoji-picker").click();
        document.querySelector("#wdt-emoji-search").focus();
      }

      refleshStatus();
    });
    document.querySelector(".wdt-emoji-picker").addEventListener("click", function() {
      document.querySelector("#wdt-emoji-search").focus();
    });
  }

  function count() {
    let content = stringToArray(document.querySelector("#content").value);
    let spoiler = stringToArray(document.querySelector("#spoiler-text").value);
    let left = 500 - (content.length + spoiler.length);

    document.querySelector("#words").textContent = left;
  }

  function validateCount() {
    let content = document.querySelector("#content").value.length;
    let spoiler = document.querySelector("#spoiler-text").value.length;
    let count = content + spoiler;

    if (count <= 0 || count > 500) {
      disableTootButton();
      warningCount();
      return false;
    }
    else if (content <= 0) {
      disableTootButton();
      return false;
    }
    else {
      enableTootButton();
      clearWarningCount();
      return true;
    }
  }

  function toot() {
    let xhr = new XMLHttpRequest();
    let url = settings.domain + "/api/v1/statuses";
    let token = settings.token;
    let content = document.querySelector("#content").value;
    let visibility = document.querySelector("#visibility").value;
    let spoiler = document.querySelector("#spoiler-text").value;

    xhr.open("POST", url, true);
    xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
    xhr.setRequestHeader("Authorization", "Bearer " + token.access_token);

    xhr.onreadystatechange = function(event) {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        document.querySelector("#content").value = "";
        saveDraft();
        document.querySelector("#spoiler-text").value = "";
        document.querySelector("#cw").checked = false;
        hideSpoiler();

        let status = document.querySelector("#status");
        status.classList.remove("text-danger");
        status.classList.add("text-success");
        status.textContent = "Tooted successfully!";

        validateCount();

        setTimeout(function() {
          status.textContent = "";
          status.classList.remove("text-success");
        }, 1500);
      }
      else if (xhr.readyState === XMLHttpRequest.DONE && xhr.status >= 400 && xhr.status < 600) {
        let status = document.querySelector("#status");
        status.classList.remove("text-success");
        status.classList.add("text-danger");

        if (settings.token.access_token) {
          status.textContent = "Toot failed";
        }
        else {
          status.innerHTML = 'Auth this app from options page';
        }

        validateCount();
      }
      else {
        // this may be OPTIONS method
      }
    }

    let isChecked = document.querySelector("#cw").checked;
    if (isChecked && spoiler === "") {
      document.querySelector("#spoiler-text").style.border = "1px solid #d9534f";
      return false;
    }
    else if (isChecked && spoiler !== "") {
      disableTootButton();
      xhr.send("status=" + encodeURIComponent(content) + "&visibility=" + visibility + "&spoiler_text=" + encodeURIComponent(spoiler));
    }
    else {
      disableTootButton();
      xhr.send("status=" + encodeURIComponent(content) + "&visibility=" + visibility);
    }

  }

  function saveDraft() {
    let content = document.querySelector("#content").value;
    chrome.storage.local.set({
      draft: content
    });
  }

  function validateFocus() {
    let active = document.activeElement;
    let content = document.querySelector("#content");
    let spoiler = document.querySelector("#spoiler-text");

    if (active === content || active === spoiler) {
      return true;
    }
    else {
      return false;
    }
  }

  function toggleSpoilerField() {
    let isChecked = document.querySelector("#cw").checked;
    if (isChecked) {
      showSpoiler();
    }
    else {
      hideSpoiler();
    }
  }

  function showSpoiler() {
    document.querySelector("#spoiler-text").style.display = "block";
    document.querySelector("#cw-hide-area").style.height = "0";
  }

  function hideSpoiler() {
    document.querySelector("#spoiler-text").value = "";
    count();
    validateCount();
    document.querySelector("#spoiler-text").style.display = "none";
    document.querySelector("#spoiler-text").style.border = "";
    document.querySelector("#cw-hide-area").style.height = "48px";
  }

  function disableTootButton() {
    let tootButton = document.querySelector("#toot");
    tootButton.setAttribute("disabled", "");
  }

  function enableTootButton() {
    let tootButton = document.querySelector("#toot");
    tootButton.removeAttribute("disabled");
  }

  function tootable() {
    let tootButton = document.querySelector("#toot");

    if (validateCount === false) {
      return false;
    }
    else if (validateFocus === false) {
      return false;
    }
    else if (tootButton.getAttribute("disabled") !== null) {
      return false;
    }
    else {
      return true;
    }
  }

  function warningCount() {
    let words = document.querySelector("#words");

    if (words.textContent !== "500") {
      words.style.color = "#d9534f";
    }
  }

  function clearWarningCount() {
    let words = document.querySelector("#words");
    words.style.color = "";
  }

  function refleshStatus() {
    let status = document.querySelector("#status");
    status.textContent = "";
    status.classList.remove("text-success");
    status.classList.remove("text-danger");
  }

  function stringToArray(str) {
    return str.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[^\uD800-\uDFFF]/g) || [];
  }

})();
<|endoftext|>


<commit_before>let TetherBase;
if (typeof TetherBase === 'undefined') {
  TetherBase = {modules: []};
}

let zeroElement = null;

function getScrollParents(el) {
  // In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;
  // https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  const computedStyle = getComputedStyle(el) || {};
  const position = computedStyle.position;
  let parents = [];

  if (position === 'fixed') {
    return [el];
  }

  let parent = el;
  while ((parent = parent.parentNode) && parent && parent.nodeType === 1) {
    let style;
    try {
      style = getComputedStyle(parent);
    } catch (err) {}

    if (typeof style === 'undefined' || style === null) {
      parents.push(parent);
      return parents;
    }

    const {overflow, overflowX, overflowY} = style;
    if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) {
      if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {
        parents.push(parent)
      }
    }
  }

  parents.push(document.body);
  return parents;
}

const uniqueId = (() => {
  let id = 0;
  return () => ++id;
})();

const zeroPosCache = {};
const getOrigin = () => {
  // getBoundingClientRect is unfortunately too accurate.  It introduces a pixel or two of
  // jitter as the user scrolls that messes with our ability to detect if two positions
  // are equivilant or not.  We place an element at the top left of the page that will
  // get the same jitter, so we can cancel the two out.
  let node = zeroElement;
  if (!node) {
    node = document.createElement('div');
    node.setAttribute('data-tether-id', uniqueId());
    extend(node.style, {
      top: 0,
      left: 0,
      position: 'absolute'
    });

    document.body.appendChild(node);

    zeroElement = node;
  }

  const id = node.getAttribute('data-tether-id');
  if (typeof zeroPosCache[id] === 'undefined') {
    zeroPosCache[id] = {};

    const rect = node.getBoundingClientRect();
    for (let k in rect) {
      // Can't use extend, as on IE9, elements don't resolve to be hasOwnProperty
      zeroPosCache[id][k] = rect[k];
    }

    // Clear the cache when this position call is done
    defer(() => {
      delete zeroPosCache[id];
    });
  }

  return zeroPosCache[id];
};

function removeUtilElements() {
  if (zeroElement) {
    document.body.removeChild(zeroElement);
  }
  zeroElement = null;
};

function getBounds(el) {
  let doc;
  if (el === document) {
    doc = document;
    el = document.documentElement;
  } else {
    doc = el.ownerDocument;
  }

  const docEl = doc.documentElement;

  const box = {};
  // The original object returned by getBoundingClientRect is immutable, so we clone it
  // We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
  const rect = el.getBoundingClientRect();
  for (let k in rect) {
    box[k] = rect[k];
  }

  // Take into account DOMElements that reside in another Frame by adjusting the
  // getBoundingClientRect properties by the outer Frame's getBoundingClientRect offset
  if (doc !== document) {
    let frameElement = doc.defaultView.frameElement;
    if (frameElement) {
      let frameRect = getBounds(frameElement);
      box.top += frameRect.top;
      box.bottom += frameRect.top;
      box.left += frameRect.left;
      box.right += frameRect.left;
    }
  }

  const origin = getOrigin();

  box.top -= origin.top;
  box.left -= origin.left;

  if (typeof box.width === 'undefined') {
    box.width = document.body.scrollWidth - box.left - box.right;
  }
  if (typeof box.height === 'undefined') {
    box.height = document.body.scrollHeight - box.top - box.bottom;
  }

  box.top = box.top - docEl.clientTop;
  box.left = box.left - docEl.clientLeft;
  box.right = doc.body.clientWidth - box.width - box.left;
  box.bottom = doc.body.clientHeight - box.height - box.top;

  return box;
}

function getOffsetParent(el) {
  return el.offsetParent || document.documentElement;
}

function getScrollBarSize() {
  const inner = document.createElement('div');
  inner.style.width = '100%';
  inner.style.height = '200px';

  const outer = document.createElement('div');
  extend(outer.style, {
    position: 'absolute',
    top: 0,
    left: 0,
    pointerEvents: 'none',
    visibility: 'hidden',
    width: '200px',
    height: '150px',
    overflow: 'hidden'
  });

  outer.appendChild(inner);

  document.body.appendChild(outer);

  const widthContained = inner.offsetWidth;
  outer.style.overflow = 'scroll';
  let widthScroll = inner.offsetWidth;

  if (widthContained === widthScroll) {
    widthScroll = outer.clientWidth;
  }

  document.body.removeChild(outer);

  const width = widthContained - widthScroll;

  return {width, height: width};
}

function extend(out={}) {
  const args = [];

  Array.prototype.push.apply(args, arguments);

  args.slice(1).forEach(obj => {
    if (obj) {
      for (let key in obj) {
        if ({}.hasOwnProperty.call(obj, key)) {
          out[key] = obj[key];
        }
      }
    }
  });

  return out;
}

function removeClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    name.split(' ').forEach(cls => {
      if (cls.trim()) {
        el.classList.remove(cls);
      }
    });
  } else {
    const regex = new RegExp(`(^| )${ name.split(' ').join('|') }( |$)`, 'gi');
    const className = getClassName(el).replace(regex, ' ');
    setClassName(el, className);
  }
}

function addClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    name.split(' ').forEach(cls => {
      if (cls.trim()) {
        el.classList.add(cls);
      }
    });
  } else {
    removeClass(el, name);
    const cls = getClassName(el) + ` ${name}`;
    setClassName(el, cls);
  }
}

function hasClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    return el.classList.contains(name);
  }
  const className = getClassName(el);
  return new RegExp(`(^| )${ name }( |$)`, 'gi').test(className);
}

function getClassName(el) {
  if (el.className instanceof SVGAnimatedString) {
    return el.className.baseVal;
  }
  return el.className;
}

function setClassName(el, className) {
  el.setAttribute('class', className);
}

function updateClasses(el, add, all) {
  // Of the set of 'all' classes, we need the 'add' classes, and only the
  // 'add' classes to be set.
  all.forEach(cls => {
    if (add.indexOf(cls) === -1 && hasClass(el, cls)) {
      removeClass(el, cls);
    }
  });

  add.forEach(cls => {
    if (!hasClass(el, cls)) {
      addClass(el, cls);
    }
  });
}

const deferred = [];

const defer = (fn) => {
  deferred.push(fn);
};

const flush = () => {
  let fn;
  while(fn = deferred.pop()) {
    fn();
  }
};

class Evented {
  on(event, handler, ctx, once=false) {
    if (typeof this.bindings === 'undefined') {
      this.bindings = {};
    }
    if (typeof this.bindings[event] === 'undefined') {
      this.bindings[event] = [];
    }
    this.bindings[event].push({handler, ctx, once});
  }

  once(event, handler, ctx) {
    this.on(event, handler, ctx, true);
  }

  off(event, handler) {
    if (typeof this.bindings === 'undefined' ||
        typeof this.bindings[event] === 'undefined') {
      return;
    }

    if (typeof handler === 'undefined') {
      delete this.bindings[event];
    } else {
      let i = 0;
      while (i < this.bindings[event].length) {
        if (this.bindings[event][i].handler === handler) {
          this.bindings[event].splice(i, 1);
        } else {
          ++i;
        }
      }
    }
  }

  trigger(event, ...args) {
    if (typeof this.bindings !== 'undefined' && this.bindings[event]) {
      let i = 0;
      while (i < this.bindings[event].length) {
        const {handler, ctx, once} = this.bindings[event][i];

        let context = ctx;
        if (typeof context === 'undefined') {
          context = this;
        }

        handler.apply(context, args);

        if (once) {
          this.bindings[event].splice(i, 1);
        } else {
          ++i;
        }
      }
    }
  }
}

TetherBase.Utils = {
  getScrollParents,
  getBounds,
  getOffsetParent,
  extend,
  addClass,
  removeClass,
  hasClass,
  updateClasses,
  defer,
  flush,
  uniqueId,
  Evented,
  getScrollBarSize,
  removeUtilElements
};
<commit_msg>Update utils.js<commit_after>let TetherBase;
if (typeof TetherBase === 'undefined') {
  TetherBase = {modules: []};
}

let zeroElement = null;

function getScrollParents(el) {
  // In firefox if the el is inside an iframe with display: none; window.getComputedStyle() will return null;
  // https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  const computedStyle = getComputedStyle(el) || {};
  const position = computedStyle.position;
  let parents = [];

  if (position === 'fixed') {
    return [el];
  }

  let parent = el;
  while ((parent = parent.parentNode) && parent && parent.nodeType === 1) {
    let style;
    try {
      style = getComputedStyle(parent);
    } catch (err) {}

    if (typeof style === 'undefined' || style === null) {
      parents.push(parent);
      return parents;
    }

    const {overflow, overflowX, overflowY} = style;
    if (/(auto|scroll)/.test(overflow + overflowY + overflowX)) {
      if (position !== 'absolute' || ['relative', 'absolute', 'fixed'].indexOf(style.position) >= 0) {
        parents.push(parent)
      }
    }
  }

  parents.push(el.ownerDocument.body);

  // If the node is within a frame, account for the parent window scroll
  if (el.ownerDocument !== document) {
    parents.push(el.ownerDocument.defaultView);
  }

  return parents;
}

const uniqueId = (() => {
  let id = 0;
  return () => ++id;
})();

const zeroPosCache = {};
const getOrigin = () => {
  // getBoundingClientRect is unfortunately too accurate.  It introduces a pixel or two of
  // jitter as the user scrolls that messes with our ability to detect if two positions
  // are equivilant or not.  We place an element at the top left of the page that will
  // get the same jitter, so we can cancel the two out.
  let node = zeroElement;
  if (!node) {
    node = document.createElement('div');
    node.setAttribute('data-tether-id', uniqueId());
    extend(node.style, {
      top: 0,
      left: 0,
      position: 'absolute'
    });

    document.body.appendChild(node);

    zeroElement = node;
  }

  const id = node.getAttribute('data-tether-id');
  if (typeof zeroPosCache[id] === 'undefined') {
    zeroPosCache[id] = {};

    const rect = node.getBoundingClientRect();
    for (let k in rect) {
      // Can't use extend, as on IE9, elements don't resolve to be hasOwnProperty
      zeroPosCache[id][k] = rect[k];
    }

    // Clear the cache when this position call is done
    defer(() => {
      delete zeroPosCache[id];
    });
  }

  return zeroPosCache[id];
};

function removeUtilElements() {
  if (zeroElement) {
    document.body.removeChild(zeroElement);
  }
  zeroElement = null;
};

function getBounds(el) {
  let doc;
  if (el === document) {
    doc = document;
    el = document.documentElement;
  } else {
    doc = el.ownerDocument;
  }

  const docEl = doc.documentElement;

  const box = {};
  // The original object returned by getBoundingClientRect is immutable, so we clone it
  // We can't use extend because the properties are not considered part of the object by hasOwnProperty in IE9
  const rect = el.getBoundingClientRect();
  for (let k in rect) {
    box[k] = rect[k];
  }

  // Take into account DOMElements that reside in another Frame by adjusting the
  // getBoundingClientRect properties by the outer Frame's getBoundingClientRect offset
  if (doc !== document) {
    let frameElement = doc.defaultView.frameElement;
    if (frameElement) {
      let frameRect = getBounds(frameElement);
      box.top += frameRect.top;
      box.bottom += frameRect.top;
      box.left += frameRect.left;
      box.right += frameRect.left;
    }
  }

  const origin = getOrigin();

  box.top -= origin.top;
  box.left -= origin.left;

  if (typeof box.width === 'undefined') {
    box.width = document.body.scrollWidth - box.left - box.right;
  }
  if (typeof box.height === 'undefined') {
    box.height = document.body.scrollHeight - box.top - box.bottom;
  }

  box.top = box.top - docEl.clientTop;
  box.left = box.left - docEl.clientLeft;
  box.right = doc.body.clientWidth - box.width - box.left;
  box.bottom = doc.body.clientHeight - box.height - box.top;

  return box;
}

function getOffsetParent(el) {
  return el.offsetParent || document.documentElement;
}

function getScrollBarSize() {
  const inner = document.createElement('div');
  inner.style.width = '100%';
  inner.style.height = '200px';

  const outer = document.createElement('div');
  extend(outer.style, {
    position: 'absolute',
    top: 0,
    left: 0,
    pointerEvents: 'none',
    visibility: 'hidden',
    width: '200px',
    height: '150px',
    overflow: 'hidden'
  });

  outer.appendChild(inner);

  document.body.appendChild(outer);

  const widthContained = inner.offsetWidth;
  outer.style.overflow = 'scroll';
  let widthScroll = inner.offsetWidth;

  if (widthContained === widthScroll) {
    widthScroll = outer.clientWidth;
  }

  document.body.removeChild(outer);

  const width = widthContained - widthScroll;

  return {width, height: width};
}

function extend(out={}) {
  const args = [];

  Array.prototype.push.apply(args, arguments);

  args.slice(1).forEach(obj => {
    if (obj) {
      for (let key in obj) {
        if ({}.hasOwnProperty.call(obj, key)) {
          out[key] = obj[key];
        }
      }
    }
  });

  return out;
}

function removeClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    name.split(' ').forEach(cls => {
      if (cls.trim()) {
        el.classList.remove(cls);
      }
    });
  } else {
    const regex = new RegExp(`(^| )${ name.split(' ').join('|') }( |$)`, 'gi');
    const className = getClassName(el).replace(regex, ' ');
    setClassName(el, className);
  }
}

function addClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    name.split(' ').forEach(cls => {
      if (cls.trim()) {
        el.classList.add(cls);
      }
    });
  } else {
    removeClass(el, name);
    const cls = getClassName(el) + ` ${name}`;
    setClassName(el, cls);
  }
}

function hasClass(el, name) {
  if (typeof el.classList !== 'undefined') {
    return el.classList.contains(name);
  }
  const className = getClassName(el);
  return new RegExp(`(^| )${ name }( |$)`, 'gi').test(className);
}

function getClassName(el) {
  if (el.className instanceof SVGAnimatedString) {
    return el.className.baseVal;
  }
  return el.className;
}

function setClassName(el, className) {
  el.setAttribute('class', className);
}

function updateClasses(el, add, all) {
  // Of the set of 'all' classes, we need the 'add' classes, and only the
  // 'add' classes to be set.
  all.forEach(cls => {
    if (add.indexOf(cls) === -1 && hasClass(el, cls)) {
      removeClass(el, cls);
    }
  });

  add.forEach(cls => {
    if (!hasClass(el, cls)) {
      addClass(el, cls);
    }
  });
}

const deferred = [];

const defer = (fn) => {
  deferred.push(fn);
};

const flush = () => {
  let fn;
  while(fn = deferred.pop()) {
    fn();
  }
};

class Evented {
  on(event, handler, ctx, once=false) {
    if (typeof this.bindings === 'undefined') {
      this.bindings = {};
    }
    if (typeof this.bindings[event] === 'undefined') {
      this.bindings[event] = [];
    }
    this.bindings[event].push({handler, ctx, once});
  }

  once(event, handler, ctx) {
    this.on(event, handler, ctx, true);
  }

  off(event, handler) {
    if (typeof this.bindings === 'undefined' ||
        typeof this.bindings[event] === 'undefined') {
      return;
    }

    if (typeof handler === 'undefined') {
      delete this.bindings[event];
    } else {
      let i = 0;
      while (i < this.bindings[event].length) {
        if (this.bindings[event][i].handler === handler) {
          this.bindings[event].splice(i, 1);
        } else {
          ++i;
        }
      }
    }
  }

  trigger(event, ...args) {
    if (typeof this.bindings !== 'undefined' && this.bindings[event]) {
      let i = 0;
      while (i < this.bindings[event].length) {
        const {handler, ctx, once} = this.bindings[event][i];

        let context = ctx;
        if (typeof context === 'undefined') {
          context = this;
        }

        handler.apply(context, args);

        if (once) {
          this.bindings[event].splice(i, 1);
        } else {
          ++i;
        }
      }
    }
  }
}

TetherBase.Utils = {
  getScrollParents,
  getBounds,
  getOffsetParent,
  extend,
  addClass,
  removeClass,
  hasClass,
  updateClasses,
  defer,
  flush,
  uniqueId,
  Evented,
  getScrollBarSize,
  removeUtilElements
};
<|endoftext|>


<commit_before>define(['backbone', 'd3', 'js/utils'], function(Backbone, d3, utils) {
    "use strict";

    var Views = {};

    Views.TimerView = Backbone.View.extend({
        initialize: function() {
            this.model = null;
            this._interval = null;
            this._renderTitle = false;
            this.render();

            _.bindAll(this, 'render', 'startTicking');
        },

        /**
         * Starts ticking the timer, updating it every second.
         */
        startRunning: function(pomodoro) {
            this.model = pomodoro;
            setTimeout(this.startTicking, 50);
        },

        /**
         * Accurate timer hack.
         *
         * If we start the setInterval at the exact same time
         * we start the pomodoro, the lack of accuracy of
         * the setTimeout function will create an irregularity
         * in the timer rendering.
         *
         * Hence, we wait a short time before starting the loop,
         * so every time the `render` method is called, we are sure
         * a different timer value will be displayed.
         */
        startTicking: function() {
            this.render();
            clearInterval(this._interval);
            this._interval = setInterval(this.render, 1000);
        },

        /**
         * Stops the timer ticks.
         */
        stopRunning: function() {
            clearInterval(this._interval);
            this.model = null;
            this.render();
        },

        render: function() {
            var time;
            if (this.model) {
                var remainingTime = this.model.remainingTime();
                time = utils.prettifyTime(remainingTime);
            } else {
                time = '00:00';
            }

            this.$el.text(time);

            if (this._renderTitle) {
                $('title').text(time);
            } else {
                $('title').text('Pomodoro');
            }
            return this;
        }
    });

    Views.ControlView = Backbone.View.extend({
        events: {
            'click button': 'onClick'
        },
        initialize: function() {
            this.render();
        },
        renderForPomodoro: function(pomodoro) {
            this.model = pomodoro;
            this.render();
        },
        resetView: function() {
            this.model = null;
            this.render();
        },

        /**
         * Disable the control buttons depending of the state of the
         * current pomodoro.
         */
        render: function() {
            var startButtons = this.$el.find('button.start-control');
            var stopButton = this.$el.find('button.stop-control');

            if (this.model) {
                startButtons.prop('disabled', true);
                stopButton.prop('disabled', false);
            } else {
                startButtons.prop('disabled', false);
                stopButton.prop('disabled', true);
            }
            return this;
        },

        /**
         * Trigger events depending on clicked buttons.
         */
        onClick: function(event) {
            var target = event.target;
            var action = target.getAttribute('data-action');
            var type = target.getAttribute('data-type');

            if (action === 'stop') {
                this.trigger('timerInterrupted');
            } else {
                var duration = target.getAttribute('data-duration');
                this.trigger('timerStarted', {
                    duration: duration,
                    type: type
                });
            }
        }
    });

    /**
     * Updates the app configuration when the form is modified.
     */
    Views.ConfigurationView = Backbone.View.extend({
        events: {
            'change': 'onConfigurationChange',
            'click input[type=checkbox]': 'toggleTitle'
        },
        buttons: [
            {
                field: '#pomodoro-duration',
                button: '#start-pomodoro-btn',
                option: 'pomodoroDuration'
            },
            {
                field: '#break-duration',
                button: '#start-break-btn',
                option: 'breakDuration'
            },
            {
                field: '#lbreak-duration',
                button: '#start-lbreak-btn',
                option: 'lbreakDuration'
            },
        ],

        initialize: function() {
            this.dynamicTitleCb = this.$el.find('#dynamicTitleCb');

            this.updateConfigurationForm();
            this.updateTimerButtons();
        },

        /**
         * Set configuration form default values to current
         * configuration values.
         */
        updateConfigurationForm: function() {
            this.dynamicTitleCb.prop('checked', this.model.get('dynamicTitleEnabled'));
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = model.get(data.option) / 60000;
                field.val(value);
            });
        },

        /**
         * Set the data-duration attributes on timer buttons.
         */
        updateTimerButtons: function() {
            var model = this.model;
            _.each(this.buttons, function(data) {
                var button = $(data.button);
                var value = model.get(data.option);
                button.attr('data-duration', value);
            });

        },

        /**
         * Updates the configuration model object with data from the
         * configuration form.
         */
        onConfigurationChange: function() {
            this.model.set('dynamicTitleEnabled', this.isTitleDynamic());
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = field.val() * 60000;
                model.set(data.option, value);
            });

            this.updateTimerButtons();
            this.trigger('configurationChanged');
        },

        toggleTitle: function() {
            this.trigger('dynamicTitleToggled');
        },

        isTitleDynamic: function() {
            return this.dynamicTitleCb.prop('checked');
        }
    });

    /**
     * Display a wonderful chart of pomodoro counts.
     */
    Views.ChartView = Backbone.View.extend({
        initialize: function(options) {
            this.initializeChart();

            // Required options for this view
            var chartOptions = ['interval', 'dateFormat', 'dataProvider'];
            _.extend(this, _.pick(options, chartOptions));

            _.bindAll(this, 'render');
            this.listenTo(this.collection, 'add', this.render);
            this.listenTo(this.collection, 'change', this.render);
        },

        /**
         * Creates the initial svg structure for the chart.
         */
        initializeChart: function() {
            this.margin = {top: 20, right: 20, bottom: 100, left: 40};
            this.width = this.$el.width() - this.margin.left - this.margin.right;
            this.height = this.$el.height() - this.margin.top - this.margin.bottom;

            this.svg = d3.select(this.el).append("svg")
                .attr('class', 'chart')
                .attr("width", this.$el.width())
                .attr("height", this.$el.height())
              .append("g")
                .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");

            this.xAxis = this.svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + this.height + ")");

            this.yAxis = this.svg.append("g")
                .attr("class", "y axis");
            this.yAxis
                .append("text")
                .attr("transform", "rotate(-90)")
                .attr("y", 6)
                .attr("dy", ".71em")
                .style("text-anchor", "end")
                .text("Pomodoros");
        },

        /**
         * Generates the chart svg structure.
         */
        getScales: function(data) {
            var xDomain = data.map(function(d) { return d.date; });
            var x = d3.scale.ordinal()
                .domain(xDomain)
                .rangeRoundBands([0, this.width], 0.1);

            var yDomain = [0, d3.max(data, function(d) { return d.pomodoros; })];
            var y = d3.scale.linear()
                .domain(yDomain)
                .range([this.height, 0]);

            return {x: x, y: y};
        },

        /**
         * Get X and Y axes.
         */
        getAxes: function(data, scales) {
            var format = d3.time.format(this.dateFormat);
            var xAxis = d3.svg.axis()
                .scale(scales.x)
                .tickFormat(function(d) {
                    return format(d);
                })
                .orient("bottom");

            var maxPomodoros = (d3.max(data, function(d) { return d.pomodoros; }));
            var yTicks = d3.min(20, maxPomodoros);
            var yAxis = d3.svg.axis()
                .scale(scales.y)
                .tickFormat(d3.format("d"))
                .ticks(yTicks)
                .orient("left");

            return {x: xAxis, y: yAxis};
        },

        /**
         * Render the axes on the chart.
         */
        renderAxes: function(axes) {
            this.xAxis
                .transition()
                .duration(1000)
                .call(axes.x);

            this.xAxis.selectAll("text")
                .style("text-anchor", "end")
                .attr("dx", "-.8em")
                .attr("dy", ".15em")
                .attr("transform", function(d) {
                    return "rotate(-45)";
                });

            this.yAxis
                .transition()
                .duration(1000)
                .call(axes.y);
        },

        /**
         * Display data on the chart.
         */
        renderData: function(data, scales) {
            var that = this;

            var bars = this.svg.selectAll('rect')
                .data(data, function(d) { return d.date; });

            bars.enter()
                .append("rect")
                .attr("y", that.height)
                .attr("height", 0)
                .attr("class", "bar");

            bars.transition()
                .duration(1000)
                .attr("x", function(d) { return scales.x(d.date); })
                .attr("width", scales.x.rangeBand())
                .attr("y", function(d) { return scales.y(d.pomodoros); })
                .attr("height", function(d) { return that.height - scales.y(d.pomodoros); });
        },

        render: function() {
            var data = this.dataProvider.getData();
            var scales = this.getScales(data);
            var axes = this.getAxes(data, scales);
            this.renderAxes(axes);
            this.renderData(data, scales);
            return this;
        }
    });

    Views.AnnotationView = Backbone.View.extend({
        events: {
            'submit': 'annotatePomodoro'
        },
        initialize: function() {
            this.input = this.$el.find('input[type="text"]');
        },
        annotatePomodoro: function(event) {
            event.preventDefault();
            var pomodoro = this.collection.last();
            var annotation = this.input.val();

            if (pomodoro && annotation) {
                var project = utils.extractProject(annotation);
                pomodoro.set('project', project);

                var tags = utils.extractTags(annotation);
                pomodoro.set('tags', tags);

                pomodoro.save();
            }
        }
    });

    return Views;
});
<commit_msg>Clear annotation input once submitted<commit_after>define(['backbone', 'd3', 'js/utils'], function(Backbone, d3, utils) {
    "use strict";

    var Views = {};

    Views.TimerView = Backbone.View.extend({
        initialize: function() {
            this.model = null;
            this._interval = null;
            this._renderTitle = false;
            this.render();

            _.bindAll(this, 'render', 'startTicking');
        },

        /**
         * Starts ticking the timer, updating it every second.
         */
        startRunning: function(pomodoro) {
            this.model = pomodoro;
            setTimeout(this.startTicking, 50);
        },

        /**
         * Accurate timer hack.
         *
         * If we start the setInterval at the exact same time
         * we start the pomodoro, the lack of accuracy of
         * the setTimeout function will create an irregularity
         * in the timer rendering.
         *
         * Hence, we wait a short time before starting the loop,
         * so every time the `render` method is called, we are sure
         * a different timer value will be displayed.
         */
        startTicking: function() {
            this.render();
            clearInterval(this._interval);
            this._interval = setInterval(this.render, 1000);
        },

        /**
         * Stops the timer ticks.
         */
        stopRunning: function() {
            clearInterval(this._interval);
            this.model = null;
            this.render();
        },

        render: function() {
            var time;
            if (this.model) {
                var remainingTime = this.model.remainingTime();
                time = utils.prettifyTime(remainingTime);
            } else {
                time = '00:00';
            }

            this.$el.text(time);

            if (this._renderTitle) {
                $('title').text(time);
            } else {
                $('title').text('Pomodoro');
            }
            return this;
        }
    });

    Views.ControlView = Backbone.View.extend({
        events: {
            'click button': 'onClick'
        },
        initialize: function() {
            this.render();
        },
        renderForPomodoro: function(pomodoro) {
            this.model = pomodoro;
            this.render();
        },
        resetView: function() {
            this.model = null;
            this.render();
        },

        /**
         * Disable the control buttons depending of the state of the
         * current pomodoro.
         */
        render: function() {
            var startButtons = this.$el.find('button.start-control');
            var stopButton = this.$el.find('button.stop-control');

            if (this.model) {
                startButtons.prop('disabled', true);
                stopButton.prop('disabled', false);
            } else {
                startButtons.prop('disabled', false);
                stopButton.prop('disabled', true);
            }
            return this;
        },

        /**
         * Trigger events depending on clicked buttons.
         */
        onClick: function(event) {
            var target = event.target;
            var action = target.getAttribute('data-action');
            var type = target.getAttribute('data-type');

            if (action === 'stop') {
                this.trigger('timerInterrupted');
            } else {
                var duration = target.getAttribute('data-duration');
                this.trigger('timerStarted', {
                    duration: duration,
                    type: type
                });
            }
        }
    });

    /**
     * Updates the app configuration when the form is modified.
     */
    Views.ConfigurationView = Backbone.View.extend({
        events: {
            'change': 'onConfigurationChange',
            'click input[type=checkbox]': 'toggleTitle'
        },
        buttons: [
            {
                field: '#pomodoro-duration',
                button: '#start-pomodoro-btn',
                option: 'pomodoroDuration'
            },
            {
                field: '#break-duration',
                button: '#start-break-btn',
                option: 'breakDuration'
            },
            {
                field: '#lbreak-duration',
                button: '#start-lbreak-btn',
                option: 'lbreakDuration'
            },
        ],

        initialize: function() {
            this.dynamicTitleCb = this.$el.find('#dynamicTitleCb');

            this.updateConfigurationForm();
            this.updateTimerButtons();
        },

        /**
         * Set configuration form default values to current
         * configuration values.
         */
        updateConfigurationForm: function() {
            this.dynamicTitleCb.prop('checked', this.model.get('dynamicTitleEnabled'));
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = model.get(data.option) / 60000;
                field.val(value);
            });
        },

        /**
         * Set the data-duration attributes on timer buttons.
         */
        updateTimerButtons: function() {
            var model = this.model;
            _.each(this.buttons, function(data) {
                var button = $(data.button);
                var value = model.get(data.option);
                button.attr('data-duration', value);
            });

        },

        /**
         * Updates the configuration model object with data from the
         * configuration form.
         */
        onConfigurationChange: function() {
            this.model.set('dynamicTitleEnabled', this.isTitleDynamic());
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = field.val() * 60000;
                model.set(data.option, value);
            });

            this.updateTimerButtons();
            this.trigger('configurationChanged');
        },

        toggleTitle: function() {
            this.trigger('dynamicTitleToggled');
        },

        isTitleDynamic: function() {
            return this.dynamicTitleCb.prop('checked');
        }
    });

    /**
     * Display a wonderful chart of pomodoro counts.
     */
    Views.ChartView = Backbone.View.extend({
        initialize: function(options) {
            this.initializeChart();

            // Required options for this view
            var chartOptions = ['interval', 'dateFormat', 'dataProvider'];
            _.extend(this, _.pick(options, chartOptions));

            _.bindAll(this, 'render');
            this.listenTo(this.collection, 'add', this.render);
            this.listenTo(this.collection, 'change', this.render);
        },

        /**
         * Creates the initial svg structure for the chart.
         */
        initializeChart: function() {
            this.margin = {top: 20, right: 20, bottom: 100, left: 40};
            this.width = this.$el.width() - this.margin.left - this.margin.right;
            this.height = this.$el.height() - this.margin.top - this.margin.bottom;

            this.svg = d3.select(this.el).append("svg")
                .attr('class', 'chart')
                .attr("width", this.$el.width())
                .attr("height", this.$el.height())
              .append("g")
                .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");

            this.xAxis = this.svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + this.height + ")");

            this.yAxis = this.svg.append("g")
                .attr("class", "y axis");
            this.yAxis
                .append("text")
                .attr("transform", "rotate(-90)")
                .attr("y", 6)
                .attr("dy", ".71em")
                .style("text-anchor", "end")
                .text("Pomodoros");
        },

        /**
         * Generates the chart svg structure.
         */
        getScales: function(data) {
            var xDomain = data.map(function(d) { return d.date; });
            var x = d3.scale.ordinal()
                .domain(xDomain)
                .rangeRoundBands([0, this.width], 0.1);

            var yDomain = [0, d3.max(data, function(d) { return d.pomodoros; })];
            var y = d3.scale.linear()
                .domain(yDomain)
                .range([this.height, 0]);

            return {x: x, y: y};
        },

        /**
         * Get X and Y axes.
         */
        getAxes: function(data, scales) {
            var format = d3.time.format(this.dateFormat);
            var xAxis = d3.svg.axis()
                .scale(scales.x)
                .tickFormat(function(d) {
                    return format(d);
                })
                .orient("bottom");

            var maxPomodoros = (d3.max(data, function(d) { return d.pomodoros; }));
            var yTicks = d3.min(20, maxPomodoros);
            var yAxis = d3.svg.axis()
                .scale(scales.y)
                .tickFormat(d3.format("d"))
                .ticks(yTicks)
                .orient("left");

            return {x: xAxis, y: yAxis};
        },

        /**
         * Render the axes on the chart.
         */
        renderAxes: function(axes) {
            this.xAxis
                .transition()
                .duration(1000)
                .call(axes.x);

            this.xAxis.selectAll("text")
                .style("text-anchor", "end")
                .attr("dx", "-.8em")
                .attr("dy", ".15em")
                .attr("transform", function(d) {
                    return "rotate(-45)";
                });

            this.yAxis
                .transition()
                .duration(1000)
                .call(axes.y);
        },

        /**
         * Display data on the chart.
         */
        renderData: function(data, scales) {
            var that = this;

            var bars = this.svg.selectAll('rect')
                .data(data, function(d) { return d.date; });

            bars.enter()
                .append("rect")
                .attr("y", that.height)
                .attr("height", 0)
                .attr("class", "bar");

            bars.transition()
                .duration(1000)
                .attr("x", function(d) { return scales.x(d.date); })
                .attr("width", scales.x.rangeBand())
                .attr("y", function(d) { return scales.y(d.pomodoros); })
                .attr("height", function(d) { return that.height - scales.y(d.pomodoros); });
        },

        render: function() {
            var data = this.dataProvider.getData();
            var scales = this.getScales(data);
            var axes = this.getAxes(data, scales);
            this.renderAxes(axes);
            this.renderData(data, scales);
            return this;
        }
    });

    Views.AnnotationView = Backbone.View.extend({
        events: {
            'submit': 'annotatePomodoro'
        },
        initialize: function() {
            this.input = this.$el.find('input[type="text"]');
        },
        annotatePomodoro: function(event) {
            event.preventDefault();
            var pomodoro = this.collection.last();
            var annotation = this.input.val();

            if (pomodoro && annotation) {
                var project = utils.extractProject(annotation);
                pomodoro.set('project', project);

                var tags = utils.extractTags(annotation);
                pomodoro.set('tags', tags);

                pomodoro.save();
            }

            this.input.val('');
        }
    });

    return Views;
});
<|endoftext|>


<commit_before>#!/usr/bin/env node
/* eslint-disable no-console */

const fetch = require('node-fetch');
const { execSync } = require('child_process');

const { FRONTPAGE_WEBHOOK } = process.env;

const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();

console.log('build-frontpage');
if (branch === 'master') {
  if (FRONTPAGE_WEBHOOK) {
    console.log('triggering frontpage build');
    const url = `https://api.netlify.com/build_hooks/${FRONTPAGE_WEBHOOK}`;
    fetch(url, { method: 'post' }).then((res) => console.log('result', res.status));
  } else {
    console.log('no webhook defined');
  }
} else {
  console.log('skipping branch', branch);
}
<commit_msg>Deploy the `next` branch of frontpage too<commit_after>#!/usr/bin/env node
/* eslint-disable no-console */

const fetch = require('node-fetch');
const { execSync } = require('child_process');

const { FRONTPAGE_WEBHOOK, FRONTPAGE_WEBHOOK_NEXT } = process.env;

const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();

const branchToHook = {
  master: FRONTPAGE_WEBHOOK,
  '6.0-docs': FRONTPAGE_WEBHOOK_NEXT, // <- NOTE: change this to `next` when we merge the new docs
};

console.log('build-frontpage');
const hook = branchToHook[branch];
if (hook) {
  console.log('triggering frontpage build');
  const url = `https://api.netlify.com/build_hooks/${hook}`;
  fetch(url, { method: 'post' }).then((res) => console.log('result', res.status));
} else {
  console.log('skipping branch', branch);
}
<|endoftext|>


<commit_before>define(['backbone', 'd3', 'js/utils'], function(Backbone, d3, utils) {
    "use strict";

    var Views = {};

    Views.TimerView = Backbone.View.extend({
        initialize: function() {
            this.model = null;
            this._interval = null;
            this._renderTitle = false;
            this.render();

            _.bindAll(this, 'render');
        },

        /**
         * Starts ticking the timer, updating it every second.
         */
        startRunning: function(pomodoro) {
            this.model = pomodoro;
            this.render();
            clearInterval(this._interval);
            this._interval = setInterval(this.render, 100);
        },

        /**
         * Stops the timer ticks.
         */
        stopRunning: function() {
            clearInterval(this._interval);
            this.model = null;
            this.render();
        },

        render: function() {
            var time;
            if (this.model) {
                var remainingTime = this.model.remainingTime();
                time = utils.prettifyTime(remainingTime);
            } else {
                time = '00:00';
            }

            this.$el.text(time);

            if (this._renderTitle) {
                $('title').text(time);
            } else {
                $('title').text('Pomodoro');
            }
            return this;
        }
    });

    Views.ControlView = Backbone.View.extend({
        events: {
            'click button': 'onClick'
        },
        initialize: function() {
            this.render();
        },
        renderForPomodoro: function(pomodoro) {
            this.model = pomodoro;
            this.render();
        },
        resetView: function() {
            this.model = null;
            this.render();
        },

        /**
         * Disable the control buttons depending of the state of the
         * current pomodoro.
         */
        render: function() {
            var startButtons = this.$el.find('button.start-control');
            var stopButton = this.$el.find('button.stop-control');

            if (this.model) {
                startButtons.prop('disabled', true);
                stopButton.prop('disabled', false);
            } else {
                startButtons.prop('disabled', false);
                stopButton.prop('disabled', true);
            }
            return this;
        },

        /**
         * Trigger events depending on clicked buttons.
         */
        onClick: function(event) {
            var target = event.target;
            var action = target.getAttribute('data-action');
            var type = target.getAttribute('data-type');

            if (action === 'stop') {
                this.trigger('timerInterrupted');
            } else {
                var duration = target.getAttribute('data-duration');
                this.trigger('timerStarted', {
                    duration: duration,
                    type: type
                });
            }
        }
    });

    /**
     * Updates the app configuration when the form is modified.
     */
    Views.ConfigurationView = Backbone.View.extend({
        events: {
            'change': 'onConfigurationChange',
            'click input[type=checkbox]': 'toggleTitle'
        },
        buttons: [
            {
                field: '#pomodoro-duration',
                button: '#start-pomodoro-btn',
                option: 'pomodoroDuration'
            },
            {
                field: '#break-duration',
                button: '#start-break-btn',
                option: 'breakDuration'
            },
            {
                field: '#lbreak-duration',
                button: '#start-lbreak-btn',
                option: 'lbreakDuration'
            },
        ],

        initialize: function() {
            this.dynamicTitleCb = this.$el.find('#dynamicTitleCb');

            this.updateConfigurationForm();
            this.updateTimerButtons();
        },

        /**
         * Set configuration form default values to current
         * configuration values.
         */
        updateConfigurationForm: function() {
            this.dynamicTitleCb.prop('checked', this.model.get('dynamicTitleEnabled'));
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = model.get(data.option) / 60000;
                field.val(value);
            });
        },

        /**
         * Set the data-duration attributes on timer buttons.
         */
        updateTimerButtons: function() {
            var model = this.model;
            _.each(this.buttons, function(data) {
                var button = $(data.button);
                var value = model.get(data.option);
                button.attr('data-duration', value);
            });

        },

        /**
         * Updates the configuration model object with data from the
         * configuration form.
         */
        onConfigurationChange: function() {
            this.model.set('dynamicTitleEnabled', this.isTitleDynamic());
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = field.val() * 60000;
                model.set(data.option, value);
            });

            this.updateTimerButtons();
            this.trigger('configurationChanged');
        },

        toggleTitle: function() {
            this.trigger('dynamicTitleToggled');
        },

        isTitleDynamic: function() {
            return this.dynamicTitleCb.prop('checked');
        }
    });

    /**
     * Display a wonderful chart of pomodoro counts.
     */
    Views.ChartView = Backbone.View.extend({
        initialize: function(options) {
            this.initializeChart();

            // Required options for this view
            var chartOptions = ['interval', 'dateFormat', 'dataProvider'];
            _.extend(this, _.pick(options, chartOptions));

            _.bindAll(this, 'render');
            this.listenTo(this.collection, 'add', this.render);
        },

        /**
         * Creates the initial svg structure for the chart.
         */
        initializeChart: function() {
            this.margin = {top: 20, right: 20, bottom: 100, left: 40};
            this.width = this.$el.width() - this.margin.left - this.margin.right;
            this.height = this.$el.height() - this.margin.top - this.margin.bottom;

            this.svg = d3.select(this.el).append("svg")
                .attr('class', 'chart')
                .attr("width", this.$el.width())
                .attr("height", this.$el.height())
              .append("g")
                .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");

            this.xAxis = this.svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + this.height + ")");

            this.yAxis = this.svg.append("g")
                .attr("class", "y axis");
            this.yAxis
                .append("text")
                .attr("transform", "rotate(-90)")
                .attr("y", 6)
                .attr("dy", ".71em")
                .style("text-anchor", "end")
                .text("Pomodoros");
        },

        /**
         * Generates the chart svg structure.
         */
        getScales: function(data) {
            var xDomain = data.map(function(d) { return d.date; });
            var x = d3.scale.ordinal()
                .domain(xDomain)
                .rangeRoundBands([0, this.width], 0.1);

            var yDomain = [0, d3.max(data, function(d) { return d.pomodoros; })];
            var y = d3.scale.linear()
                .domain(yDomain)
                .range([this.height, 0]);

            return {x: x, y: y};
        },

        /**
         * Get X and Y axes.
         */
        getAxes: function(data, scales) {
            var format = d3.time.format(this.dateFormat);
            var xAxis = d3.svg.axis()
                .scale(scales.x)
                .tickFormat(function(d) {
                    return format(d);
                })
                .orient("bottom");

            var maxPomodoros = (d3.max(data, function(d) { return d.pomodoros; }));
            var yTicks = d3.min(20, maxPomodoros);
            var yAxis = d3.svg.axis()
                .scale(scales.y)
                .tickFormat(d3.format("d"))
                .ticks(yTicks)
                .orient("left");

            return {x: xAxis, y: yAxis};
        },

        /**
         * Render the axes on the chart.
         */
        renderAxes: function(axes) {
            this.xAxis
                .transition()
                .duration(1000)
                .call(axes.x);

            this.xAxis.selectAll("text")
                .style("text-anchor", "end")
                .attr("dx", "-.8em")
                .attr("dy", ".15em")
                .attr("transform", function(d) {
                    return "rotate(-45)";
                });

            this.yAxis
                .transition()
                .duration(1000)
                .call(axes.y);
        },

        /**
         * Display data on the chart.
         */
        renderData: function(data, scales) {
            var that = this;

            var bars = this.svg.selectAll('rect')
                .data(data, function(d) { return d.date; });

            bars.enter()
                .append("rect")
                .attr("y", that.height)
                .attr("height", 0)
                .attr("class", "bar");

            bars.transition()
                .duration(1000)
                .attr("x", function(d) { return scales.x(d.date); })
                .attr("width", scales.x.rangeBand())
                .attr("y", function(d) { return scales.y(d.pomodoros); })
                .attr("height", function(d) { return that.height - scales.y(d.pomodoros); });
        },

        render: function() {
            var data = this.dataProvider.getData();
            var scales = this.getScales(data);
            var axes = this.getAxes(data, scales);
            this.renderAxes(axes);
            this.renderData(data, scales);
            return this;
        }
    });

    return Views;
});
<commit_msg>Better timer rendering accuracy<commit_after>define(['backbone', 'd3', 'js/utils'], function(Backbone, d3, utils) {
    "use strict";

    var Views = {};

    Views.TimerView = Backbone.View.extend({
        initialize: function() {
            this.model = null;
            this._interval = null;
            this._renderTitle = false;
            this.render();

            _.bindAll(this, 'render', 'startTicking');
        },

        /**
         * Starts ticking the timer, updating it every second.
         */
        startRunning: function(pomodoro) {
            this.model = pomodoro;
            setTimeout(this.startTicking, 50);
        },

        /**
         * Accurate timer hack.
         *
         * If we start the setInterval at the exact same time
         * we start the pomodoro, the lack of accuracy of
         * the setTimeout function will create an irregularity
         * in the timer rendering.
         *
         * Hence, we wait a short time before starting the loop,
         * so every time the `render` method is called, we are sure
         * a different timer value will be displayed.
         */
        startTicking: function() {
            this.render();
            clearInterval(this._interval);
            this._interval = setInterval(this.render, 1000);
        },

        /**
         * Stops the timer ticks.
         */
        stopRunning: function() {
            clearInterval(this._interval);
            this.model = null;
            this.render();
        },

        render: function() {
            var time;
            if (this.model) {
                var remainingTime = this.model.remainingTime();
                time = utils.prettifyTime(remainingTime);
            } else {
                time = '00:00';
            }

            this.$el.text(time);

            if (this._renderTitle) {
                $('title').text(time);
            } else {
                $('title').text('Pomodoro');
            }
            return this;
        }
    });

    Views.ControlView = Backbone.View.extend({
        events: {
            'click button': 'onClick'
        },
        initialize: function() {
            this.render();
        },
        renderForPomodoro: function(pomodoro) {
            this.model = pomodoro;
            this.render();
        },
        resetView: function() {
            this.model = null;
            this.render();
        },

        /**
         * Disable the control buttons depending of the state of the
         * current pomodoro.
         */
        render: function() {
            var startButtons = this.$el.find('button.start-control');
            var stopButton = this.$el.find('button.stop-control');

            if (this.model) {
                startButtons.prop('disabled', true);
                stopButton.prop('disabled', false);
            } else {
                startButtons.prop('disabled', false);
                stopButton.prop('disabled', true);
            }
            return this;
        },

        /**
         * Trigger events depending on clicked buttons.
         */
        onClick: function(event) {
            var target = event.target;
            var action = target.getAttribute('data-action');
            var type = target.getAttribute('data-type');

            if (action === 'stop') {
                this.trigger('timerInterrupted');
            } else {
                var duration = target.getAttribute('data-duration');
                this.trigger('timerStarted', {
                    duration: duration,
                    type: type
                });
            }
        }
    });

    /**
     * Updates the app configuration when the form is modified.
     */
    Views.ConfigurationView = Backbone.View.extend({
        events: {
            'change': 'onConfigurationChange',
            'click input[type=checkbox]': 'toggleTitle'
        },
        buttons: [
            {
                field: '#pomodoro-duration',
                button: '#start-pomodoro-btn',
                option: 'pomodoroDuration'
            },
            {
                field: '#break-duration',
                button: '#start-break-btn',
                option: 'breakDuration'
            },
            {
                field: '#lbreak-duration',
                button: '#start-lbreak-btn',
                option: 'lbreakDuration'
            },
        ],

        initialize: function() {
            this.dynamicTitleCb = this.$el.find('#dynamicTitleCb');

            this.updateConfigurationForm();
            this.updateTimerButtons();
        },

        /**
         * Set configuration form default values to current
         * configuration values.
         */
        updateConfigurationForm: function() {
            this.dynamicTitleCb.prop('checked', this.model.get('dynamicTitleEnabled'));
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = model.get(data.option) / 60000;
                field.val(value);
            });
        },

        /**
         * Set the data-duration attributes on timer buttons.
         */
        updateTimerButtons: function() {
            var model = this.model;
            _.each(this.buttons, function(data) {
                var button = $(data.button);
                var value = model.get(data.option);
                button.attr('data-duration', value);
            });

        },

        /**
         * Updates the configuration model object with data from the
         * configuration form.
         */
        onConfigurationChange: function() {
            this.model.set('dynamicTitleEnabled', this.isTitleDynamic());
            var model = this.model;
            _.each(this.buttons, function(data) {
                var field = $(data.field);
                var value = field.val() * 60000;
                model.set(data.option, value);
            });

            this.updateTimerButtons();
            this.trigger('configurationChanged');
        },

        toggleTitle: function() {
            this.trigger('dynamicTitleToggled');
        },

        isTitleDynamic: function() {
            return this.dynamicTitleCb.prop('checked');
        }
    });

    /**
     * Display a wonderful chart of pomodoro counts.
     */
    Views.ChartView = Backbone.View.extend({
        initialize: function(options) {
            this.initializeChart();

            // Required options for this view
            var chartOptions = ['interval', 'dateFormat', 'dataProvider'];
            _.extend(this, _.pick(options, chartOptions));

            _.bindAll(this, 'render');
            this.listenTo(this.collection, 'add', this.render);
        },

        /**
         * Creates the initial svg structure for the chart.
         */
        initializeChart: function() {
            this.margin = {top: 20, right: 20, bottom: 100, left: 40};
            this.width = this.$el.width() - this.margin.left - this.margin.right;
            this.height = this.$el.height() - this.margin.top - this.margin.bottom;

            this.svg = d3.select(this.el).append("svg")
                .attr('class', 'chart')
                .attr("width", this.$el.width())
                .attr("height", this.$el.height())
              .append("g")
                .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");

            this.xAxis = this.svg.append("g")
                .attr("class", "x axis")
                .attr("transform", "translate(0," + this.height + ")");

            this.yAxis = this.svg.append("g")
                .attr("class", "y axis");
            this.yAxis
                .append("text")
                .attr("transform", "rotate(-90)")
                .attr("y", 6)
                .attr("dy", ".71em")
                .style("text-anchor", "end")
                .text("Pomodoros");
        },

        /**
         * Generates the chart svg structure.
         */
        getScales: function(data) {
            var xDomain = data.map(function(d) { return d.date; });
            var x = d3.scale.ordinal()
                .domain(xDomain)
                .rangeRoundBands([0, this.width], 0.1);

            var yDomain = [0, d3.max(data, function(d) { return d.pomodoros; })];
            var y = d3.scale.linear()
                .domain(yDomain)
                .range([this.height, 0]);

            return {x: x, y: y};
        },

        /**
         * Get X and Y axes.
         */
        getAxes: function(data, scales) {
            var format = d3.time.format(this.dateFormat);
            var xAxis = d3.svg.axis()
                .scale(scales.x)
                .tickFormat(function(d) {
                    return format(d);
                })
                .orient("bottom");

            var maxPomodoros = (d3.max(data, function(d) { return d.pomodoros; }));
            var yTicks = d3.min(20, maxPomodoros);
            var yAxis = d3.svg.axis()
                .scale(scales.y)
                .tickFormat(d3.format("d"))
                .ticks(yTicks)
                .orient("left");

            return {x: xAxis, y: yAxis};
        },

        /**
         * Render the axes on the chart.
         */
        renderAxes: function(axes) {
            this.xAxis
                .transition()
                .duration(1000)
                .call(axes.x);

            this.xAxis.selectAll("text")
                .style("text-anchor", "end")
                .attr("dx", "-.8em")
                .attr("dy", ".15em")
                .attr("transform", function(d) {
                    return "rotate(-45)";
                });

            this.yAxis
                .transition()
                .duration(1000)
                .call(axes.y);
        },

        /**
         * Display data on the chart.
         */
        renderData: function(data, scales) {
            var that = this;

            var bars = this.svg.selectAll('rect')
                .data(data, function(d) { return d.date; });

            bars.enter()
                .append("rect")
                .attr("y", that.height)
                .attr("height", 0)
                .attr("class", "bar");

            bars.transition()
                .duration(1000)
                .attr("x", function(d) { return scales.x(d.date); })
                .attr("width", scales.x.rangeBand())
                .attr("y", function(d) { return scales.y(d.pomodoros); })
                .attr("height", function(d) { return that.height - scales.y(d.pomodoros); });
        },

        render: function() {
            var data = this.dataProvider.getData();
            var scales = this.getScales(data);
            var axes = this.getAxes(data, scales);
            this.renderAxes(axes);
            this.renderData(data, scales);
            return this;
        }
    });

    return Views;
});
<|endoftext|>