Replace content scripts with React
This commit is contained in:
parent
611658c275
commit
c23837e6e1
36
package-lock.json
generated
36
package-lock.json
generated
|
@ -1137,6 +1137,12 @@
|
||||||
"uri-js": "^4.2.1"
|
"uri-js": "^4.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ajv-errors": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz",
|
||||||
|
@ -5367,6 +5373,12 @@
|
||||||
"brorand": "^1.0.1"
|
"brorand": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"mime-db": {
|
"mime-db": {
|
||||||
"version": "1.36.0",
|
"version": "1.36.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
|
||||||
|
@ -9705,6 +9717,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"url-loader": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "^1.1.0",
|
||||||
|
"mime": "^2.0.3",
|
||||||
|
"schema-utils": "^1.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"schema-utils": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^6.1.0",
|
||||||
|
"ajv-errors": "^1.0.0",
|
||||||
|
"ajv-keywords": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"use": {
|
"use": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"sass-loader": "^7.0.3",
|
"sass-loader": "^7.0.3",
|
||||||
"style-loader": "^0.21.0",
|
"style-loader": "^0.21.0",
|
||||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||||
|
"url-loader": "^1.1.2",
|
||||||
"webextension-polyfill": "^0.3.1",
|
"webextension-polyfill": "^0.3.1",
|
||||||
"webpack": "^4.10.2",
|
"webpack": "^4.10.2",
|
||||||
"webpack-cli": "^3.0.1",
|
"webpack-cli": "^3.0.1",
|
||||||
|
|
|
@ -1,86 +1,75 @@
|
||||||
/* Copyright (c) 2017-2018 Sienori All rights reserved.
|
let translationHistory = [];
|
||||||
* 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/. */
|
|
||||||
|
|
||||||
class Translate {
|
const getHistory = (sourceWord, sourceLang, targetLang) => {
|
||||||
constructor() {
|
const history = translationHistory.find(
|
||||||
this.history = [];
|
history =>
|
||||||
}
|
history.sourceWord == sourceWord &&
|
||||||
|
history.sourceLang == sourceLang &&
|
||||||
|
history.targetLang == targetLang &&
|
||||||
|
history.result.statusText == "OK"
|
||||||
|
);
|
||||||
|
return history;
|
||||||
|
};
|
||||||
|
|
||||||
getHistory(sourceWord, sourceLang, targetLang) {
|
const setHistory = (sourceWord, sourceLang, targetLang, formattedResult) => {
|
||||||
const history = this.history.find(
|
translationHistory.push({
|
||||||
history =>
|
sourceWord: sourceWord,
|
||||||
history.sourceWord == sourceWord &&
|
sourceLang: sourceLang,
|
||||||
history.sourceLang == sourceLang &&
|
targetLang: targetLang,
|
||||||
history.targetLang == targetLang &&
|
result: formattedResult
|
||||||
history.result.statusText == "OK"
|
});
|
||||||
);
|
};
|
||||||
return history;
|
|
||||||
}
|
|
||||||
|
|
||||||
setHistory(sourceWord, sourceLang, targetLang, formattedResult) {
|
const sendRequest = (word, sourceLang, targetLang) => {
|
||||||
this.history.push({
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dt=bd&dj=1&q=${encodeURIComponent(
|
||||||
sourceWord: sourceWord,
|
word
|
||||||
sourceLang: sourceLang,
|
)}`;
|
||||||
targetLang: targetLang,
|
const xhr = new XMLHttpRequest();
|
||||||
result: formattedResult
|
xhr.responseType = "json";
|
||||||
});
|
xhr.open("GET", url);
|
||||||
}
|
xhr.send();
|
||||||
|
|
||||||
async translate(sourceWord, sourceLang = "auto", targetLang) {
|
return new Promise((resolve, reject) => {
|
||||||
sourceWord = sourceWord.trim();
|
xhr.onload = () => {
|
||||||
|
resolve(xhr);
|
||||||
const history = this.getHistory(sourceWord, sourceLang, targetLang);
|
|
||||||
if (history) return history.result;
|
|
||||||
|
|
||||||
const result = await this.sendRequest(sourceWord, sourceLang, targetLang);
|
|
||||||
const formattedResult = this.formatResult(result);
|
|
||||||
this.setHistory(sourceWord, sourceLang, targetLang, formattedResult);
|
|
||||||
|
|
||||||
return formattedResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendRequest(word, sourceLang, targetLang) {
|
|
||||||
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&dt=bd&dj=1&q=${encodeURIComponent(
|
|
||||||
word
|
|
||||||
)}`;
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.responseType = "json";
|
|
||||||
xhr.open("GET", url);
|
|
||||||
xhr.send();
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
xhr.onload = () => {
|
|
||||||
resolve(xhr);
|
|
||||||
};
|
|
||||||
xhr.onerror = () => {
|
|
||||||
resolve(xhr);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
formatResult(result) {
|
|
||||||
const resultData = {
|
|
||||||
resultText: "",
|
|
||||||
candidateText: "",
|
|
||||||
sourceLanguage: "",
|
|
||||||
percentage: 0,
|
|
||||||
statusText: ""
|
|
||||||
};
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
resolve(xhr);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
resultData.statusText = result.statusText;
|
const formatResult = result => {
|
||||||
if (resultData.statusText !== "OK") return resultData;
|
const resultData = {
|
||||||
|
resultText: "",
|
||||||
|
candidateText: "",
|
||||||
|
sourceLanguage: "",
|
||||||
|
percentage: 0,
|
||||||
|
statusText: ""
|
||||||
|
};
|
||||||
|
|
||||||
resultData.sourceLanguage = result.response.src;
|
resultData.statusText = result.statusText;
|
||||||
resultData.percentage = result.response.confidence;
|
if (resultData.statusText !== "OK") return resultData;
|
||||||
resultData.resultText = result.response.sentences.map(sentence => sentence.trans).join("");
|
|
||||||
if (result.response.dict) {
|
|
||||||
resultData.candidateText = result.response.dict
|
|
||||||
.map(dict => `${dict.pos}${dict.pos != "" ? ": " : ""}${dict.terms.join(", ")}\n`)
|
|
||||||
.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return resultData;
|
resultData.sourceLanguage = result.response.src;
|
||||||
|
resultData.percentage = result.response.confidence;
|
||||||
|
resultData.resultText = result.response.sentences.map(sentence => sentence.trans).join("");
|
||||||
|
if (result.response.dict) {
|
||||||
|
resultData.candidateText = result.response.dict
|
||||||
|
.map(dict => `${dict.pos}${dict.pos != "" ? ": " : ""}${dict.terms.join(", ")}\n`)
|
||||||
|
.join("");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return resultData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (sourceWord, sourceLang = "auto", targetLang) => {
|
||||||
|
sourceWord = sourceWord.trim();
|
||||||
|
const history = getHistory(sourceWord, sourceLang, targetLang);
|
||||||
|
if (history) return history.result;
|
||||||
|
|
||||||
|
const result = await sendRequest(sourceWord, sourceLang, targetLang);
|
||||||
|
const formattedResult = formatResult(result);
|
||||||
|
setHistory(sourceWord, sourceLang, targetLang, formattedResult);
|
||||||
|
return formattedResult;
|
||||||
|
};
|
||||||
|
|
37
src/content/components/TranslateButton.js
Normal file
37
src/content/components/TranslateButton.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React from "react";
|
||||||
|
import { getSettings } from "src/settings/settings";
|
||||||
|
import "../styles/TranslateButton.scss";
|
||||||
|
|
||||||
|
const calcPosition = () => {
|
||||||
|
const buttonSize = parseInt(getSettings("buttonSize"));
|
||||||
|
const offset = 10;
|
||||||
|
switch (getSettings("buttonPosition")) {
|
||||||
|
case "rightUp":
|
||||||
|
return { top: -buttonSize - offset, left: offset };
|
||||||
|
case "rightDown":
|
||||||
|
return { top: offset, left: offset };
|
||||||
|
case "leftUp":
|
||||||
|
return { top: -buttonSize - offset, left: -buttonSize - offset };
|
||||||
|
case "leftDown":
|
||||||
|
return { top: offset, left: -buttonSize - offset };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default props => {
|
||||||
|
const { position, shouldShow } = props;
|
||||||
|
const buttonSize = parseInt(getSettings("buttonSize"));
|
||||||
|
const { top, left } = calcPosition();
|
||||||
|
const buttonStyle = {
|
||||||
|
height: buttonSize,
|
||||||
|
width: buttonSize,
|
||||||
|
top: top + position.y,
|
||||||
|
left: left + position.x
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
style={buttonStyle}
|
||||||
|
className={`simple-translate-button ${shouldShow ? "isShow" : ""}`}
|
||||||
|
onClick={props.handleButtonClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
181
src/content/components/TranslateContainer.js
Normal file
181
src/content/components/TranslateContainer.js
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
|
import translate from "src/common/translate";
|
||||||
|
import { initSettings, getSettings, handleSettingsChange } from "src/settings/settings";
|
||||||
|
import TranslateButton from "./TranslateButton";
|
||||||
|
import TranslatePanel from "./TranslatePanel";
|
||||||
|
import "../styles/TranslateContainer.scss";
|
||||||
|
|
||||||
|
const getSelectedText = () => {
|
||||||
|
const element = document.activeElement;
|
||||||
|
const isInTextField = element.tagName === "INPUT" || element.tagName === "TEXTAREA";
|
||||||
|
const selectedText = isInTextField
|
||||||
|
? element.value.substring(element.selectionStart, element.selectionEnd)
|
||||||
|
: window.getSelection().toString();
|
||||||
|
return selectedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedPosition = () => {
|
||||||
|
const element = document.activeElement;
|
||||||
|
const isInTextField = element.tagName === "INPUT" || element.tagName === "TEXTAREA";
|
||||||
|
const selectedRect = isInTextField
|
||||||
|
? element.getBoundingClientRect()
|
||||||
|
: window
|
||||||
|
.getSelection()
|
||||||
|
.getRangeAt(0)
|
||||||
|
.getBoundingClientRect();
|
||||||
|
const selectedPosition = {
|
||||||
|
x: selectedRect.left + selectedRect.width / 2,
|
||||||
|
y: selectedRect.bottom
|
||||||
|
};
|
||||||
|
return selectedPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateText = async text => {
|
||||||
|
const targetLang = getSettings("targetLang");
|
||||||
|
const result = await translate(text, "auto", targetLang);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchesTargetLang = async selectedText => {
|
||||||
|
const targetLang = getSettings("targetLang");
|
||||||
|
//detectLanguageで判定
|
||||||
|
const langInfo = await browser.i18n.detectLanguage(selectedText);
|
||||||
|
const matchsLangsByDetect = langInfo.isReliable && langInfo.languages[0].language === targetLang;
|
||||||
|
if (matchsLangsByDetect) return true;
|
||||||
|
|
||||||
|
//先頭100字を翻訳にかけて判定
|
||||||
|
const partSelectedText = selectedText.substring(0, 100);
|
||||||
|
const result = await translateText(partSelectedText);
|
||||||
|
const matchsLangs = targetLang === result.sourceLanguage && result.percentage > 0;
|
||||||
|
return matchsLangs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class TranslateContainer extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isInit: false,
|
||||||
|
shouldShowButton: false,
|
||||||
|
buttonPosition: { x: 0, y: 0 },
|
||||||
|
shouldShowPanel: false,
|
||||||
|
panelPosition: { x: 0, y: 0 },
|
||||||
|
resultText: "",
|
||||||
|
candidateText: ""
|
||||||
|
};
|
||||||
|
this.selectedText = "";
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init = async () => {
|
||||||
|
await initSettings();
|
||||||
|
this.setState({ isInit: true });
|
||||||
|
document.addEventListener("mouseup", e => setTimeout(() => this.handleMouseUp(e), 0));
|
||||||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
browser.storage.onChanged.addListener(handleSettingsChange);
|
||||||
|
browser.runtime.onMessage.addListener(this.handleMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMessage = async request => {
|
||||||
|
switch (request.message) {
|
||||||
|
case "getTabInfo":
|
||||||
|
const tabInfo = { url: location.href, selectedText: this.selectedText };
|
||||||
|
return tabInfo;
|
||||||
|
case "translateSelectedText":
|
||||||
|
this.selectedText = getSelectedText();
|
||||||
|
const position = getSelectedPosition();
|
||||||
|
if (this.selectedText.length === 0) return;
|
||||||
|
this.hideButton();
|
||||||
|
this.showPanel(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleKeyDown = e => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
this.hideButton();
|
||||||
|
this.hidePanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
showButton = position => {
|
||||||
|
this.setState({ shouldShowButton: true, buttonPosition: position });
|
||||||
|
};
|
||||||
|
|
||||||
|
hideButton = () => {
|
||||||
|
this.setState({ shouldShowButton: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleButtonClick = e => {
|
||||||
|
const position = { x: e.clientX, y: e.clientY };
|
||||||
|
this.showPanel(position);
|
||||||
|
this.hideButton();
|
||||||
|
};
|
||||||
|
|
||||||
|
showPanel = async position => {
|
||||||
|
const result = await translateText(this.selectedText);
|
||||||
|
this.setState({
|
||||||
|
shouldShowPanel: true,
|
||||||
|
panelPosition: position,
|
||||||
|
resultText: result.resultText,
|
||||||
|
candidateText: getSettings("ifShowCandidate") ? result.candidateText : ""
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
hidePanel = () => {
|
||||||
|
this.setState({ shouldShowPanel: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseUp = e => {
|
||||||
|
const isLeftClick = e.button === 0;
|
||||||
|
const isInPasswordField = e.target.tagName === "INPUT" && e.target.type === "password";
|
||||||
|
const isInThisElement = document.querySelector("#simple-translate").contains(e.target);
|
||||||
|
if (!isLeftClick) return;
|
||||||
|
if (isInPasswordField) return;
|
||||||
|
if (isInThisElement) return;
|
||||||
|
this.hideButton();
|
||||||
|
this.hidePanel();
|
||||||
|
|
||||||
|
this.selectedText = getSelectedText();
|
||||||
|
const position = { x: e.clientX, y: e.clientY };
|
||||||
|
|
||||||
|
if (this.selectedText.length === 0) return;
|
||||||
|
this.handleTextSelect(position);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTextSelect = async position => {
|
||||||
|
const onSelectBehavior = getSettings("whenSelectText");
|
||||||
|
if (onSelectBehavior === "dontShowButton") return;
|
||||||
|
|
||||||
|
if (getSettings("ifCheckLang")) {
|
||||||
|
const matchesLang = await matchesTargetLang(this.selectedText);
|
||||||
|
if (matchesLang) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSelectBehavior === "showButton") {
|
||||||
|
this.showButton(position);
|
||||||
|
} else if (onSelectBehavior === "showPanel") {
|
||||||
|
this.showPanel(position);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
if (!this.state.isInit) return null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TranslateButton
|
||||||
|
shouldShow={this.state.shouldShowButton}
|
||||||
|
position={this.state.buttonPosition}
|
||||||
|
handleButtonClick={this.handleButtonClick}
|
||||||
|
/>
|
||||||
|
<TranslatePanel
|
||||||
|
shouldShow={this.state.shouldShowPanel}
|
||||||
|
position={this.state.panelPosition}
|
||||||
|
resultText={this.state.resultText}
|
||||||
|
candidateText={this.state.candidateText}
|
||||||
|
hidePanel={this.hidePanel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
116
src/content/components/TranslatePanel.js
Normal file
116
src/content/components/TranslatePanel.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { getSettings } from "src/settings/settings";
|
||||||
|
import "../styles/TranslatePanel.scss";
|
||||||
|
|
||||||
|
export default class TranslatePanel extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
panelPosition: { x: 0, y: 0 },
|
||||||
|
panelWidth: 0,
|
||||||
|
panelHeight: 0,
|
||||||
|
shouldResize: true
|
||||||
|
};
|
||||||
|
this.isFirst = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcPosition = () => {
|
||||||
|
const maxWidth = parseInt(getSettings("width"));
|
||||||
|
const maxHeight = parseInt(getSettings("height"));
|
||||||
|
const wrapper = ReactDOM.findDOMNode(this.refs.wrapper);
|
||||||
|
const panelWidth = Math.min(wrapper.clientWidth, maxWidth);
|
||||||
|
const panelHeight = Math.min(wrapper.clientHeight, maxHeight);
|
||||||
|
const windowWidth = document.documentElement.clientWidth;
|
||||||
|
const windowHeight = document.documentElement.clientHeight;
|
||||||
|
const offset = 10;
|
||||||
|
|
||||||
|
//TODO: パネルの表示位置オプション
|
||||||
|
let position = {
|
||||||
|
x: this.props.position.x - panelWidth / 2,
|
||||||
|
y: this.props.position.y + offset
|
||||||
|
};
|
||||||
|
|
||||||
|
if (position.x + panelWidth > windowWidth - offset) {
|
||||||
|
position.x = windowWidth - panelWidth - offset;
|
||||||
|
}
|
||||||
|
if (position.y + panelHeight > windowHeight - offset) {
|
||||||
|
position.y = windowHeight - panelHeight - offset;
|
||||||
|
}
|
||||||
|
if (position.x < 0 + offset) {
|
||||||
|
position.x = offset;
|
||||||
|
}
|
||||||
|
if (position.y < 0 + offset) {
|
||||||
|
position.y = offset;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
};
|
||||||
|
|
||||||
|
calcSize = () => {
|
||||||
|
const wrapper = ReactDOM.findDOMNode(this.refs.wrapper);
|
||||||
|
const wrapperWidth = wrapper.clientWidth;
|
||||||
|
const wrapperHeight = wrapper.clientHeight;
|
||||||
|
return { panelWidth: wrapperWidth, panelHeight: wrapperHeight };
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillReceiveProps = nextProps => {
|
||||||
|
const isChangedContents =
|
||||||
|
this.props.resultText !== nextProps.resultText ||
|
||||||
|
this.props.candidateText !== nextProps.candidateText;
|
||||||
|
|
||||||
|
if (isChangedContents && nextProps.shouldShow) this.setState({ shouldResize: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidUpdate = () => {
|
||||||
|
if (!this.state.shouldResize || !this.props.shouldShow) return;
|
||||||
|
const panelPosition = this.calcPosition();
|
||||||
|
const { panelWidth, panelHeight } = this.calcSize();
|
||||||
|
this.setState({
|
||||||
|
shouldResize: false,
|
||||||
|
panelPosition: panelPosition,
|
||||||
|
panelWidth: panelWidth,
|
||||||
|
panelHeight: panelHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
const { width, height } = this.state.shouldResize
|
||||||
|
? { width: parseInt(getSettings("width")), height: parseInt(getSettings("height")) }
|
||||||
|
: { width: this.state.panelWidth, height: this.state.panelHeight };
|
||||||
|
|
||||||
|
const panelStyles = {
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
top: this.state.panelPosition.y,
|
||||||
|
left: this.state.panelPosition.x,
|
||||||
|
fontSize: parseInt(getSettings("fontSize")),
|
||||||
|
backgroundColor: getSettings("bgColor")
|
||||||
|
};
|
||||||
|
const wrapperStyles = {
|
||||||
|
overflow: this.state.shouldResize ? "hidden" : "auto"
|
||||||
|
};
|
||||||
|
const resultStyles = {
|
||||||
|
color: getSettings("resultFontColor")
|
||||||
|
};
|
||||||
|
const candidateStyles = {
|
||||||
|
color: getSettings("candidateFontColor")
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`simple-translate-panel ${this.props.shouldShow ? "isShow" : ""}`}
|
||||||
|
ref="panel"
|
||||||
|
style={panelStyles}
|
||||||
|
>
|
||||||
|
<div className="simple-translate-result-wrapper" ref="wrapper" style={wrapperStyles}>
|
||||||
|
<p className="simple-translate-result" style={resultStyles}>
|
||||||
|
{this.props.resultText}
|
||||||
|
</p>
|
||||||
|
<p className="simple-translate-candidate" style={candidateStyles}>
|
||||||
|
{this.props.candidateText}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
6
src/content/index.js
Normal file
6
src/content/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import TranslateContainer from "./components/TranslateContainer";
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML("afterend", "<div id='simple-translate'></div>");
|
||||||
|
ReactDOM.render(<TranslateContainer />, document.getElementById("simple-translate"));
|
40
src/content/styles/TranslateButton.scss
Normal file
40
src/content/styles/TranslateButton.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
#simple-translate {
|
||||||
|
.simple-translate-button {
|
||||||
|
all: initial;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 10%;
|
||||||
|
background-image: url("../../icons/512.png");
|
||||||
|
background-size: 75%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2147483647;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition-property: opacity, visibility;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
&.isShow {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition-duration: 0ms;
|
||||||
|
animation: simple-translate-showButton 200ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes simple-translate-showButton {
|
||||||
|
0% {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale3d(1.1, 1.1, 1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale3d(1, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/content/styles/TranslateContainer.scss
Normal file
8
src/content/styles/TranslateContainer.scss
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
:root {
|
||||||
|
--simple-translate-main-text: #0c0c0d;
|
||||||
|
--simple-translate-sub-text: #737373;
|
||||||
|
--simple-translate-line: #ededf0;
|
||||||
|
--simple-translate-button: #d7d7db;
|
||||||
|
--simple-translate-highlight: #5595ff;
|
||||||
|
--simple-translate-main-bg: #ffffff;
|
||||||
|
}
|
56
src/content/styles/TranslatePanel.scss
Normal file
56
src/content/styles/TranslatePanel.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
#simple-translate {
|
||||||
|
.simple-translate-panel {
|
||||||
|
all: initial;
|
||||||
|
position: fixed;
|
||||||
|
background-color: var(--simple-translate-main-bg);
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 2147483646;
|
||||||
|
|
||||||
|
transition-property: opacity, visibility;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
&.isShow {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simple-translate-result-wrapper {
|
||||||
|
padding: 10px 18px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: max-content;
|
||||||
|
height: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
p {
|
||||||
|
all: initial;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", "San Francisco", "Ubuntu", "Fira Sans", "Roboto", "Arial",
|
||||||
|
"Helvetica", sans-serif !important;
|
||||||
|
text-align: left;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: 150%;
|
||||||
|
visibility: inherit;
|
||||||
|
opacity: inherit;
|
||||||
|
|
||||||
|
&::-moz-selection {
|
||||||
|
background: var(--simple-translate-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.simple-translate-result {
|
||||||
|
color: var(--simple-translate-main-text);
|
||||||
|
}
|
||||||
|
&.simple-translate-candidate {
|
||||||
|
color: var(--simple-translate-sub-text);
|
||||||
|
margin-top: 1em;
|
||||||
|
&:empty {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,8 +43,7 @@
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||||
"css": ["content/simple-translate.css"],
|
"js": ["content/content.js"]
|
||||||
"js": ["content/simple-translate.js"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,7 @@
|
||||||
{
|
{
|
||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||||
"css": ["content/simple-translate.css"],
|
"js": ["content/content.js"]
|
||||||
"js": ["content/simple-translate.js"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* Copyright (c) 2018 Kamil Mikosz
|
/* Copyright (c) 2018 Kamil Mikosz
|
||||||
|
* Copyright (c) 2019 Sienori
|
||||||
* Released under the MIT license.
|
* Released under the MIT license.
|
||||||
* see https://opensource.org/licenses/MIT */
|
* see https://opensource.org/licenses/MIT */
|
||||||
|
|
||||||
|
@ -72,6 +73,11 @@ const generalConfig = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif)$/,
|
||||||
|
loader: "url-loader",
|
||||||
|
options: {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,11 @@ const generalConfig = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif)$/,
|
||||||
|
loader: "url-loader",
|
||||||
|
options: {}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ const getEntry = (sourceDir = "src") => {
|
||||||
return {
|
return {
|
||||||
popup: path.resolve(__dirname, `${sourceDir}/popup/index.js`),
|
popup: path.resolve(__dirname, `${sourceDir}/popup/index.js`),
|
||||||
options: path.resolve(__dirname, `${sourceDir}/options/index.js`),
|
options: path.resolve(__dirname, `${sourceDir}/options/index.js`),
|
||||||
content: path.resolve(__dirname, `${sourceDir}/content/simple-translate.js`),
|
content: path.resolve(__dirname, `${sourceDir}/content/index.js`),
|
||||||
background: path.resolve(__dirname, `${sourceDir}/background/background.js`)
|
background: path.resolve(__dirname, `${sourceDir}/background/background.js`)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -49,10 +49,6 @@ const getCopyPlugins = (browserDir, outputDir = "dev", sourceDir = "src") => [
|
||||||
from: `${sourceDir}/_locales`,
|
from: `${sourceDir}/_locales`,
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`)
|
to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
from: `${sourceDir}/content/simple-translate.css`,
|
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/content/simple-translate.css`)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
from: `${sourceDir}/manifest-chrome.json`,
|
from: `${sourceDir}/manifest-chrome.json`,
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`)
|
to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`)
|
||||||
|
@ -70,10 +66,6 @@ const getFirefoxCopyPlugins = (browserDir, outputDir = "dev", sourceDir = "src")
|
||||||
from: `${sourceDir}/_locales`,
|
from: `${sourceDir}/_locales`,
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`)
|
to: path.resolve(__dirname, `${outputDir}/${browserDir}/_locales`)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
from: `${sourceDir}/content/simple-translate.css`,
|
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/content/simple-translate.css`)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
from: `${sourceDir}/manifest-firefox.json`,
|
from: `${sourceDir}/manifest-firefox.json`,
|
||||||
to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`)
|
to: path.resolve(__dirname, `${outputDir}/${browserDir}/manifest.json`)
|
||||||
|
|
Loading…
Reference in a new issue