From a73359094a02fdc29b6c048fa02cc90395bf3de0 Mon Sep 17 00:00:00 2001 From: sienori Date: Sat, 23 Feb 2019 00:21:55 +0900 Subject: [PATCH] Replace popup with React --- src/common/openUrl.js | 6 + src/common/translate.js | 9 ++ src/popup/components/Footer.js | 50 ++++++ src/popup/components/Header.js | 42 ++++++ src/popup/components/InputArea.js | 33 ++++ src/popup/components/PopupPage.js | 123 +++++++++++++++ src/popup/components/ResultArea.js | 38 +++++ src/popup/icons/heart.svg | 6 + src/popup/icons/settings.svg | 20 +++ src/popup/index.html | 75 +-------- src/popup/index.js | 234 +---------------------------- src/popup/popup.css | 227 ---------------------------- src/popup/styles/Footer.scss | 57 +++++++ src/popup/styles/Header.scss | 61 ++++++++ src/popup/styles/InputArea.scss | 26 ++++ src/popup/styles/PopupPage.scss | 38 +++++ src/popup/styles/ResultArea.scss | 25 +++ 17 files changed, 541 insertions(+), 529 deletions(-) create mode 100644 src/common/openUrl.js create mode 100644 src/popup/components/Footer.js create mode 100644 src/popup/components/Header.js create mode 100644 src/popup/components/InputArea.js create mode 100644 src/popup/components/PopupPage.js create mode 100644 src/popup/components/ResultArea.js create mode 100644 src/popup/icons/heart.svg create mode 100644 src/popup/icons/settings.svg delete mode 100644 src/popup/popup.css create mode 100644 src/popup/styles/Footer.scss create mode 100644 src/popup/styles/Header.scss create mode 100644 src/popup/styles/InputArea.scss create mode 100644 src/popup/styles/PopupPage.scss create mode 100644 src/popup/styles/ResultArea.scss diff --git a/src/common/openUrl.js b/src/common/openUrl.js new file mode 100644 index 0000000..04623ef --- /dev/null +++ b/src/common/openUrl.js @@ -0,0 +1,6 @@ +import browser from "webextension-polyfill"; + +export default async url => { + const activeTab = (await browser.tabs.query({ currentWindow: true, active: true }))[0]; + browser.tabs.create({ url: url, index: activeTab.index + 1 }); +}; diff --git a/src/common/translate.js b/src/common/translate.js index 6025ee0..4177e4d 100644 --- a/src/common/translate.js +++ b/src/common/translate.js @@ -65,6 +65,15 @@ const formatResult = result => { export default async (sourceWord, sourceLang = "auto", targetLang) => { sourceWord = sourceWord.trim(); + if (sourceWord === "") + return { + resultText: "", + candidateText: "", + sourceLanguage: "en", + percentage: 0, + statusText: "OK" + }; + const history = getHistory(sourceWord, sourceLang, targetLang); if (history) return history.result; diff --git a/src/popup/components/Footer.js b/src/popup/components/Footer.js new file mode 100644 index 0000000..0b67a10 --- /dev/null +++ b/src/popup/components/Footer.js @@ -0,0 +1,50 @@ +import React, { Component } from "react"; +import browser from "webextension-polyfill"; +import genelateLangOptions from "src/common/genelateLangOptions"; +import openUrl from "src/common/openUrl"; +import "../styles/Footer.scss"; + +export default class Footer extends Component { + constructor(props) { + super(props); + this.langList = genelateLangOptions(); + } + + handleLinkClick = async () => { + const { tabUrl, targetLang } = this.props; + const encodedUrl = encodeURIComponent(tabUrl); + const translateUrl = `https://translate.google.com/translate?hl=${targetLang}&sl=auto&u=${encodedUrl}`; + openUrl(translateUrl); + }; + + handleChange = e => { + const lang = e.target.value; + this.props.handleLangChange(lang); + }; + + render() { + const { tabUrl, targetLang } = this.props; + + return ( + + ); + } +} diff --git a/src/popup/components/Header.js b/src/popup/components/Header.js new file mode 100644 index 0000000..53ca3cd --- /dev/null +++ b/src/popup/components/Header.js @@ -0,0 +1,42 @@ +import React from "react"; +import browser from "webextension-polyfill"; +import browserInfo from "browser-info"; +import openUrl from "src/common/openUrl"; +import HeartIcon from "../icons/heart.svg"; +import SettingsIcon from "../icons/settings.svg"; +import "../styles/header.scss"; + +//TODO: 次のタブで開く +const openPayPal = () => { + const isChrome = browserInfo().name === "Chrome"; + const url = `https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&no_shipping=1&business=sienori.firefox@gmail.com&item_name=SimpleTranslate ${ + isChrome ? "for Chrome " : "" + }- Donation`; + openUrl(url); +}; +const openSettings = () => { + const url = "../options/index.html#settings"; + openUrl(url); +}; + +export default () => ( + +); diff --git a/src/popup/components/InputArea.js b/src/popup/components/InputArea.js new file mode 100644 index 0000000..426d654 --- /dev/null +++ b/src/popup/components/InputArea.js @@ -0,0 +1,33 @@ +import React, { Component } from "react"; +import ReactDOM from "react-dom"; +import browser from "webextension-polyfill"; +import "../styles/inputArea.scss"; + +export default class InputArea extends Component { + resizeTextArea = () => { + const textarea = ReactDOM.findDOMNode(this.refs.textarea); + textarea.style.height = "1px"; + textarea.style.height = `${textarea.scrollHeight + 2}px`; + }; + + handleInputText = e => { + const inputText = e.target.value; + this.props.handleInputText(inputText); + this.resizeTextArea(); + }; + + render() { + return ( +
+ -
-
-

-

-
-
- - - - - +
\ No newline at end of file diff --git a/src/popup/index.js b/src/popup/index.js index 3708e59..fe37619 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -1,231 +1,5 @@ -/* Copyright (c) 2017-2018 Sienori All rights reserved. - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import React from "react"; +import ReactDOM from "react-dom"; +import PopupPage from "./components/PopupPage"; -const S = new settingsObj(); -const T = new Translate(); - -//設定を読み出し -S.init().then(function(value) { - defaultTargetLang = value.targetLang; - secondTargetLang = value.secondTargetLang; - langList.value = value.targetLang; //リスト初期値をセット - langList.addEventListener("change", changeLang); - - document.body.style.fontSize = value.fontSize; -}); - -let target = document.getElementById("target"); -let langList = document.getElementById("langList"); -let textarea = document.getElementById("textarea"); - -const initialText = browser.i18n.getMessage("initialTextArea"); -textarea.placeholder = initialText; - -let secondTargetLang; -let defaultTargetLang; -let sourceWord = ""; - -setLangList(); - -function setLangList() { - let langListStr = browser.i18n.getMessage("langList"); - langListStr = langListStr.split(", "); - - for (let i in langListStr) { - langListStr[i] = langListStr[i].split(":"); - } - langListStr = langListStr.sort(alphabeticallySort); - - let langListHtml = ""; - for (let i of langListStr) { - langListHtml += ``; - } - - langList.innerHTML = langListHtml; -} - -setTitles(); -function setTitles() { - document.getElementById("donate").title = browser.i18n.getMessage("donateWithPaypalLabel"); - document.getElementById("setting").title = browser.i18n.getMessage("settingsLabel"); - document.getElementById("langList").title = browser.i18n.getMessage("targetLangLabel"); -} - -function alphabeticallySort(a, b) { - if (a[1].toString() > b[1].toString()) { - return 1; - } else { - return -1; - } -} - -//翻訳先言語変更時に更新 -async function changeLang() { - if (typeof url != "undefined") showLink(); - - if (sourceWord !== "") { - const resultData = await T.translate(sourceWord, undefined, langList.value); - showResult(resultData.resultText, resultData.candidateText, resultData.statusText); - } -} - -//アクティブなタブを取得して渡す -browser.tabs - .query({ - currentWindow: true, - active: true - }) - .then(function(tabs) { - getSelectionWord(tabs); - }); - -//アクティブタブから選択文字列とurlを取得 -function getSelectionWord(tabs) { - for (let tab of tabs) { - browser.tabs - .sendMessage(tab.id, { - message: "fromPopup" - }) - .then(response => { - sourceWord = response.word || ""; - url = response.url; - refleshSource(); - showLink(); - }) - .catch(() => {}); - } -} - -//ページ翻訳へのリンクを表示 -function showLink() { - document.getElementById("link").innerHTML = - "" + - browser.i18n.getMessage("showLink") + - ""; -} - -//翻訳元テキストを表示 -function refleshSource() { - if (sourceWord !== "") { - textarea.innerHTML = sourceWord; - resize(); - inputText(); - } -} - -textarea.addEventListener("paste", () => { - resize(); - inputText(); -}); - -textarea.addEventListener("keydown", resize); - -textarea.addEventListener("keyup", function(event) { - if (sourceWord == textarea.value) return; - - resize(); - inputText(); -}); - -//テキストボックスをリサイズ -function resize() { - setTimeout(function() { - textarea.style.height = "0px"; - textarea.style.height = parseInt(textarea.scrollHeight) + "px"; - }, 0); -} - -textarea.addEventListener("click", textAreaClick, { - once: true -}); -//テキストエリアクリック時の処理 -function textAreaClick() { - textarea.select(); -} - -let inputTimer; -//文字入力時の処理 -function inputText() { - sourceWord = textarea.value; - const waitTime = S.get().waitTime; - - clearTimeout(inputTimer); - inputTimer = setTimeout(() => { - runTranslation(); - }, waitTime); -} - -async function runTranslation() { - if (sourceWord == "") { - showResult("", ""); - return; - } - - const resultData = await T.translate(sourceWord, "auto", langList.value); - changeSecondLang(defaultTargetLang, resultData.sourceLanguage, resultData.percentage); - showResult(resultData.resultText, resultData.candidateText, resultData.statusText); -} - -function showResult(resultText, candidateText, statusText = "OK") { - const resultArea = target.getElementsByClassName("result")[0]; - const candidateArea = target.getElementsByClassName("candidate")[0]; - - resultArea.innerText = resultText; - if (S.get().ifShowCandidate) candidateArea.innerText = candidateText; - - if (statusText != "OK") showError(statusText); -} - -function showError(statusText) { - let errorMessage = ""; - switch (statusText) { - case "": - errorMessage = browser.i18n.getMessage("networkError"); - break; - case "Service Unavailable": - errorMessage = browser.i18n.getMessage("unavailableError"); - break; - default: - errorMessage = `${browser.i18n.getMessage("unknownError")} [${statusText}]`; - break; - } - const candidateArea = target.getElementsByClassName("candidate")[0]; - candidateArea.innerText = errorMessage; -} - -let changeLangFlag = false; - -function changeSecondLang(defaultTargetLang, sourceLang, percentage) { - if (!S.get().ifChangeSecondLang) return; - //検出された翻訳元言語がターゲット言語と一致 - const equalsSourceAndTarget = sourceLang == langList.value && percentage > 0; - - //検出された翻訳元言語がデフォルト言語と一致 - const equalsSourceAndDefault = sourceLang == defaultTargetLang && percentage > 0; - - if (!changeLangFlag) { - //通常時 - if (equalsSourceAndTarget && equalsSourceAndDefault) { - //ソースとターゲットとデフォルトが一致する場合 - //ターゲットを第2言語に変更 - changeLangFlag = true; - langList.value = secondTargetLang; - changeLang(); - } - } else { - //第2言語に切替した後 - if (!equalsSourceAndDefault) { - //ソースとデフォルトが異なる場合 - //ターゲットをデフォルトに戻す - changeLangFlag = false; - langList.value = defaultTargetLang; - changeLang(); - } - } -} +ReactDOM.render(, document.getElementById("root")); diff --git a/src/popup/popup.css b/src/popup/popup.css deleted file mode 100644 index b9b5119..0000000 --- a/src/popup/popup.css +++ /dev/null @@ -1,227 +0,0 @@ -:root { - --main-text: #0c0c0d; - --sub-text: #737373; - --line: #ededf0; - --button: #d7d7db; - --highlight: #5595ff; - --main-bg: #ffffff; - --confirm: #ff4f4f; -} - -body { - font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica", - sans-serif; - text-align: left; - font-size: 13px; - width: 348px; - overflow: hidden; - background-color: var(--main-bg); - - padding: 0px; - margin: 0px; - - display: flex; - flex-direction: column; -} - -.hidden { - display: none; -} - -svg { - pointer-events: none; -} - -#header { - padding: 10px; - background-color: var(--line); - - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - - -moz-user-select: none; -} - -#title { - font-size: 15px; - font-weight: 400; - color: #666; - cursor: default; -} - -#header .rightButtons { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; -} - -#donate { - display: flex; - align-items: center; - cursor: pointer; - margin-right: 8px; -} - -#donate svg { - height: 18px; - width: 18px; - fill: var(--sub-text); - transition: all 100ms; -} - -#donate:hover svg { - fill: var(--confirm); -} - -#setting { - display: flex; - align-items: center; - cursor: pointer; -} - -#setting svg { - flex-shrink: 0; - height: 18px; - width: 18px; - fill: var(--sub-text); - transform: rotate(180deg); - transition: fill 100ms, transform 300ms ease; -} - -#setting:hover svg { - fill: var(--highlight); - transform: rotate(270deg); -} - -#main { - padding: 10px; -} - -textarea { - font: inherit; - resize: none; - overflow: auto; - background-color: var(--main-bg); - - max-height: 215px; - height: 37px; - - /* 100% - padding*2 - border*2 */ - width: calc(100% - 22px); - - padding: 10px; - border: solid 1px var(--button); - border-radius: 2px; - transition: border-color 100ms ease-out; -} - -textarea:hover, -textarea:focus { - border-color: var(--highlight); -} - -hr { - border: none; - border-top: solid 1px var(--button); - height: 1px; - margin: 10px 0px; -} - -#target { - max-height: 215px; - min-height: 30px; - overflow-y: auto; - word-wrap: break-word; - padding: 0px 5px 0px; - background-color: var(--main-bg); -} - -#target p { - margin: 0; - background-color: var(--main-bg); -} - -#target .result { - background-color: var(--main-bg); -} - -#target .candidate { - color: var(--sub-text); - margin-top: 1em; - background-color: var(--main-bg); -} - -#target .candidate:empty { - margin-top: 0; -} - -#footer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 0px 10px 10px; -} - -#link { - flex-shrink: 0; -} - -#link a { - font-style: normal; - text-decoration: none; - color: var(--highlight); -} - -#link a:hover { - text-decoration: underline; -} - -select { - -moz-appearance: none; - text-overflow: ellipsis; - border: var(--button) solid 1px; - border-radius: 2px; - padding: 3px 5px; - padding-right: 20px; - width: 100%; - transition: border-color 100ms ease-out; -} - -select:hover { - border: var(--highlight) solid 1px; -} - -.selectWrap { - position: relative; - margin-left: 5px; -} - -.selectWrap:before { - pointer-events: none; - content: ""; - z-index: 1; - position: absolute; - top: 40%; - right: 7px; - width: 5px; - height: 5px; - - transform: rotate(45deg); - border-bottom: 2px solid var(--sub-text); - border-right: 2px solid var(--sub-text); - - transition: border-color 100ms ease-out; -} - -.selectWrap:hover::before { - border-bottom: 2px solid var(--highlight); - border-right: 2px solid var(--highlight); -} - -::-moz-selection { - background: var(--line); -} diff --git a/src/popup/styles/Footer.scss b/src/popup/styles/Footer.scss new file mode 100644 index 0000000..52df177 --- /dev/null +++ b/src/popup/styles/Footer.scss @@ -0,0 +1,57 @@ +#footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0px 10px 10px; + + .translateLink { + flex-shrink: 0; + a { + font-style: normal; + text-decoration: none; + color: var(--highlight); + cursor: pointer; + &:hover { + text-decoration: underline; + } + } + } + + .selectWrap { + position: relative; + margin-left: 5px; + &:before { + pointer-events: none; + content: ""; + z-index: 1; + position: absolute; + top: 40%; + right: 7px; + width: 5px; + height: 5px; + + transform: rotate(45deg); + border-bottom: 2px solid var(--sub-text); + border-right: 2px solid var(--sub-text); + + transition: border-color 100ms ease-out; + } + &:hover::before { + border-bottom: 2px solid var(--highlight); + border-right: 2px solid var(--highlight); + } + + select { + -moz-appearance: none; + text-overflow: ellipsis; + background-color: var(--main-bg); + border: var(--button) solid 1px; + border-radius: 2px; + padding: 3px 5px; + padding-right: 20px; + width: 100%; + transition: border-color 100ms ease-out; + } + } +} diff --git a/src/popup/styles/Header.scss b/src/popup/styles/Header.scss new file mode 100644 index 0000000..d5f133a --- /dev/null +++ b/src/popup/styles/Header.scss @@ -0,0 +1,61 @@ +#header { + padding: 10px; + background-color: var(--line); + display: flex; + flex-direction: row; + flex-shrink: 0; + align-items: center; + justify-content: space-between; + -moz-user-select: none; + -webkit-user-select: none; + + .title { + font-size: 15px; + font-weight: 400; + color: #666; + cursor: default; + flex-shrink: 0; + } + + .rightButtons { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + height: 18px; + + button { + display: block; + background-color: transparent; + border: none; + cursor: pointer; + outline: none; + padding: 0; + } + + .heartButton { + display: flex; + align-items: center; + margin-right: 10px; + &:hover svg { + fill: var(--confirm); + } + } + + .settingsButton { + display: flex; + align-items: center; + &:hover svg { + fill: var(--highlight); + transform: rotate(90deg); + } + } + + svg { + height: 18px; + width: 18px; + fill: var(--sub-text); + transition: fill 100ms, transform 300ms ease; + } + } +} diff --git a/src/popup/styles/InputArea.scss b/src/popup/styles/InputArea.scss new file mode 100644 index 0000000..3700b4a --- /dev/null +++ b/src/popup/styles/InputArea.scss @@ -0,0 +1,26 @@ +#inputArea { + margin: 10px; + textarea { + font: inherit; + resize: none; + overflow: auto; + background-color: var(--main-bg); + + box-sizing: border-box; + width: 100%; + height: 60px; + max-height: 240px; + min-height: 60px; + + margin: 0; + padding: 10px; + border: solid 1px var(--button); + border-radius: 2px; + transition: border-color 100ms ease-out; + } + + textarea:hover, + textarea:focus { + border-color: var(--highlight); + } +} diff --git a/src/popup/styles/PopupPage.scss b/src/popup/styles/PopupPage.scss new file mode 100644 index 0000000..bfbab9f --- /dev/null +++ b/src/popup/styles/PopupPage.scss @@ -0,0 +1,38 @@ +body { + margin: 0; + font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial", "Helvetica", + sans-serif; + font-size: 13px; + width: 348px; + overflow: hidden; + background-color: var(--main-bg); + + #root { + height: 100%; + } + + hr { + border: none; + border-top: solid 1px var(--button); + height: 1px; + margin: 0px 10px; + } + + ::-moz-selection { + background: var(--line); + } +} + +:root { + --main-text: #0c0c0d; + --sub-text: #737373; + --line: #ededf0; + --button: #d7d7db; + --highlight: #5595ff; + --main-bg: #ffffff; + --confirm: #ff4f4f; + --error: #d70022; + --warn: #ff8f00; + --success: #058b00; + --info: #0a84ff; +} diff --git a/src/popup/styles/ResultArea.scss b/src/popup/styles/ResultArea.scss new file mode 100644 index 0000000..8f51b59 --- /dev/null +++ b/src/popup/styles/ResultArea.scss @@ -0,0 +1,25 @@ +#resultArea { + max-height: 240px; + min-height: 30px; + overflow-y: auto; + word-wrap: break-word; + background-color: var(--main-bg); + margin: 10px; + + p { + margin: 0; + padding: 0px 5px; + background-color: var(--main-bg); + + &.resultText { + color: var(--main-text); + } + &.candidateText { + color: var(--sub-text); + margin-top: 1em; + &:empty { + margin-top: 0; + } + } + } +}