<commit_before>

'use strict'

const SakuyaBot = class {
  constructor (client, db, logger, rand) {
    this.client = client
    this.db = db
    this.logger = logger
    this.rand = rand

    // ツイートテキストを引数として、リアクションを返却する関数軍を
    // SakuyaBotに任意に組み込むことができる機能を提供する。
    // これを、リアクションプラグインと呼称し、
    // SakuyaBotの汎用的な口上を組み込む仕組みとして利用する。
    this.reaction_plugins = []

    this.teaSelector = require('./Teapot.js')

    // 起動時にアカウントの有効性を確認する
    this.client.verifyCredentials().then(
      (res) => {
        this.logger.info('Account verification succeed.')
        this.name = res.name
        this.screen_name = res.screen_name
        this.id = res.id_str
      }
    ).catch(
      () => {
        this.logger.fatal('Account verification failed.')
      }
    )
  }

  start () {
    this.logger.info('SakuyaBot started.')
    this.client.openUserStream({
      data: this.read.bind(this),
      error: (error) => console.log(error),
      event: this.receive.bind(this)
    })
  }

  /**
   * リアクションプラグインを組み込む
   * @param func リアクションプラグイン
   */
  addReactionPlugin (plugin) {
    this.reaction_plugins.push(plugin)
  }

  // 毎日 0時に実行する
  daily_work (today) {
    // 秒単位のずれで日付が前後することを防ぐため 12時間分加算する
    today = new Date(today.getTime() + 43200)

    let m = today.getMonth() + 1
    let d = today.getDate()
    return this.db.sumDonation().then( (amount) => {
      return this.client.tweet(`${m}月 ${d}日になったわね。 先日は ${amount} ml の献血をいただきましたわ。`)
    }).then(() => {
      return this.db.stashDonatedLog()
    }).catch((err) => {
      this.logger.error(err)
      this.logger.trace(err.trace)
    })
  }

  read (tweet) {
    this.logger.info(`[${tweet.id_str}] @${tweet.user.screen_name}: ${tweet.text}`)
    return new Promise((resolve,reject) => {

      // 自身以外へのrepliesを除外する
      let replies = (tweet.text.match(/@[0-9a-zA-Z]+/g) || []).filter((u) => u !== `@${this.screen_name}`)
      if (replies.length) reject()

      // 先頭が@でないツイートには反応しない
      if (!tweet.text.startsWith('@')) reject()

      resolve()
    }).then(() => {
      // ユーザの情報をDBから取得する
      return this.db.getUser(tweet.user)

    // ユーザ情報を作成する
    }).then( (user) => {
      if (user !== null) {
        return Promise.resolve(user)
      } else {
        return this.db.createUser(tweet.user)
      }
    }).then( (userdata) => {
      // createしていた場合は初期値のuser情報を設定
      if( userdata.ops !== undefined) userdata = userdata.ops[0]

      // 呼び名設定
      let callAs = this._replaceNN(userdata.nickname, tweet.user)

      return new Promise((resolve) => {

        // 返信する
        if (tweet.text.match(/こんにちは/)) {
          resolve(`${callAs}、こんにちは。`)

        } else if (tweet.text.match(/紅茶/)) {
          let tea = this.teaSelector(tweet.user)
          let message = this.rand.choice([
            `はい、${tea.name}を淹れてみましたわ。花言葉は${tea.language_of}ね。召し上がれ。`,
            `今日のお茶は${tea.name}よ。花言葉は${tea.language_of}ね。どうぞ召し上がれ。`
          ])
          resolve(message, tweet)

        } else if (tweet.text.match(/誕生日は[0-9]+月[0-9]+日/)) {
          let [, m, d] = (tweet.text.match(/誕生日は([12]?[0-9])月([1-3]?[0-9])日/) || [] )

          if( m !== undefined && d !== undefined) {

            // 不正な日付を与えられた時にガードする
            let dayOfMonth = [31,29,31,30,31,30,31,31,30,31,30,31][Number(m) - 1]
            if ( m > 0 && m <= 12 &&
                 d > 0 && d <= dayOfMonth) {

              return this.db.setBirthday(tweet.user, m, d)
                .then(() => {
                  resolve(`あなたの誕生日は${m}月${d}日なのね。覚えたわ。`)
                })
            } else {
              resolve(null)
            }
          }
        } else if (tweet.text.match(/「(.+)」((と|って)呼んで)/)) {
          let newNickname = (tweet.text.match(/「(.+)」/) || [])[1]
          if (newNickname !== undefined) {
            callAs = this._replaceNN(newNickname, tweet.user)
            resolve(`${callAs}…って呼べばいいのね。`)

          } else {
            resolve(null)
          }
        } else if (tweet.text.match(/献血/)) {
          return this.db.userIsDonated(tweet.user)
            .then( (donated) => {
              if (!donated) {
                // 献血していない場合
                let donateAmount = this.rand.genInt(200) + 200
                return this.db.addDonateLog(tweet.user, donateAmount)
                  .then( () => (resolve(`あなたの血を ${donateAmount} mL頂いたわ。お嬢様もきっと喜ぶと思うわ。どうもありがとう。`) ))

              } else {
                // 献血している場合
                resolve(`献血は1日1回までよ。`)
              }
            })
        } else if (tweet.text.match(/(俺|僕|私|(わ|あ)たし)の(こと|事)どう(思|おも)う(？|\?)/)) {
          // 整数値で管理する好感度は、表記上は0.05刻みの少数値として表記する。
          // 現在はひとまず、最小値0.00(0), 最大値5.00(100)としておく。
          let normalized_lovelity =
            userdata.lovelity > 100 ? 100 :
              userdata.lovelity < 0 ?   0 :
                userdata.lovelity
          let screen_lovelity = normalized_lovelity * 0.05

          resolve(`あなたへの好感度: ${screen_lovelity.toFixed(2)}`)

        } else {
          // リアクションプラグインから応答を返せる場合は応答する
          for (let plugin of this.reaction_plugins) {
            let reaction = plugin(tweet)
            if (reaction) {
              // 応答をランダムに選択
              let reply = this.rand.choice(reaction.reply_patterns)

              // 好感度の増減がある場合は増減させる
              return ( reaction.lovelity
                ? this.db.increaseLovelity(tweet.user, reaction.lovelity)
                : Promise.resolve(null)
              ).then( () => {
                resolve(reply)
              })
            }
          }

          resolve(null)
        }
      })

    }).then((reply) => {
      // ツイートがある場合はツイートする
      if (reply !== null) {
        this.client.tweet(reply, tweet).then((result) => {

          // 何か応答を行なった場合はログに残す
          if (result === null) return
          this.logger.debug(`[${result.id_str}]response: ${result.text}`)
          console.log(result)
        })
      }
    }).catch((err) => {
      if (err !== undefined) {
        this.logger.error(`error! : ${err}`)
        this.logger.trace(err.trace)
      }
    })

  }

  receive (event) {
    // 自身で発生したイベントは無視する
    if (event.source.screen_name === this.screen_name) return
    this.logger.info(`received event @${event.source.screen_name}: ${event.event}.`)

    // フォローイベント
    if (event.event === 'follow') {
      // Follower数がFollow数の15%以上であればフォローする
      if (event.source.friends_count * 0.15 < event.source.followers_count) {
        console.dir(this.client.follow(event.source.id_str))
        return this.client.follow(event.source.id_str).then((result) => {
          this.logger.info(`re-follow: @${event.source.screen_name}`)

          return Promise.resolve(result)
        })
      }
    }
    return Promise.resolve(null)
  }

  /******** 以降はprivate methodとして運用 ********/

  /**
   * 与えられた文字列をニックネームとして文字置換する
   * @param nickname {string} ニックネーム文字列
   * @param user {UserObject} ユーザオブジェクト
   * @return 置換後のニックネーム文字列
   */
  _replaceNN (nickname, user) {
    return nickname
      .replace(':NAME:', user.name.split(/[@＠]/)[0])
      .replace(':NAME_ORG:', user.name.replace(/[@＠]/, ' at '))
      .replace('[a-z]+://', '[url]')
  }
}

module.exports = SakuyaBot<commit_msg>bugfix: requiresにmin_lovelityが指定されている口上を読み込んだ際にエラーが発生する<commit_after>

'use strict'

const SakuyaBot = class {
  constructor (client, db, logger, rand) {
    this.client = client
    this.db = db
    this.logger = logger
    this.rand = rand

    // ツイートテキストを引数として、リアクションを返却する関数軍を
    // SakuyaBotに任意に組み込むことができる機能を提供する。
    // これを、リアクションプラグインと呼称し、
    // SakuyaBotの汎用的な口上を組み込む仕組みとして利用する。
    this.reaction_plugins = []

    this.teaSelector = require('./Teapot.js')

    // 起動時にアカウントの有効性を確認する
    this.client.verifyCredentials().then(
      (res) => {
        this.logger.info('Account verification succeed.')
        this.name = res.name
        this.screen_name = res.screen_name
        this.id = res.id_str
      }
    ).catch(
      () => {
        this.logger.fatal('Account verification failed.')
      }
    )
  }

  start () {
    this.logger.info('SakuyaBot started.')
    this.client.openUserStream({
      data: this.read.bind(this),
      error: (error) => console.log(error),
      event: this.receive.bind(this)
    })
  }

  /**
   * リアクションプラグインを組み込む
   * @param func リアクションプラグイン
   */
  addReactionPlugin (plugin) {
    this.reaction_plugins.push(plugin)
  }

  // 毎日 0時に実行する
  daily_work (today) {
    // 秒単位のずれで日付が前後することを防ぐため 12時間分加算する
    today = new Date(today.getTime() + 43200)

    let m = today.getMonth() + 1
    let d = today.getDate()
    return this.db.sumDonation().then( (amount) => {
      return this.client.tweet(`${m}月 ${d}日になったわね。 先日は ${amount} ml の献血をいただきましたわ。`)
    }).then(() => {
      return this.db.stashDonatedLog()
    }).catch((err) => {
      this.logger.error(err)
      this.logger.trace(err.trace)
    })
  }

  read (tweet) {
    this.logger.info(`[${tweet.id_str}] @${tweet.user.screen_name}: ${tweet.text}`)
    return new Promise((resolve,reject) => {

      // 自身以外へのrepliesを除外する
      let replies = (tweet.text.match(/@[0-9a-zA-Z]+/g) || []).filter((u) => u !== `@${this.screen_name}`)
      if (replies.length) reject()

      // 先頭が@でないツイートには反応しない
      if (!tweet.text.startsWith('@')) reject()

      resolve()
    }).then(() => {
      // ユーザの情報をDBから取得する
      return this.db.getUser(tweet.user)

    // ユーザ情報を作成する
    }).then( (user) => {
      if (user !== null) {
        return Promise.resolve(user)
      } else {
        return this.db.createUser(tweet.user)
      }
    }).then( (userdata) => {
      // createしていた場合は初期値のuser情報を設定
      if( userdata.ops !== undefined) userdata = userdata.ops[0]

      // 呼び名設定
      let callAs = this._replaceNN(userdata.nickname, tweet.user)

      return new Promise((resolve) => {

        // 返信する
        if (tweet.text.match(/こんにちは/)) {
          resolve(`${callAs}、こんにちは。`)

        } else if (tweet.text.match(/紅茶/)) {
          let tea = this.teaSelector(tweet.user)
          let message = this.rand.choice([
            `はい、${tea.name}を淹れてみましたわ。花言葉は${tea.language_of}ね。召し上がれ。`,
            `今日のお茶は${tea.name}よ。花言葉は${tea.language_of}ね。どうぞ召し上がれ。`
          ])
          resolve(message, tweet)

        } else if (tweet.text.match(/誕生日は[0-9]+月[0-9]+日/)) {
          let [, m, d] = (tweet.text.match(/誕生日は([12]?[0-9])月([1-3]?[0-9])日/) || [] )

          if( m !== undefined && d !== undefined) {

            // 不正な日付を与えられた時にガードする
            let dayOfMonth = [31,29,31,30,31,30,31,31,30,31,30,31][Number(m) - 1]
            if ( m > 0 && m <= 12 &&
                 d > 0 && d <= dayOfMonth) {

              return this.db.setBirthday(tweet.user, m, d)
                .then(() => {
                  resolve(`あなたの誕生日は${m}月${d}日なのね。覚えたわ。`)
                })
            } else {
              resolve(null)
            }
          }
        } else if (tweet.text.match(/「(.+)」((と|って)呼んで)/)) {
          let newNickname = (tweet.text.match(/「(.+)」/) || [])[1]
          if (newNickname !== undefined) {
            callAs = this._replaceNN(newNickname, tweet.user)
            resolve(`${callAs}…って呼べばいいのね。`)

          } else {
            resolve(null)
          }
        } else if (tweet.text.match(/献血/)) {
          return this.db.userIsDonated(tweet.user)
            .then( (donated) => {
              if (!donated) {
                // 献血していない場合
                let donateAmount = this.rand.genInt(200) + 200
                return this.db.addDonateLog(tweet.user, donateAmount)
                  .then( () => (resolve(`あなたの血を ${donateAmount} mL頂いたわ。お嬢様もきっと喜ぶと思うわ。どうもありがとう。`) ))

              } else {
                // 献血している場合
                resolve(`献血は1日1回までよ。`)
              }
            })
        } else if (tweet.text.match(/(俺|僕|私|(わ|あ)たし)の(こと|事)どう(思|おも)う(？|\?)/)) {
          // 整数値で管理する好感度は、表記上は0.05刻みの少数値として表記する。
          // 現在はひとまず、最小値0.00(0), 最大値5.00(100)としておく。
          let normalized_lovelity =
            userdata.lovelity > 100 ? 100 :
              userdata.lovelity < 0 ?   0 :
                userdata.lovelity
          let screen_lovelity = normalized_lovelity * 0.05

          resolve(`あなたへの好感度: ${screen_lovelity.toFixed(2)}`)

        } else {
          // リアクションプラグインから応答を返せる場合は応答する
          for (let plugin of this.reaction_plugins) {
            let reaction = plugin(tweet, userdata)
            if (reaction) {
              // 応答をランダムに選択
              let reply = this.rand.choice(reaction.reply_patterns)

              // 好感度の増減がある場合は増減させる
              return ( reaction.lovelity
                ? this.db.increaseLovelity(tweet.user, reaction.lovelity)
                : Promise.resolve(null)
              ).then( () => {
                resolve(reply)
              })
            }
          }

          resolve(null)
        }
      })

    }).then((reply) => {
      // ツイートがある場合はツイートする
      if (reply !== null) {
        this.client.tweet(reply, tweet).then((result) => {

          // 何か応答を行なった場合はログに残す
          if (result === null) return
          this.logger.debug(`[${result.id_str}]response: ${result.text}`)
          console.log(result)
        })
      }
    }).catch((err) => {
      if (err !== undefined) {
        this.logger.error(`error! : ${err}`)
        this.logger.trace(err.trace)
      }
    })

  }

  receive (event) {
    // 自身で発生したイベントは無視する
    if (event.source.screen_name === this.screen_name) return
    this.logger.info(`received event @${event.source.screen_name}: ${event.event}.`)

    // フォローイベント
    if (event.event === 'follow') {
      // Follower数がFollow数の15%以上であればフォローする
      if (event.source.friends_count * 0.15 < event.source.followers_count) {
        console.dir(this.client.follow(event.source.id_str))
        return this.client.follow(event.source.id_str).then((result) => {
          this.logger.info(`re-follow: @${event.source.screen_name}`)

          return Promise.resolve(result)
        })
      }
    }
    return Promise.resolve(null)
  }

  /******** 以降はprivate methodとして運用 ********/

  /**
   * 与えられた文字列をニックネームとして文字置換する
   * @param nickname {string} ニックネーム文字列
   * @param user {UserObject} ユーザオブジェクト
   * @return 置換後のニックネーム文字列
   */
  _replaceNN (nickname, user) {
    return nickname
      .replace(':NAME:', user.name.split(/[@＠]/)[0])
      .replace(':NAME_ORG:', user.name.replace(/[@＠]/, ' at '))
      .replace('[a-z]+://', '[url]')
  }
}

module.exports = SakuyaBot<|endoftext|>


<commit_before>/**
 * Dropzone div that initiates all sharing actions
 *
 */
betterlink_user_interface.createModule("Dropzone.Nexus", function(api, apiInternal) {
	api.requireModules( ["Util.DOM", "LastSubmission", "Draggable", "Drawer Dropzone"] );

	var NO_SUBMISSION_TEXT = document.createTextNode("drag | drop | share"),
		ABOUT_TO_SHARE_TEXT = document.createTextNode("drop to share"),
		LINK_SUBMITTED_TEXT = document.createTextNode("choose where");

	var NEXUS_CLASS = "nexus",
		DRAGGING_CLASS = "dragging",
		HAS_SUBMISSION_CLASS = "has-submission",
		HAS_ERROR_CLASS = "has-error";
	var NEXUS_CSS =
		[   "." + apiInternal.dropzone.HOVER_CLASS + "." + NEXUS_CLASS + " { background-color: #1CD3A2; color: #333; }",
			"." + apiInternal.dropzone.CLASS + "." + NEXUS_CLASS + "." + DRAGGING_CLASS + " { padding-top: 50px; padding-bottom: 50px }",
			"." + apiInternal.dropzone.CLASS + "." + NEXUS_CLASS + " { -webkit-transition: padding 0.3s ease; transition: padding 0.3s ease; }"].join(' ');

	var stylesInitialized = false;
	var nexusDropzone;

	apiInternal.dropzone.sharingNexus = {
		create: createDropzone
	};
	/****************************************************************************************************/

	function createDropzone(submissionFn) {
		if(!stylesInitialized) {
			insertStyles();
		}

		nexusDropzone = apiInternal.dropzone.create(NO_SUBMISSION_TEXT.nodeValue);
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, NEXUS_CLASS);

		triggerChangeOnDrag();
		triggerSubmissionOnDrop(submissionFn);
		triggerChooseOnSubmission();

		return nexusDropzone;
	}

	// When the user is dragging a potential submission, alter the dislplay of
	// the div to highlight where to drop the content
	function triggerChangeOnDrag() {
		apiInternal.draggable.subscribeGlobal.dragstart(alertToDrop);
		apiInternal.draggable.subscribeGlobal.dragend(revertDraggingChange);
	}

	// When the user drops a potential submission on this element, submit it
	function triggerSubmissionOnDrop(submissionFn) {
		nexusDropzone.subscribeToDrop(submissionFn);
	}

	// When the user has a successful submission, show them options to share their
	// submission
	function triggerChooseOnSubmission() {
		apiInternal.lastSubmission.subscribeSuccess.oncomplete(alertToChoose);
	}

	// Inform the user that they should drop their submission in this dropzone to
	// start their share action.
	function alertToDrop() {
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, DRAGGING_CLASS);
		apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, ABOUT_TO_SHARE_TEXT);
	}

	// Set the content of the dropzone back to where it was before dragging
	function revertDraggingChange() {
		apiInternal.util.dom.removeClassFromElement(nexusDropzone.element, DRAGGING_CLASS);

		if(apiInternal.lastSubmission.lastSuccessful.exists()) {
			alertToChoose();
		}
		else {
			apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, NO_SUBMISSION_TEXT);
		}
	}

	// Inform the user they should choose the service to use to copmlete their share
	// action.
	function alertToChoose() {
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, HAS_SUBMISSION_CLASS);
		apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, LINK_SUBMITTED_TEXT);
	}

	function insertStyles() {
		stylesInitialized = true;
		apiInternal.util.dom.createAndAppendStyleElement(NEXUS_CSS);
	}
});
<commit_msg>show loading element on submission<commit_after>/**
 * Dropzone div that initiates all sharing actions
 *
 */
betterlink_user_interface.createModule("Dropzone.Nexus", function(api, apiInternal) {
	api.requireModules( ["Util.DOM", "LastSubmission", "Draggable", "Drawer Dropzone"] );

	var NO_SUBMISSION_TEXT = document.createTextNode("drag | drop | share"),
		LOADING_TEXT = document.createTextNode("Loading..."),
		ABOUT_TO_SHARE_TEXT = document.createTextNode("drop to share"),
		LINK_SUBMITTED_TEXT = document.createTextNode("choose where");

	var NEXUS_CLASS = "nexus",
		DRAGGING_CLASS = "dragging",
		HAS_SUBMISSION_CLASS = "has-submission",
		HAS_ERROR_CLASS = "has-error";
	var NEXUS_CSS =
		[   "." + apiInternal.dropzone.HOVER_CLASS + "." + NEXUS_CLASS + " { background-color: #1CD3A2; color: #333; }",
			"." + apiInternal.dropzone.CLASS + "." + NEXUS_CLASS + "." + DRAGGING_CLASS + " { padding-top: 50px; padding-bottom: 50px }",
			"." + apiInternal.dropzone.CLASS + "." + NEXUS_CLASS + " { -webkit-transition: padding 0.3s ease; transition: padding 0.3s ease; }"].join(' ');

	var stylesInitialized = false;
	var nexusDropzone;

	apiInternal.dropzone.sharingNexus = {
		create: createDropzone
	};
	/****************************************************************************************************/

	function createDropzone(submissionFn) {
		if(!stylesInitialized) {
			insertStyles();
		}

		nexusDropzone = apiInternal.dropzone.create(NO_SUBMISSION_TEXT.nodeValue);
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, NEXUS_CLASS);

		triggerChangeOnDrag();
		triggerSubmissionOnDrop(submissionFn);
		triggerLoadingOnSubmission();
		triggerChooseOnSuccess();

		return nexusDropzone;
	}

	// When the user is dragging a potential submission, alter the dislplay of
	// the div to highlight where to drop the content
	function triggerChangeOnDrag() {
		apiInternal.draggable.subscribeGlobal.dragstart(alertToDrop);
		apiInternal.draggable.subscribeGlobal.dragend(revertDraggingChange);
	}

	// When the user drops a potential submission on this element, submit it
	function triggerSubmissionOnDrop(submissionFn) {
		nexusDropzone.subscribeToDrop(submissionFn);
	}

	// When the user makes a new submission, display that we are processing
	function triggerLoadingOnSubmission() {
		apiInternal.lastSubmission.subscribeSuccess.onsubmit(alertLoading);
	}

	// When the user has a successful submission, show them options to share their
	// submission
	function triggerChooseOnSuccess() {
		apiInternal.lastSubmission.subscribeSuccess.oncomplete(alertToChoose);
	}

	// Inform the user that they should drop their submission in this dropzone to
	// start their share action.
	function alertToDrop() {
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, DRAGGING_CLASS);
		apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, ABOUT_TO_SHARE_TEXT);
	}

	// Set the content of the dropzone back to where it was before dragging
	function revertDraggingChange() {
		apiInternal.util.dom.removeClassFromElement(nexusDropzone.element, DRAGGING_CLASS);

		if(apiInternal.lastSubmission.lastSuccessful.exists()) {
			alertToChoose();
		}
		else {
			apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, NO_SUBMISSION_TEXT);
		}
	}

	// Inform the user that we're generating their link
	function alertLoading() {
		apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, LOADING_TEXT);
	}

	// Inform the user they should choose the service to use to copmlete their share
	// action.
	function alertToChoose() {
		apiInternal.util.dom.applyClassToElement(nexusDropzone.element, HAS_SUBMISSION_CLASS);
		apiInternal.util.dom.addOrReplaceChild(nexusDropzone.element, LINK_SUBMITTED_TEXT);
	}

	function insertStyles() {
		stylesInitialized = true;
		apiInternal.util.dom.createAndAppendStyleElement(NEXUS_CSS);
	}
});
<|endoftext|>


<commit_before>const values = [0.710852, 0.918123, 0.022008, 0.063375, 0.227326];
function rate(iLikes,iComments,iTags)
{
    likes = iLikes*2
    comments = iComments*2
    tags = iTags*2

    z = values[0];
    x = values[1];
    a = values[2];
    b = values[3];
    c = values[4];

    return z * (x * ((a * likes) + (b * comments)) + (c * c * tags));
}

module.exports = {rate}
<commit_msg>Score update<commit_after>const values = [0.710852, 0.918123, 0.022008, 0.063375, 0.227326];
function rate(iLikes,iComments,iTags)
{
    likes = iLikes*2
    comments = iComments*2
    tags = iTags*2

    z = values[0];
    x = values[1];
    a = values[2];
    b = values[3];
    c = values[4];

    return z * (x * ((a * likes) + (b * comments)) + (c * c * tags));
}

module.exports = {rate}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { ButtonGroup, Button } from 'react-bootstrap';

import AppActions from '../../actions/AppActions';

import utils from '../../utils/utils';

/*
|--------------------------------------------------------------------------
| Header - Queue
|--------------------------------------------------------------------------
*/

export default class Queue extends Component {

    constructor(props) {

        super(props);
        this.state = {
            draggedTrack     : null,
            draggedOverTrack : null,
            draggedBefore    : true
        }
    }

    render() {

        var self           = this;
        var playlist       = this.props.playlist;
        var playlistCursor = this.props.playlistCursor;

        var queue = playlist.slice(playlistCursor + 1, playlistCursor + 21); // Get the 20 next tracks displayed

        var wholeQueue = playlist.slice(playlistCursor + 1);
        var wholeQueueDuration = 0;

        for(var i = 0; i < wholeQueue.length; i++) {
            wholeQueueDuration += wholeQueue[i].duration;
        }

        if(queue.length == 0) {
            return(

                        queue is empty

            );
        } else {

            var queueContent = queue.map(function (track, index) {

                var classes = 'track';

                if(index === self.state.draggedTrack) {
                    classes = 'track dragged';
                } else if (index === self.state.draggedOverTrack) {
                    if(!self.state.draggedBefore) {
                        classes = 'track dragged-over-after';
                    } else {
                        classes = 'track dragged-over';
                    }
                }
                else {
                    classes = 'track';
                }

                return (

                        <Button bsSize={'xsmall'} bsStyle={'link'} className='remove' onClick={ self.removeFromQueue.bind(null, index) }>
                            ×
                        </Button>

                            { track.title }

                            { track.artist } - { track.album }

                );
            });
        }

        return (

                        { utils.getStatus(wholeQueue) }

                    <ButtonGroup>
                        <Button bsSize={'xsmall'} bsStyle={'default'} className='empty-button' onClick={ this.clearQueue }>
                            clear queue
                        </Button>
                    </ButtonGroup>

                    { queueContent }

        );
    }

    clearQueue() {
        AppActions.queue.clear();
    }

    removeFromQueue(index) {
        AppActions.queue.remove(index);
    }

    dragStart(index, e) {

        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/html', e.currentTarget);

        this.draggedIndex = index;
    }

    dragEnd(e) {

        var playlist       = this.props.playlist;
        var playlistCursor = this.props.playlistCursor;

        var draggedTrack     = this.state.draggedTrack;
        var draggedOverTrack = this.state.draggedOverTrack

        var newPlaylist = playlist.slice();
        var trackToMove = playlist[playlistCursor + 1 + draggedTrack];
        newPlaylist.splice(playlistCursor + 1 + draggedTrack, 1);
        newPlaylist.splice(playlistCursor + draggedOverTrack, 0, trackToMove);

        this.setState({
            draggedOverTrack : null,
            draggedTrack     : null
        });

        AppActions.queue.setPlaylist(newPlaylist);
    }

    dragOver(e) {

        e.preventDefault();

        var currentTarget = e.currentTarget;
        var offsetTop     = currentTarget.parentNode.offsetTop + currentTarget.parentNode.parentNode.offsetTop;

        var yEnd  = e.pageY + currentTarget.scrollTop - offsetTop;
        var limit = currentTarget.scrollHeight - currentTarget.lastChild.offsetHeight / 2;

        // If the element is dragged after the half of the last one
        var draggedBefore = yEnd > limit ? false : true;
        var draggedOverTrack = Math.ceil((e.pageY + document.querySelector('.queue-body').scrollTop - 75) / 45)

        // souldn't change. Is here otherwise dragOver wouldn't be triggered
        var index = this.draggedIndex;

        this.setState({
            draggedTrack     : index,
            draggedOverTrack : draggedOverTrack,
            draggedBefore    : draggedBefore,
            dragging         : true
        });
    }
}
<commit_msg>Remove useless code<commit_after>import React, { Component } from 'react';
import { ButtonGroup, Button } from 'react-bootstrap';

import AppActions from '../../actions/AppActions';

import utils from '../../utils/utils';

/*
|--------------------------------------------------------------------------
| Header - Queue
|--------------------------------------------------------------------------
*/

export default class Queue extends Component {

    constructor(props) {

        super(props);
        this.state = {
            draggedTrack     : null,
            draggedOverTrack : null,
            draggedBefore    : true
        }
    }

    render() {

        var self           = this;
        var playlist       = this.props.playlist;
        var playlistCursor = this.props.playlistCursor;

        var queue = playlist.slice(playlistCursor + 1, playlistCursor + 21); // Get the 20 next tracks displayed

        var wholeQueue = playlist.slice(playlistCursor + 1);

        if(queue.length == 0) {
            return(

                        queue is empty

            );
        } else {

            var queueContent = queue.map(function (track, index) {

                var classes = 'track';

                if(index === self.state.draggedTrack) {
                    classes = 'track dragged';
                } else if (index === self.state.draggedOverTrack) {
                    if(!self.state.draggedBefore) {
                        classes = 'track dragged-over-after';
                    } else {
                        classes = 'track dragged-over';
                    }
                }
                else {
                    classes = 'track';
                }

                return (

                        <Button bsSize={'xsmall'} bsStyle={'link'} className='remove' onClick={ self.removeFromQueue.bind(null, index) }>
                            ×
                        </Button>

                            { track.title }

                            { track.artist } - { track.album }

                );
            });
        }

        return (

                        { utils.getStatus(wholeQueue) }

                    <ButtonGroup>
                        <Button bsSize={'xsmall'} bsStyle={'default'} className='empty-button' onClick={ this.clearQueue }>
                            clear queue
                        </Button>
                    </ButtonGroup>

                    { queueContent }

        );
    }

    clearQueue() {
        AppActions.queue.clear();
    }

    removeFromQueue(index) {
        AppActions.queue.remove(index);
    }

    dragStart(index, e) {

        e.dataTransfer.effectAllowed = 'move';
        e.dataTransfer.setData('text/html', e.currentTarget);

        this.draggedIndex = index;
    }

    dragEnd(e) {

        var playlist       = this.props.playlist;
        var playlistCursor = this.props.playlistCursor;

        var draggedTrack     = this.state.draggedTrack;
        var draggedOverTrack = this.state.draggedOverTrack

        var newPlaylist = playlist.slice();
        var trackToMove = playlist[playlistCursor + 1 + draggedTrack];
        newPlaylist.splice(playlistCursor + 1 + draggedTrack, 1);
        newPlaylist.splice(playlistCursor + draggedOverTrack, 0, trackToMove);

        this.setState({
            draggedOverTrack : null,
            draggedTrack     : null
        });

        AppActions.queue.setPlaylist(newPlaylist);
    }

    dragOver(e) {

        e.preventDefault();

        var currentTarget = e.currentTarget;
        var offsetTop     = currentTarget.parentNode.offsetTop + currentTarget.parentNode.parentNode.offsetTop;

        var yEnd  = e.pageY + currentTarget.scrollTop - offsetTop;
        var limit = currentTarget.scrollHeight - currentTarget.lastChild.offsetHeight / 2;

        // If the element is dragged after the half of the last one
        var draggedBefore = yEnd > limit ? false : true;
        var draggedOverTrack = Math.ceil((e.pageY + document.querySelector('.queue-body').scrollTop - 75) / 45)

        // souldn't change. Is here otherwise dragOver wouldn't be triggered
        var index = this.draggedIndex;

        this.setState({
            draggedTrack     : index,
            draggedOverTrack : draggedOverTrack,
            draggedBefore    : draggedBefore,
            dragging         : true
        });
    }
}
<|endoftext|>


<commit_before>// Copyright (c) 2011-2012 UX Productivity. All rights reserved.

// FIXME: cursor does not display correctly if it is after a space at the end of the line

(function() {

    var selectionDivs = new Array();
    var selectionRange = null;

    // public
    function updateSelectionDisplay()
    {
        for (var i = 0; i < selectionDivs.length; i++)
            selectionDivs[i].parentNode.removeChild(selectionDivs[i]);
        selectionDivs = new Array();

        var rects = null;
        if (selectionRange != null)
            rects = selectionRange.getClientRects();

        if ((selectionRange != null) && selectionRange.isEmpty()) {

            // We just have a cursor

            if ((rects != null) && (rects.length == 0)) {

                // If the cursor is at the end of a paragraph and the last character is a space,
                // getClientRects() fails to return anything. So instead, we temporarily add a
                // single character to the end of the paragraph, get the client rect for that,
                // and then remove the character. This client rect will be in the same location
                // as the cursor placed at the end of the space.

                    var node = selectionRange.start.node;
                    var offset = selectionRange.start.offset;

                if ((node.nodeType == Node.TEXT_NODE) && (offset == node.nodeValue.length)) {
                    var tempNode = document.createTextNode("X");
                    node.parentNode.insertBefore(tempNode,node.nextSibling);
                    var tempRange = new Range(tempNode,0,tempNode,0);
                    rects = tempRange.getClientRects();
                    node.parentNode.removeChild(tempNode);
                }
            }

            var left;
            var top;
            var width;
            var height;

            if ((rects != null) && (rects.length > 0)) {
                left = rects[0].left + window.scrollX;
                top = rects[0].top + window.scrollY;
                width = rects[0].width;
                height = rects[0].height;
            }
            else {
                var absolute = getAbsoluteOffset(selectionRange.start.node);
                left = absolute.offsetLeft;
                top = absolute.offsetTop;
                width = selectionRange.start.node.parentNode.offsetWidth;
                height = selectionRange.start.node.parentNode.offsetHeight;
            }

            var zoom = getZoom();
            editor.setCursor(left*zoom,top*zoom,height*zoom);
            return;
        }

        if ((rects != null) && (rects.length > 0)) {
            var boundsLeft = null;
            var boundsRight = null;
            var boundsTop = null;
            var boundsBottom = null

            for (var i = 0; i < rects.length; i++) {
                var div = document.createElement("DIV");
                div.style.position = "absolute";

                var left = rects[i].left + window.scrollX;
                var top = rects[i].top + window.scrollY;
                var width = rects[i].width;
                var height = rects[i].height;
                var right = left + width;
                var bottom = top + height;

                if (boundsLeft == null) {
                    boundsLeft = left;
                    boundsTop = top;
                    boundsRight = right;
                    boundsBottom = bottom;
                }
                else {
                    if (boundsLeft > left)
                        boundsLeft = left;
                    if (boundsRight < right)
                        boundsRight = right;
                    if (boundsTop > top)
                        boundsTop = top;
                    if (boundsBottom < bottom)
                        boundsBottom = bottom;
                }

                div.style.left = left+"px";
                div.style.top = top+"px";
                div.style.width = width+"px";
                div.style.height = height+"px";
                div.style.backgroundColor = "rgb(201,221,238)";
                div.style.opacity = "30%";
                div.style.zIndex = -1;
                document.body.appendChild(div);
                selectionDivs.push(div);
            }

            var firstRect = rects[0];
            var lastRect = rects[rects.length-1];

            var zoom = getZoom();
            var x1 = (firstRect.left+window.scrollX)*zoom;
            var y1 = (firstRect.top+window.scrollY)*zoom;
            var height1 = firstRect.height*zoom;
            var x2 = (lastRect.right+window.scrollX)*zoom;
            var y2 = (lastRect.top+window.scrollY)*zoom;
            var height2 = lastRect.height*zoom;

            editor.setSelectionHandles(x1,y1,height1,x2,y2,height2);
            editor.setSelectionBounds(boundsLeft*zoom,boundsTop*zoom,
                                      boundsRight*zoom,boundsBottom*zoom);
        }
        else {
            editor.clearSelectionHandlesAndCursor();
        }

        function getAbsoluteOffset(node)
        {
            var offsetLeft = 0;
            var offsetTop = 0;
            for (; node != null; node = node.parentNode) {
                if (node.offsetLeft != null)
                    offsetLeft += node.offsetLeft;
                if (node.offsetTop != null)
                    offsetTop += node.offsetTop;
            }
            return { offsetLeft: offsetLeft, offsetTop: offsetTop };
        }
    }

    // public
    function selectAll()
    {
        selectionRange = new Range(document.body,0,
                                   document.body,document.body.childNodes.length);
        updateSelectionDisplay();
    }

    // public
    function beginSelectionAtCoords(x,y)
    {
        selectionRange = null;

        var zoom = getZoom();
        var pos = positionAtPoint(x/zoom,y/zoom);
        if (pos != null) {
            if (pos.node.nodeType == Node.TEXT_NODE) {
                selectionRange = new Range(pos.node,pos.offset,pos.node,pos.offset);
                selectionRange.start.moveToStartOfWord();
                selectionRange.end.moveToEndOfWord();
            }
        }

        updateSelectionDisplay();
    }

    // public
    function setSelectionStartAtCoords(x,y)
    {
        var zoom = getZoom();
        var position = positionAtPoint(x/zoom,y/zoom);
        if (position != null) {
            selectionRange.start = position;
            updateSelectionDisplay();
        }
    }

    // public
    function setSelectionEndAtCoords(x,y)
    {
        var zoom = getZoom();
        var position = positionAtPoint(x/zoom,y/zoom);
        if (position != null) {
            selectionRange.end = position;
            updateSelectionDisplay();
        }
    }

    // public
    function getSelectionRange()
    {
        return selectionRange;
    }

    // public
    function setSelectionRange(range)
    {
        selectionRange = range;
        updateSelectionDisplay();
    }

    function ensureCursorVisible()
    {
        var rects = selectionRange.getClientRects();
        if (rects.length > 0) {
            var lastRect = rects[rects.length-1];
            var cursorY = lastRect.top + window.scrollY + lastRect.height/2;
            if ((cursorY < window.scrollY) ||
                (cursorY > window.scrollY + window.innerHeight)) {
                var newScrollY = cursorY - window.innerHeight/2;
                if (newScrollY < 0)
                    newScrollY = 0;
                window.scrollTo(window.scrollX,newScrollY);
            }
        }
    }

    // public
    function setEmptySelectionAt(node,offset)
    {
        setSelectionRange(new Range(node,offset,node,offset));
        ensureCursorVisible();
    }

    // public
    function deleteSelectionContents()
    {
        if (selectionRange == null)
            return;

        var finalNode = selectionRange.start.node;
        var finalOffset = selectionRange.start.offset;

        var nodes = selectionRange.getOutermostSelectedNodes();
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];

            var removeWholeNode = false;

            if ((node == selectionRange.start.node) &&
                (node == selectionRange.end.node)) {
                var startOffset = selectionRange.start.offset;
                var endOffset = selectionRange.end.offset;
                if ((node.nodeType == Node.TEXT_NODE) &&
                    ((startOffset > 0) || (endOffset < node.nodeValue.length))) {
                    node.nodeValue = node.nodeValue.slice(0,startOffset) +
                                     node.nodeValue.slice(endOffset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else if (node == selectionRange.start.node) {
                var offset = selectionRange.start.offset;
                if ((node.nodeType == Node.TEXT_NODE) && (offset > 0)) {
                    node.nodeValue = node.nodeValue.slice(0,offset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else if (node == selectionRange.end.node) {
                var offset = selectionRange.end.offset;
                if ((node.nodeType == Node.TEXT_NODE) && (offset < node.nodeValue.length)) {
                    node.nodeValue = node.nodeValue.slice(offset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else {
                removeWholeNode = true;
            }

            if (removeWholeNode) {
                if (finalNode == node) {
                    finalNode = node.parentNode;
                    finalOffset = 0;
                }
                node.parentNode.removeChild(node);
            }
        }

        setEmptySelectionAt(finalNode,finalOffset);
    }

    // public
    function clearSelection()
    {
        selectionRange = null;
        updateSelectionDisplay();
    }

    window.updateSelectionDisplay = updateSelectionDisplay;
    window.selectAll = selectAll;
    window.beginSelectionAtCoords = beginSelectionAtCoords;
    window.setSelectionStartAtCoords = setSelectionStartAtCoords;
    window.setSelectionEndAtCoords = setSelectionEndAtCoords;
    window.getSelectionRange = getSelectionRange;
    window.setSelectionRange = setSelectionRange;
    window.setEmptySelectionAt = setEmptySelectionAt;
    window.deleteSelectionContents = deleteSelectionContents;
    window.clearSelection = clearSelection;

})();
<commit_msg>An alternate workaround for the caretRangeFromPoint bug initially addressed in revision 581. Previously, we first checked if the point was in the elements bounding rect, but this meant that it wasn't possible to place the cursor at the nearest node, if the click location was not exactly on a node.<commit_after>// Copyright (c) 2011-2012 UX Productivity. All rights reserved.

// FIXME: cursor does not display correctly if it is after a space at the end of the line

(function() {

    var selectionDivs = new Array();
    var selectionRange = null;

    // public
    function updateSelectionDisplay()
    {
        for (var i = 0; i < selectionDivs.length; i++)
            selectionDivs[i].parentNode.removeChild(selectionDivs[i]);
        selectionDivs = new Array();

        var rects = null;
        if (selectionRange != null)
            rects = selectionRange.getClientRects();

        debug("");
        debug("selectionRange = "+selectionRange);

        if ((selectionRange != null) && selectionRange.isEmpty()) {

            // We just have a cursor

            if ((rects != null) &&
                ((rects.length == 0) || (rects[0].width == 0) || (rects[0].height == 0))) {

                // If the cursor is at the end of a paragraph and the last character is a space,
                // getClientRects() fails to return anything. So instead, we temporarily add a
                // single character to the end of the paragraph, get the client rect for that,
                // and then remove the character. This client rect will be in the same location
                // as the cursor placed at the end of the space.

                debug("No rects, so creating one from temporary node");

/*

                var position = selectionRange.start;
                var location = position.toLocation();

                var candidate = false;
                if ((position.node.nodeType == Node.ELEMENT_NODE) ||
                    ((position.node.nodeType == Node.TEXT_NODE) &&
                     (position.offset == node.nodeValue.length))) {

                    debug("position = "+position);
                    debug("location = "+location);
                }
*/

                var node = selectionRange.start.node;
                var offset = selectionRange.start.offset;

                if ((node.nodeType == Node.ELEMENT_NODE) ||
                    (node.nodeType == Node.TEXT_NODE) && (offset == node.nodeValue.length)) {
                    var tempNode = document.createTextNode("X");

                    if (node.nodeType == Node.TEXT_NODE) {
                        debug("tempNode: inserting after text node");
                        node.parentNode.insertBefore(tempNode,node.nextSibling);
                    }
                    else if (node.nodeType == Node.ELEMENT_NODE) {
                        if (offset >= node.childNodes.length) {
                            if (node.lastChild != null)
                                debug("tempNode: appending to element after "+
                                      nodeString(node.lastChild));
                            else
                                debug("tempNode: appending to element");
                            node.appendChild(tempNode);
                        }
                        else {
                            debug("tempNode: inserting before "+node.childNodes[offset]);
                            node.insertBefore(tempNode,node.childNodes[offset]);
                        }
                    }
                    var tempRange = new Range(tempNode,0,tempNode,0);
                    rects = tempRange.getClientRects();
//                    node.parentNode.removeChild(tempNode);
                    tempNode.parentNode.removeChild(tempNode);
                }
                else {
                    debug("tempNode: Not a candidate");
                }

/*
                if ((node.nodeType == Node.TEXT_NODE) && (offset == node.nodeValue.length)) {
                    var tempNode = document.createTextNode("X");
                    node.parentNode.insertBefore(tempNode,node.nextSibling);
                    var tempRange = new Range(tempNode,0,tempNode,0);
                    rects = tempRange.getClientRects();
                    node.parentNode.removeChild(tempNode);
                    debug("Is a candidate");
                }
                else {
                    debug("Not a candidate");
                }
*/
            }

            var left;
            var top;
            var width;
            var height;

            if ((rects != null) && (rects.length > 0)) {
                left = rects[0].left + window.scrollX;
                top = rects[0].top + window.scrollY;
                width = rects[0].width;
                height = rects[0].height;
                debug("updateSelectionDisplay: rect for cursor at "+selectionRange.start+": ("+
                     left+","+top+","+width+","+height+")");
            }
            else {
                debug("updateSelectionDisplay: no rects for cursor at "+selectionRange.start);
                var absolute = getAbsoluteOffset(selectionRange.start.node);
                left = absolute.offsetLeft;
                top = absolute.offsetTop;
                width = selectionRange.start.node.parentNode.offsetWidth;
                height = selectionRange.start.node.parentNode.offsetHeight;
            }

            var zoom = getZoom();
            editor.setCursor(left*zoom,top*zoom,height*zoom);
            return;
        }

        if ((rects != null) && (rects.length > 0)) {
            var boundsLeft = null;
            var boundsRight = null;
            var boundsTop = null;
            var boundsBottom = null

            for (var i = 0; i < rects.length; i++) {
                var div = document.createElement("DIV");
                div.style.position = "absolute";

                var left = rects[i].left + window.scrollX;
                var top = rects[i].top + window.scrollY;
                var width = rects[i].width;
                var height = rects[i].height;
                var right = left + width;
                var bottom = top + height;

                if (boundsLeft == null) {
                    boundsLeft = left;
                    boundsTop = top;
                    boundsRight = right;
                    boundsBottom = bottom;
                }
                else {
                    if (boundsLeft > left)
                        boundsLeft = left;
                    if (boundsRight < right)
                        boundsRight = right;
                    if (boundsTop > top)
                        boundsTop = top;
                    if (boundsBottom < bottom)
                        boundsBottom = bottom;
                }

                div.style.left = left+"px";
                div.style.top = top+"px";
                div.style.width = width+"px";
                div.style.height = height+"px";
                div.style.backgroundColor = "rgb(201,221,238)";
                div.style.opacity = "30%";
                div.style.zIndex = -1;
                document.body.appendChild(div);
                selectionDivs.push(div);
            }

            var firstRect = rects[0];
            var lastRect = rects[rects.length-1];

            var zoom = getZoom();
            var x1 = (firstRect.left+window.scrollX)*zoom;
            var y1 = (firstRect.top+window.scrollY)*zoom;
            var height1 = firstRect.height*zoom;
            var x2 = (lastRect.right+window.scrollX)*zoom;
            var y2 = (lastRect.top+window.scrollY)*zoom;
            var height2 = lastRect.height*zoom;

            editor.setSelectionHandles(x1,y1,height1,x2,y2,height2);
            editor.setSelectionBounds(boundsLeft*zoom,boundsTop*zoom,
                                      boundsRight*zoom,boundsBottom*zoom);
        }
        else {
            editor.clearSelectionHandlesAndCursor();
        }

        function getAbsoluteOffset(node)
        {
            var offsetLeft = 0;
            var offsetTop = 0;
            for (; node != null; node = node.parentNode) {
                if (node.offsetLeft != null)
                    offsetLeft += node.offsetLeft;
                if (node.offsetTop != null)
                    offsetTop += node.offsetTop;
            }
            return { offsetLeft: offsetLeft, offsetTop: offsetTop };
        }
    }

    // public
    function selectAll()
    {
        selectionRange = new Range(document.body,0,
                                   document.body,document.body.childNodes.length);
        updateSelectionDisplay();
    }

    // public
    function beginSelectionAtCoords(x,y)
    {
        selectionRange = null;

        var zoom = getZoom();
        var pos = positionAtPoint(x/zoom,y/zoom);
        if (pos != null) {
            if (pos.node.nodeType == Node.TEXT_NODE) {
                selectionRange = new Range(pos.node,pos.offset,pos.node,pos.offset);
                selectionRange.start.moveToStartOfWord();
                selectionRange.end.moveToEndOfWord();
            }
        }

        updateSelectionDisplay();
    }

    // public
    function setSelectionStartAtCoords(x,y)
    {
        var zoom = getZoom();
        var position = positionAtPoint(x/zoom,y/zoom);
        if (position != null) {
            selectionRange.start = position;
            updateSelectionDisplay();
        }
    }

    // public
    function setSelectionEndAtCoords(x,y)
    {
        var zoom = getZoom();
        var position = positionAtPoint(x/zoom,y/zoom);
        if (position != null) {
            selectionRange.end = position;
            updateSelectionDisplay();
        }
    }

    // public
    function getSelectionRange()
    {
        return selectionRange;
    }

    // public
    function setSelectionRange(range)
    {
        selectionRange = range;
        updateSelectionDisplay();
    }

    function ensureCursorVisible()
    {
        var rects = selectionRange.getClientRects();
        if (rects.length > 0) {
            var lastRect = rects[rects.length-1];
            var cursorY = lastRect.top + window.scrollY + lastRect.height/2;
            if ((cursorY < window.scrollY) ||
                (cursorY > window.scrollY + window.innerHeight)) {
                var newScrollY = cursorY - window.innerHeight/2;
                if (newScrollY < 0)
                    newScrollY = 0;
                window.scrollTo(window.scrollX,newScrollY);
            }
        }
    }

    // public
    function setEmptySelectionAt(node,offset)
    {
        setSelectionRange(new Range(node,offset,node,offset));
        ensureCursorVisible();
    }

    // public
    function deleteSelectionContents()
    {
        if (selectionRange == null)
            return;

        var finalNode = selectionRange.start.node;
        var finalOffset = selectionRange.start.offset;

        var nodes = selectionRange.getOutermostSelectedNodes();
        for (var i = 0; i < nodes.length; i++) {
            var node = nodes[i];

            var removeWholeNode = false;

            if ((node == selectionRange.start.node) &&
                (node == selectionRange.end.node)) {
                var startOffset = selectionRange.start.offset;
                var endOffset = selectionRange.end.offset;
                if ((node.nodeType == Node.TEXT_NODE) &&
                    ((startOffset > 0) || (endOffset < node.nodeValue.length))) {
                    node.nodeValue = node.nodeValue.slice(0,startOffset) +
                                     node.nodeValue.slice(endOffset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else if (node == selectionRange.start.node) {
                var offset = selectionRange.start.offset;
                if ((node.nodeType == Node.TEXT_NODE) && (offset > 0)) {
                    node.nodeValue = node.nodeValue.slice(0,offset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else if (node == selectionRange.end.node) {
                var offset = selectionRange.end.offset;
                if ((node.nodeType == Node.TEXT_NODE) && (offset < node.nodeValue.length)) {
                    node.nodeValue = node.nodeValue.slice(offset);
                }
                else {
                    removeWholeNode = true;
                }
            }
            else {
                removeWholeNode = true;
            }

            if (removeWholeNode) {
                if (finalNode == node) {
                    finalNode = node.parentNode;
                    finalOffset = 0;
                }
                node.parentNode.removeChild(node);
            }
        }

        setEmptySelectionAt(finalNode,finalOffset);
    }

    // public
    function clearSelection()
    {
        selectionRange = null;
        updateSelectionDisplay();
    }

    window.updateSelectionDisplay = updateSelectionDisplay;
    window.selectAll = selectAll;
    window.beginSelectionAtCoords = beginSelectionAtCoords;
    window.setSelectionStartAtCoords = setSelectionStartAtCoords;
    window.setSelectionEndAtCoords = setSelectionEndAtCoords;
    window.getSelectionRange = getSelectionRange;
    window.setSelectionRange = setSelectionRange;
    window.setEmptySelectionAt = setEmptySelectionAt;
    window.deleteSelectionContents = deleteSelectionContents;
    window.clearSelection = clearSelection;

})();
<|endoftext|>


<commit_before>'use strict';

angular.module('copayApp.controllers').controller('txpDetailsController', function($scope, $rootScope, $timeout, $interval, $ionicModal, $log, ongoingProcess, platformInfo, $ionicScrollDelegate, txFormatService, fingerprintService, bwcError, gettextCatalog, lodash, walletService, popupService, $state, $ionicHistory) {
  var self = $scope.self;
  var tx = $scope.tx;
  var copayers = $scope.copayers;
  var isGlidera = $scope.isGlidera;
  var GLIDERA_LOCK_TIME = 6 * 60 * 60;
  var now = Math.floor(Date.now() / 1000);
  var countDown;

  $scope.init = function() {
    $scope.loading = null;
    $scope.isCordova = platformInfo.isCordova;
    $scope.copayerId = $scope.wallet.credentials.copayerId;
    $scope.isShared = $scope.wallet.credentials.n > 1;
    $scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal();
    $scope.color = $scope.wallet.color;
    $scope.data = {};
    $scope.hasClick = platformInfo.hasClick;
    $scope.displayAmount = getDisplayAmount(tx.amountStr);
    $scope.displayUnit = getDisplayUnit(tx.amountStr);
    initActionList();
    checkPaypro();
  }

  function getDisplayAmount(amountStr) {
    return amountStr.split(' ')[0];
  }

  function getDisplayUnit(amountStr) {
    return amountStr.split(' ')[1];
  }

  function initActionList() {
    $scope.actionList = [];

    if (!$scope.isShared) return;

    var actionDescriptions = {
      created: gettextCatalog.getString('Proposal Created'),
      accept: gettextCatalog.getString('Accepted'),
      reject: gettextCatalog.getString('Rejected'),
      broadcasted: gettextCatalog.getString('Broadcasted'),
    };

    $scope.actionList.push({
      type: 'created',
      time: tx.createdOn,
      description: actionDescriptions['created'],
      by: tx.creatorName
    });

    lodash.each(tx.actions, function(action) {
      $scope.actionList.push({
        type: action.type,
        time: action.createdOn,
        description: actionDescriptions[action.type],
        by: action.copayerName
      });
    });
  };

  $scope.$on('accepted', function(event) {
    $scope.sign();
  });

  // ToDo: use tx.customData instead of tx.message
  if (tx.message === 'Glidera transaction' && isGlidera) {
    tx.isGlidera = true;
    if (tx.canBeRemoved) {
      tx.canBeRemoved = (Date.now() / 1000 - (tx.ts || tx.createdOn)) > GLIDERA_LOCK_TIME;
    }
  }

  var setSendError = function(msg) {
    $scope.sendStatus = '';
    var error = msg || gettextCatalog.getString('Could not send payment');
    popupService.showAlert(gettextCatalog.getString('Error'), error);
  }

  $scope.sign = function(onSendStatusChange) {
    $scope.loading = true;
    walletService.publishAndSign($scope.wallet, $scope.tx, function(err, txp) {
      $scope.$emit('UpdateTx');
      if (err) return setSendError(err);
      success();
    }, onSendStatusChange);
  };

  function setError(err, prefix) {
    $scope.loading = false;
    popupService.showAlert(gettextCatalog.getString('Error'), bwcError.msg(err, prefix));
  };

  $scope.reject = function(txp) {
    var title = gettextCatalog.getString('Warning!');
    var msg = gettextCatalog.getString('Are you sure you want to reject this transaction?');
    popupService.showConfirm(title, msg, null, null, function(res) {
      if (res) {
        $scope.loading = true;

        walletService.reject($scope.wallet, $scope.tx, function(err, txpr) {
          if (err)
            return setError(err, gettextCatalog.getString('Could not reject payment'));

          $scope.close();
        });
      }
    });
  };

  $scope.remove = function() {
    $scope.loading = true;

    $timeout(function() {
      ongoingProcess.set('removeTx', true);
      walletService.removeTx($scope.wallet, $scope.tx, function(err) {
        ongoingProcess.set('removeTx', false);

        // Hacky: request tries to parse an empty response
        if (err && !(err.message && err.message.match(/Unexpected/))) {
          $scope.$emit('UpdateTx');
          return setError(err, gettextCatalog.getString('Could not delete payment proposal'));
        }

        $scope.close();
      });
    }, 10);
  };

  $scope.broadcast = function(txp) {
    $scope.loading = true;

    $timeout(function() {
      ongoingProcess.set('broadcastTx', true);
      walletService.broadcastTx($scope.wallet, $scope.tx, function(err, txpb) {
        ongoingProcess.set('broadcastTx', false);

        if (err) {
          return setError(err, gettextCatalog.getString('Could not broadcast payment'));
        }

        $scope.close();
      });
    }, 10);
  };

  $scope.getShortNetworkName = function() {
    return $scope.wallet.credentials.networkName.substring(0, 4);
  };

  function checkPaypro() {
    if (tx.payProUrl && !platformInfo.isChromeApp) {
      $scope.wallet.fetchPayPro({
        payProUrl: tx.payProUrl,
      }, function(err, paypro) {
        if (err) return;
        tx.paypro = paypro;
        paymentTimeControl(tx.paypro.expires);
        $timeout(function() {
          $ionicScrollDelegate.resize();
        }, 10);
      });
    }
  };

  function paymentTimeControl(expirationTime) {
    $scope.paymentExpired = false;
    setExpirationTime();

    countDown = $interval(function() {
      setExpirationTime();
    }, 1000);

    function setExpirationTime() {
      var now = Math.floor(Date.now() / 1000);
      if (now > expirationTime) {
        $scope.paymentExpired = true;
        if (countDown) $interval.cancel(countDown);
        return;
      }
      var totalSecs = expirationTime - now;
      var m = Math.floor(totalSecs / 60);
      var s = totalSecs % 60;
      $scope.expires = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
    };
  };

  lodash.each(['TxProposalRejectedBy', 'TxProposalAcceptedBy', 'transactionProposalRemoved', 'TxProposalRemoved', 'NewOutgoingTx', 'UpdateTx'], function(eventName) {
    $rootScope.$on(eventName, function() {
      $scope.wallet.getTx($scope.tx.id, function(err, tx) {
        if (err) {
          if (err.message && err.message == 'TX_NOT_FOUND' &&
            (eventName == 'transactionProposalRemoved' || eventName == 'TxProposalRemoved')) {
            $scope.tx.removed = true;
            $scope.tx.canBeRemoved = false;
            $scope.tx.pendingForUs = false;
            $scope.$apply();
          }
          return;
        }

        var action = lodash.find(tx.actions, {
          copayerId: $scope.wallet.credentials.copayerId
        });

        $scope.tx = txFormatService.processTx(tx);

        if (!action && tx.status == 'pending')
          $scope.tx.pendingForUs = true;

        $scope.updateCopayerList();
        $scope.$apply();
      });
    });
  });

  $scope.updateCopayerList = function() {
    lodash.map($scope.copayers, function(cp) {
      lodash.each($scope.tx.actions, function(ac) {
        if (cp.id == ac.copayerId) {
          cp.action = ac.type;
        }
      });
    });
  };

  function statusChangeHandler(processName, showName, isOn) {
    $log.debug('statusChangeHandler: ', processName, showName, isOn);
    if (showName) {
      $scope.sendStatus = showName;
    }
  }

  function success() {
    $scope.sendStatus = 'success';
    $scope.$digest();
  }

  $scope.statusChangeHandler = statusChangeHandler;

  $scope.onConfirm = function() {
    $scope.sign(statusChangeHandler);
  };

  $scope.onSuccessConfirm = function() {
    $ionicHistory.nextViewOptions({
      disableAnimate: true
    });
    $scope.close();
  };

  $scope.close = function() {
    $scope.loading = null;
    $scope.txpDetailsModal.hide();
  };
});
<commit_msg>Updates info when a copayer accept/reject<commit_after>'use strict';

angular.module('copayApp.controllers').controller('txpDetailsController', function($scope, $rootScope, $timeout, $interval, $log, ongoingProcess, platformInfo, $ionicScrollDelegate, txFormatService, bwcError, gettextCatalog, lodash, walletService, popupService, $ionicHistory) {
  var isGlidera = $scope.isGlidera;
  var GLIDERA_LOCK_TIME = 6 * 60 * 60;
  var now = Math.floor(Date.now() / 1000);
  var countDown;
  var listeners = [];

  $scope.init = function() {
    $scope.loading = null;
    $scope.isCordova = platformInfo.isCordova;
    $scope.copayerId = $scope.wallet.credentials.copayerId;
    $scope.isShared = $scope.wallet.credentials.n > 1;
    $scope.canSign = $scope.wallet.canSign() || $scope.wallet.isPrivKeyExternal();
    $scope.color = $scope.wallet.color;
    $scope.data = {};
    $scope.hasClick = platformInfo.hasClick;
    $scope.displayAmount = getDisplayAmount($scope.tx.amountStr);
    $scope.displayUnit = getDisplayUnit($scope.tx.amountStr);
    initActionList();
    checkPaypro();
  }

  function getDisplayAmount(amountStr) {
    return amountStr.split(' ')[0];
  }

  function getDisplayUnit(amountStr) {
    return amountStr.split(' ')[1];
  }

  function initActionList() {
    $scope.actionList = [];

    if (!$scope.isShared) return;

    var actionDescriptions = {
      created: gettextCatalog.getString('Proposal Created'),
      accept: gettextCatalog.getString('Accepted'),
      reject: gettextCatalog.getString('Rejected'),
      broadcasted: gettextCatalog.getString('Broadcasted'),
    };

    $scope.actionList.push({
      type: 'created',
      time: $scope.tx.createdOn,
      description: actionDescriptions['created'],
      by: $scope.tx.creatorName
    });

    lodash.each($scope.tx.actions, function(action) {
      $scope.actionList.push({
        type: action.type,
        time: action.createdOn,
        description: actionDescriptions[action.type],
        by: action.copayerName
      });
    });
  };

  function checkPaypro() {
    if ($scope.tx.payProUrl && !platformInfo.isChromeApp) {
      $scope.wallet.fetchPayPro({
        payProUrl: $scope.tx.payProUrl,
      }, function(err, paypro) {
        if (err) return;
        $scope.tx.paypro = paypro;
        paymentTimeControl($scope.tx.paypro.expires);
        $timeout(function() {
          $ionicScrollDelegate.resize();
        }, 10);
      });
    }
  };

  function paymentTimeControl(expirationTime) {
    $scope.paymentExpired = false;
    setExpirationTime();

    countDown = $interval(function() {
      setExpirationTime();
    }, 1000);

    function setExpirationTime() {
      var now = Math.floor(Date.now() / 1000);
      if (now > expirationTime) {
        $scope.paymentExpired = true;
        if (countDown) $interval.cancel(countDown);
        return;
      }
      var totalSecs = expirationTime - now;
      var m = Math.floor(totalSecs / 60);
      var s = totalSecs % 60;
      $scope.expires = ('0' + m).slice(-2) + ":" + ('0' + s).slice(-2);
    };
  };

  $scope.$on('accepted', function(event) {
    $scope.sign();
  });

  // ToDo: use tx.customData instead of tx.message
  if ($scope.tx.message === 'Glidera transaction' && isGlidera) {
    $scope.tx.isGlidera = true;
    if ($scope.tx.canBeRemoved) {
      $scope.tx.canBeRemoved = (Date.now() / 1000 - ($scope.tx.ts || $scope.tx.createdOn)) > GLIDERA_LOCK_TIME;
    }
  }

  var setError = function (err, prefix) {
    $scope.sendStatus = '';
    $scope.loading = false;
    popupService.showAlert(gettextCatalog.getString('Error'), bwcError.msg(err, prefix));
  };

  $scope.sign = function(onSendStatusChange) {
    $scope.loading = true;
    walletService.publishAndSign($scope.wallet, $scope.tx, function(err, txp) {
      $scope.$emit('UpdateTx');
      if (err) return setError(err, gettextCatalog.getString('Could not send payment'));
      success();
    }, onSendStatusChange);
  };

  $scope.reject = function(txp) {
    var title = gettextCatalog.getString('Warning!');
    var msg = gettextCatalog.getString('Are you sure you want to reject this transaction?');
    popupService.showConfirm(title, msg, null, null, function(res) {
      if (res) {
        $scope.loading = true;

        walletService.reject($scope.wallet, $scope.tx, function(err, txpr) {
          if (err)
            return setError(err, gettextCatalog.getString('Could not reject payment'));

          $scope.close();
        });
      }
    });
  };

  $scope.remove = function() {
    $scope.loading = true;

    $timeout(function() {
      ongoingProcess.set('removeTx', true);
      walletService.removeTx($scope.wallet, $scope.tx, function(err) {
        ongoingProcess.set('removeTx', false);

        // Hacky: request tries to parse an empty response
        if (err && !(err.message && err.message.match(/Unexpected/))) {
          $scope.$emit('UpdateTx');
          return setError(err, gettextCatalog.getString('Could not delete payment proposal'));
        }

        $scope.close();
      });
    }, 10);
  };

  $scope.broadcast = function(txp) {
    $scope.loading = true;

    $timeout(function() {
      ongoingProcess.set('broadcastTx', true);
      walletService.broadcastTx($scope.wallet, $scope.tx, function(err, txpb) {
        ongoingProcess.set('broadcastTx', false);

        if (err) {
          return setError(err, gettextCatalog.getString('Could not broadcast payment'));
        }

        $scope.close();
      });
    }, 10);
  };

  $scope.getShortNetworkName = function() {
    return $scope.wallet.credentials.networkName.substring(0, 4);
  };

  var updateTxInfo = function(eventName) {
    $scope.wallet.getTx($scope.tx.id, function(err, tx) {
      if (err) {
        if (err.message && err.message == 'TX_NOT_FOUND' &&
            (eventName == 'transactionProposalRemoved' || eventName == 'TxProposalRemoved')) {
          $scope.tx.removed = true;
          $scope.tx.canBeRemoved = false;
          $scope.tx.pendingForUs = false;
          $scope.$apply();
        }
        return;
      }

      var action = lodash.find(tx.actions, {
        copayerId: $scope.wallet.credentials.copayerId
      });

      $scope.tx = txFormatService.processTx(tx);

      if (!action && tx.status == 'pending')
        $scope.tx.pendingForUs = true;

      $scope.updateCopayerList();
      initActionList();
      $scope.$apply();
    });
  };

  var bwsEvent =  $rootScope.$on('bwsEvent', function(e, walletId, type, n) {
    lodash.each([
        'TxProposalRejectedBy',
        'TxProposalAcceptedBy',
        'transactionProposalRemoved',
        'TxProposalRemoved',
        'NewOutgoingTx',
        'UpdateTx'
    ], function(eventName) {
      if (walletId == $scope.wallet.id && type == eventName) {
        updateTxInfo(eventName);
      }
    });
  });

  $scope.updateCopayerList = function() {
    lodash.map($scope.copayers, function(cp) {
      lodash.each($scope.tx.actions, function(ac) {
        if (cp.id == ac.copayerId) {
          cp.action = ac.type;
        }
      });
    });
  };

  function statusChangeHandler(processName, showName, isOn) {
    $log.debug('statusChangeHandler: ', processName, showName, isOn);
    if (showName) {
      $scope.sendStatus = showName;
    }
  }

  function success() {
    $scope.sendStatus = 'success';
    $scope.$digest();
  }

  $scope.statusChangeHandler = statusChangeHandler;

  $scope.onConfirm = function() {
    $scope.sign(statusChangeHandler);
  };

  $scope.onSuccessConfirm = function() {
    $ionicHistory.nextViewOptions({
      disableAnimate: true
    });
    $scope.close();
  };

  $scope.close = function() {
    bwsEvent();
    $scope.loading = null;
    $scope.txpDetailsModal.hide();
  };
});
<|endoftext|>


<commit_before>/**
 * Copyright (c) 2013-2015 Memba Sarl. All rights reserved.
 * Sources at https://github.com/Memba
 */

/* jshint browser: true, jquery: true */
/* globals define: false */

(function (f, define) {
    'use strict';
    define([
        './window.assert',
        './window.logger',
        './vendor/kendo/kendo.binder',
        './vendor/kendo/kendo.userevents',
        './vendor/kendo/kendo.draganddrop'
        // './vendor/kendo/kendo.multiselect' // required because of a test in kendo.binder.js
    ], f);
})(function () {

    'use strict';

    // Load MathJax 2.6 dynamically - see https://docs.mathjax.org/en/v2.6-latest/advanced/dynamic.html
    // SSee configuration options - see http://mathjax.readthedocs.org/en/latest/configuration.html
    // And combined configuration options - see http://mathjax.readthedocs.org/en/latest/config-files.html
    (function () {
        var TYPE = 'text/x-mathjax-config';
        var head = document.getElementsByTagName('head')[0];
        var scripts = head.getElementsByTagName('script');
        var found = false;
        for (var i = 0; i < scripts.length; i++) {
            if (scripts[i].type === TYPE) {
                found = true;
                break;
            }
        }
        if (!found) {
            var script = document.createElement('script');
            script.type = TYPE;
            // TODO OPTIMIZE: MathML cannot be used + hide contextual menu
            script[(window.opera ? 'innerHTML' : 'text')] =
                'MathJax.Hub.Config({\n' +
                '  asciimath2jax: { delimiters: [["§","§"], ["`","`"]] }\n' +
                '});';
            head.appendChild(script);
            script = document.createElement('script');
            script.type = 'text/javascript';
            // script.src  = 'http://cdn.mathjax.org/mathjax/2.6-latest/unpacked/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
            script.src = 'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML';
            head.appendChild(script);
        }
    })();

    (function ($, undefined) {

        var kendo = window.kendo;
        var Widget = kendo.ui.Widget;
        var assert = window.assert;
        var logger = new window.Logger('kidoju.widgets.mathexpression');
        var STRING = 'string';
        var NULL = 'null';
        var UNDEFINED = 'undefined';
        var CHANGE = 'change';
        // var NS = '.kendoDropZone';
        var WIDGET_CLASS = 'k-widget kj-mathexpression';

        /*********************************************************************************
         * Widget
         *********************************************************************************/

        /**
         * MathExpression
         * @class MathExpression Widget (kendoDropZone)
         */
        var MathExpression = Widget.extend({

            /**
             * Initializes the widget
             * @param element
             * @param options
             */
            init: function (element, options) {
                var that = this;
                options = options || {};
                Widget.fn.init.call(that, element, options);
                logger.debug('widget initialized');
                that.bind(CHANGE, $.proxy(that.refresh, that));
                that._layout();
                that.value(that.options.value);
                // kendo.notify(that);
            },

            /**
             * Widget options
             * @property options
             */
            options: {
                name: 'MathExpression',
                value: null
            },

            /**
             * Widget events
             * @property events
             */
            events: [
                CHANGE
            ],

            /**
             * Value for MVVM binding
             * @param value
             */
            value: function (value) {
                var that = this;
                if ($.type(value) === STRING || $.type(value) === NULL) {
                    if (that._value !== value) {
                        that._value = value;
                        that.trigger(CHANGE, { value: that._value });
                    }
                } else if ($.type(value) === UNDEFINED) {
                    return that._value;
                } else {
                    throw new TypeError('`value` is expected to be a string if not undefined');
                }
            },

            /**
             * Builds the widget layout
             * @private
             */
            _layout: function () {
                var that = this;
                that.wrapper = that.element;
                that.element.addClass(WIDGET_CLASS);
            },

            /**
             * Refresh the widget
             */
            refresh: function() {
                var element = this.element;
                element.text(this.value() || '');
                // If MathJax is not yet loaded it will parse the page anyway
                var MathJax = window.MathJax;
                if (MathJax) {
                    // See http://mathjax.readthedocs.org/en/latest/advanced/typeset.html
                    MathJax.Hub.Queue(['Typeset', MathJax.Hub, element[0]]);
                }
            },

            /**
             * Clears the widget
             * @method _clear
             * @private
             */
            _clear: function () {
                var that = this;
                // unbind kendo
                // kendo.unbind($(that.element));
                // unbind all other events
                $(that.element).find('*').off();
                $(that.element).off();
                // remove descendants
                $(that.element).empty();
                // remove element classes
                // $(that.element).removeClass(WIDGET_CLASS);
            },

            /**
             * Destroys the widget including all DOM modifications
             * @method destroy
             */
            destroy: function () {
                var that = this;
                Widget.fn.destroy.call(that);
                that._clear();
                kendo.destroy(that.element);
            }

        });

        kendo.ui.plugin(MathExpression);

    }(window.jQuery));

    return window.kendo;

}, typeof define === 'function' && define.amd ? define : function (_, f) { 'use strict'; f(); });
<commit_msg>Hid contextual menu<commit_after>/**
 * Copyright (c) 2013-2015 Memba Sarl. All rights reserved.
 * Sources at https://github.com/Memba
 */

/* jshint browser: true, jquery: true */
/* globals define: false */

(function (f, define) {
    'use strict';
    define([
        './window.assert',
        './window.logger',
        './vendor/kendo/kendo.binder',
        './vendor/kendo/kendo.userevents',
        './vendor/kendo/kendo.draganddrop'
        // './vendor/kendo/kendo.multiselect' // required because of a test in kendo.binder.js
    ], f);
})(function () {

    'use strict';

    // Load MathJax 2.6 dynamically - see https://docs.mathjax.org/en/v2.6-latest/advanced/dynamic.html
    // See configuration options - see http://mathjax.readthedocs.org/en/latest/configuration.html
    // And combined configuration options - see http://mathjax.readthedocs.org/en/latest/config-files.html
    (function () {
        var TYPE = 'text/x-mathjax-config';
        var head = document.getElementsByTagName('head')[0];
        var scripts = head.getElementsByTagName('script');
        var found = false;
        for (var i = 0; i < scripts.length; i++) {
            if (scripts[i].type === TYPE) {
                found = true;
                break;
            }
        }
        if (!found) {
            var script = document.createElement('script');
            script.type = TYPE;
            // TODO OPTIMIZE without MathML input
            script[(window.opera ? 'innerHTML' : 'text')] =
                'MathJax.Hub.Config({\n' +
                '  showMathMenu: false,\n' + // Hide contextual menu
                '  asciimath2jax: { delimiters: [["§","§"], ["`","`"]] }\n' +
                '});';
            head.appendChild(script);
            script = document.createElement('script');
            script.type = 'text/javascript';
            // script.src  = 'http://cdn.mathjax.org/mathjax/2.6-latest/unpacked/MathJax.js?config=TeX-AMS-MML_HTMLorMML';
            script.src = 'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML';
            head.appendChild(script);
        }
    })();

    (function ($, undefined) {

        var kendo = window.kendo;
        var Widget = kendo.ui.Widget;
        var assert = window.assert;
        var logger = new window.Logger('kidoju.widgets.mathexpression');
        var STRING = 'string';
        var NULL = 'null';
        var UNDEFINED = 'undefined';
        var CHANGE = 'change';
        // var NS = '.kendoDropZone';
        var WIDGET_CLASS = 'k-widget kj-mathexpression';

        /*********************************************************************************
         * Widget
         *********************************************************************************/

        /**
         * MathExpression
         * @class MathExpression Widget (kendoDropZone)
         */
        var MathExpression = Widget.extend({

            /**
             * Initializes the widget
             * @param element
             * @param options
             */
            init: function (element, options) {
                var that = this;
                options = options || {};
                Widget.fn.init.call(that, element, options);
                logger.debug('widget initialized');
                that.bind(CHANGE, $.proxy(that.refresh, that));
                that._layout();
                that.value(that.options.value);
                // see http://www.telerik.com/forums/kendo-notify()
                kendo.notify(that);
            },

            /**
             * Widget options
             * @property options
             */
            options: {
                name: 'MathExpression',
                value: null
            },

            /**
             * Widget events
             * @property events
             */
            events: [
                CHANGE
            ],

            /**
             * Value for MVVM binding
             * @param value
             */
            value: function (value) {
                var that = this;
                if ($.type(value) === STRING || $.type(value) === NULL) {
                    if (that._value !== value) {
                        that._value = value;
                        that.trigger(CHANGE, { value: that._value });
                    }
                } else if ($.type(value) === UNDEFINED) {
                    return that._value;
                } else {
                    throw new TypeError('`value` is expected to be a string if not undefined');
                }
            },

            /**
             * Builds the widget layout
             * @private
             */
            _layout: function () {
                var that = this;
                that.wrapper = that.element;
                that.element.addClass(WIDGET_CLASS);
            },

            /**
             * Refresh the widget
             */
            refresh: function() {
                var element = this.element;
                element.text(this.value() || '');
                // If MathJax is not yet loaded it will parse the page anyway
                var MathJax = window.MathJax;
                if (MathJax) {
                    // See http://mathjax.readthedocs.org/en/latest/advanced/typeset.html
                    MathJax.Hub.Queue(['Typeset', MathJax.Hub, element[0]]);
                }
                logger.debug('widget refreshed');
            },

            /**
             * Clears the widget
             * @method _clear
             * @private
             */
            _clear: function () {
                var that = this;
                // unbind kendo
                // kendo.unbind($(that.element));
                // unbind all other events
                $(that.element).find('*').off();
                $(that.element).off();
                // remove descendants
                $(that.element).empty();
                // remove element classes
                // $(that.element).removeClass(WIDGET_CLASS);
            },

            /**
             * Destroys the widget including all DOM modifications
             * @method destroy
             */
            destroy: function () {
                var that = this;
                Widget.fn.destroy.call(that);
                that._clear();
                kendo.destroy(that.element);
            }

        });

        kendo.ui.plugin(MathExpression);

    }(window.jQuery));

    return window.kendo;

}, typeof define === 'function' && define.amd ? define : function (_, f) { 'use strict'; f(); });
<|endoftext|>


<commit_before>//@ts-check
import { isFirefox } from './tts/utils';

const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);

export class TextSelectionPlugin {

  constructor(avoidTspans = isFirefox()) {
    /**@type {PromiseLike<JQuery<HTMLElement>>} */
    this.djvuPagesPromise = null;
    // Using text elements insted of tspans for words because Firefox does not allow svg tspan strech.
    // Tspans are necessary on Chrome because they prevent newline character after every word when copying
    this.svgParagraphElement = "text";
    this.svgWordElement = "tspan";
    this.insertNewlines = avoidTspans
    if(avoidTspans) {
      this.svgParagraphElement = "g";
      this.svgWordElement = "text";
    }
  }

  /**
   * @param {string} ocaid
   */
  init(ocaid) {
    this.djvuPagesPromise = $.ajax({
      type: "GET",
      url: `https://cors.archive.org/cors/${ocaid}/${ocaid}_djvu.xml`,
      dataType: "xml",

      error: function (e) {
        return undefined;
      }
    }).then(xmlMap  => xmlMap && $(xmlMap).find("OBJECT"));
  }

  /**
   * @param {number} index
   * @returns {Promise<HTMLElement>}
   */
  async getPageText(index) {
    return (await this.djvuPagesPromise)[index];
  }

  /**
   * Intercept copied text to remove any styling applied to it
   * @param {JQuery} $container
   */
  interceptCopy($container) {
    $container.each((i, pageContainer) => {
      pageContainer.addEventListener('copy', (event) => {
        const selection = document.getSelection();
        event.clipboardData.setData('text/plain', selection.toString());
        event.preventDefault();
      });
    });
  }

  /**
   * Deselect all text after clicking elswhere in the BRcontainer
   * @param {JQuery} $container
   */
  deselectOnClick($container) {
    $container.one("mousedown", (event) => {
      if (window.getSelection) window.getSelection().removeAllRanges();
    })
  }

  /**
   * Stops page flipping on short click, allows text selction on longer mousedown
   * @param {JQuery} $container
   */
  stopPageFlip($container) {
    const $svg = $container.find('svg');
    let longPressTimer;
    this.interceptCopy($container);

    $svg.on("mousedown", (event) => {
      if ($(event.target).is(this.svgWordElement)) {
        event.stopPropagation();
        longPressTimer = window.setTimeout(() => {
          $svg.one("mouseup", (event) => {
            event.stopPropagation();
            clearTimeout(longPressTimer);
            this.deselectOnClick($container);
          });
        }, 250);
      }
    });
  }

  /**
   * @param {number} pageIndex
   * @param {JQuery} $container
   */
  async createTextLayer(pageIndex, $container) {
    const $svgLayers = $container.find('.textSelectionSVG');
    if ($svgLayers.length) return;
    const XMLpage = await this.getPageText(pageIndex);
    const XMLwidth = $(XMLpage).attr("width");
    const XMLheight = $(XMLpage).attr("height");

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svg.setAttribute("viewBox", `0 0 ${XMLwidth} ${XMLheight}`);
    $container.append(svg);
    svg.setAttribute('class', 'textSelectionSVG');
    svg.setAttribute('preserveAspectRatio', 'none');
    $(svg).css({
      "width": "100%",
      "position": "absolute",
      "height": "100%",
      "top": "0",
      "left": "0",
    });

    $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
      // Adding text element for each paragraph in the page
      const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
      paragSvg.setAttribute("class", "BRparagElement");
      const words = $(paragraph).find("WORD");
      const wordHeightArr = [];

      for(let i = 0; i < words.length; i++) {
        // Adding tspan for each word in paragraph
        const currWord = words[i];
        // eslint-disable-next-line no-unused-vars
        const [left, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
        const wordHeight = bottom - top;
        wordHeightArr.push(wordHeight);

        const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
        wordTspan.setAttribute("class", "BRwordElement");
        wordTspan.setAttribute("x", left.toString());
        wordTspan.setAttribute("y", bottom.toString());
        wordTspan.setAttribute("textLength", (right - left).toString());
        wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
        wordTspan.textContent = currWord.textContent;
        paragSvg.appendChild(wordTspan);

        // Adding spaces after words except at the end of the paragraph
        // TODO: assumes left-to-right text
        if(i < words.length - 1){
          const nextWord = words[i + 1];
          // eslint-disable-next-line no-unused-vars
          const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
          const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
          spaceTspan.setAttribute("class", "BRwordElement");
          spaceTspan.setAttribute("x", right.toString());
          spaceTspan.setAttribute("y", bottom.toString());
          if((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
          spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
          spaceTspan.textContent = " ";
          paragSvg.appendChild(spaceTspan);
        }

        // Adds newline at the end of paragraph on Firefox
        if((i ==  words.length - 1 && (this.insertNewlines))) {
          paragSvg.appendChild(document.createTextNode("\n"));
        }
      }

      wordHeightArr.sort();
      const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
      paragSvg.setAttribute("font-size", paragWordHeight.toString());
      svg.appendChild(paragSvg);
    })
    this.stopPageFlip($container);
  }
}

export class BookreaderWithTextSelection extends BookReader {
  init() {
    if(this.enableTextSelection){
      this.textSelectionPlugin = new TextSelectionPlugin();
      this.textSelectionPlugin.init(this.bookId);
    }
    super.init();
  }

  /**
   * @param {PageModel} page
   */
  _createPageContainer(page, styles = {}) {
    const $container = super._createPageContainer(page, styles);
    if(this.enableTextSelection){
      this.textSelectionPlugin.createTextLayer(page.index, $container);
    }
    return $container;
  }
}
window.BookReader = BookreaderWithTextSelection;
export default BookreaderWithTextSelection;
<commit_msg>Fix testcafe failing<commit_after>//@ts-check
import { isFirefox } from './tts/utils';

const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);

export class TextSelectionPlugin {

  constructor(avoidTspans = isFirefox()) {
    /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
    this.djvuPagesPromise = null;
    // Using text elements insted of tspans for words because Firefox does not allow svg tspan strech.
    // Tspans are necessary on Chrome because they prevent newline character after every word when copying
    this.svgParagraphElement = "text";
    this.svgWordElement = "tspan";
    this.insertNewlines = avoidTspans
    if(avoidTspans) {
      this.svgParagraphElement = "g";
      this.svgWordElement = "text";
    }
  }

  /**
   * @param {string} ocaid
   */
  init(ocaid) {
    this.djvuPagesPromise = $.ajax({
      type: "GET",
      url: `https://cors.archive.org/cors/${ocaid}/${ocaid}_djvu.xml`,
      dataType: "xml",

      error: function (e) {
        return undefined;
      }
    }).then(xmlMap  => xmlMap && $(xmlMap).find("OBJECT"));
  }

  /**
   * @param {number} index
   * @returns {Promise<HTMLElement|undefined>}
   */
  async getPageText(index) {
    const XMLpagesArr = await this.djvuPagesPromise;
    if (XMLpagesArr) return XMLpagesArr[index];
  }

  /**
   * Intercept copied text to remove any styling applied to it
   * @param {JQuery} $container
   */
  interceptCopy($container) {
    $container.each((i, pageContainer) => {
      pageContainer.addEventListener('copy', (event) => {
        const selection = document.getSelection();
        event.clipboardData.setData('text/plain', selection.toString());
        event.preventDefault();
      });
    });
  }

  /**
   * Deselect all text after clicking elswhere in the BRcontainer
   * @param {JQuery} $container
   */
  deselectOnClick($container) {
    $container.one("mousedown", (event) => {
      if (window.getSelection) window.getSelection().removeAllRanges();
    })
  }

  /**
   * Stops page flipping on short click, allows text selction on longer mousedown
   * @param {JQuery} $container
   */
  stopPageFlip($container) {
    const $svg = $container.find('svg');
    let longPressTimer;
    this.interceptCopy($container);

    $svg.on("mousedown", (event) => {
      if ($(event.target).is(this.svgWordElement)) {
        event.stopPropagation();
        longPressTimer = window.setTimeout(() => {
          $svg.one("mouseup", (event) => {
            event.stopPropagation();
            clearTimeout(longPressTimer);
            this.deselectOnClick($container);
          });
        }, 250);
      }
    });
  }

  /**
   * @param {number} pageIndex
   * @param {JQuery} $container
   */
  async createTextLayer(pageIndex, $container) {
    const $svgLayers = $container.find('.textSelectionSVG');
    if ($svgLayers.length) return;
    const XMLpage = await this.getPageText(pageIndex);
    if(!XMLpage) return;
    const XMLwidth = $(XMLpage).attr("width");
    const XMLheight = $(XMLpage).attr("height");

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    svg.setAttribute("viewBox", `0 0 ${XMLwidth} ${XMLheight}`);
    $container.append(svg);
    svg.setAttribute('class', 'textSelectionSVG');
    svg.setAttribute('preserveAspectRatio', 'none');
    $(svg).css({
      "width": "100%",
      "position": "absolute",
      "height": "100%",
      "top": "0",
      "left": "0",
    });

    $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
      // Adding text element for each paragraph in the page
      const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
      paragSvg.setAttribute("class", "BRparagElement");
      const words = $(paragraph).find("WORD");
      const wordHeightArr = [];

      for(let i = 0; i < words.length; i++) {
        // Adding tspan for each word in paragraph
        const currWord = words[i];
        // eslint-disable-next-line no-unused-vars
        const [left, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
        const wordHeight = bottom - top;
        wordHeightArr.push(wordHeight);

        const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
        wordTspan.setAttribute("class", "BRwordElement");
        wordTspan.setAttribute("x", left.toString());
        wordTspan.setAttribute("y", bottom.toString());
        wordTspan.setAttribute("textLength", (right - left).toString());
        wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
        wordTspan.textContent = currWord.textContent;
        paragSvg.appendChild(wordTspan);

        // Adding spaces after words except at the end of the paragraph
        // TODO: assumes left-to-right text
        if(i < words.length - 1){
          const nextWord = words[i + 1];
          // eslint-disable-next-line no-unused-vars
          const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
          const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
          spaceTspan.setAttribute("class", "BRwordElement");
          spaceTspan.setAttribute("x", right.toString());
          spaceTspan.setAttribute("y", bottom.toString());
          if((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
          spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
          spaceTspan.textContent = " ";
          paragSvg.appendChild(spaceTspan);
        }

        // Adds newline at the end of paragraph on Firefox
        if((i ==  words.length - 1 && (this.insertNewlines))) {
          paragSvg.appendChild(document.createTextNode("\n"));
        }
      }

      wordHeightArr.sort();
      const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
      paragSvg.setAttribute("font-size", paragWordHeight.toString());
      svg.appendChild(paragSvg);
    })
    this.stopPageFlip($container);
  }
}

export class BookreaderWithTextSelection extends BookReader {
  init() {
    if(this.enableTextSelection){
      this.textSelectionPlugin = new TextSelectionPlugin();
      this.textSelectionPlugin.init(this.bookId);
    }
    super.init();
  }

  /**
   * @param {PageModel} page
   */
  _createPageContainer(page, styles = {}) {
    const $container = super._createPageContainer(page, styles);
    if(this.enableTextSelection){
      this.textSelectionPlugin.createTextLayer(page.index, $container);
    }
    return $container;
  }
}
window.BookReader = BookreaderWithTextSelection;
export default BookreaderWithTextSelection;
<|endoftext|>