diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js index e93a82a..8c4463d 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/App.js @@ -7,6 +7,7 @@ import getNotControlledWarning from './NotControlledWarning'; import getTabPanel from './TabPanel'; import getPacChooser from './PacChooser'; import getNotifications from './Notifications'; +import getExceptions from './Exceptions'; import getFooter from './Footer'; @@ -14,8 +15,11 @@ export default function getApp(theState) { const NotControlledWarning = getNotControlledWarning(theState); const TabPanel = getTabPanel(theState); + const PacChooser = getPacChooser(theState); const Notifications = getNotifications(theState); + const Exceptions = getExceptions(theState); + const Footer = getFooter(theState); return class App extends Component { @@ -44,7 +48,7 @@ export default function getApp(theState) { const warningHtml = warns .map( - (w) => w && w.messageHtml || '' + (w) => w && w.message || '' ) .filter( (m) => m ) .map( (m) => '✘ ' + m ) @@ -104,7 +108,7 @@ export default function getApp(theState) { warns = warns.filter( (w) => w ); if (err || warns.length) { - showErrors(err, ...warns); + this.showErrors(err, ...warns); } else { this.setStatusTo(afterStatus); } @@ -119,12 +123,13 @@ export default function getApp(theState) { } - render(props) { + render(originalProps) { - props = Object.assign({}, props, { + const props = Object.assign({}, originalProps, { funs: { setStatusTo: this.setStatusTo.bind(this), conduct: this.conduct.bind(this), + showErrors: this.showErrors.bind(this), }, ifInputsDisabled: this.state.ifInputsDisabled, }); @@ -139,7 +144,7 @@ export default function getApp(theState) { }, { label: 'Исключения', - content: "Exceptions().render(this.props)", + content: createElement(Exceptions, props), }, { label: 'Свои прокси', diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js new file mode 100644 index 0000000..99ede5f --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/ExcEditor.js @@ -0,0 +1,344 @@ +import Inferno from 'inferno'; +import Component from 'inferno-component'; +import css from 'csjs-inject'; + +export default function getExcEditor(theState) { + + const scopedCss = css` + + #exc-address-container { + display: flex; + align-items: center; + width: 100%; + } + #exc-address-container > a { + border-bottom: 1px solid transparent; + margin-left: 0.2em; + align-self: flex-end; + } + #exc-address { + width: 100%; + display: flex; + align-items: baseline; + --exc-hieght: 1.6em; + font-size: 1em; + border-bottom: 1px solid var(--ribbon-color) !important; + } + input#exc-editor { + border: none; + width: 100%; + background: inherit; + /* The two below align '.' (dot) vertically. */ + max-height: var(--exc-hieght) !important; + min-height: var(--exc-hieght) !important; + } + #exc-radio { + display: flex; + justify-content: space-around; + margin-top: 0.5em; + } + [name="if-proxy-this-site"]:checked + label { + font-weight: bold; + } + #exc-address.ifYes { + background-color: lightgreen; + } + #exc-address.ifNo { + background-color: pink; + } + + `; + + const labelIfProxied = '✔'; + const labelIfNotProxied = '✘'; + const labelIfAuto = '↻'; + + /* Not used. + const sortOptions = (options) => { + + const aWins = 1; + return options.sort(([aHost, aState], [bHost, bState]) => aState === undefined ? aWins : aHost.localeCompare(bHost)) + + }; + */ + + return class ExcEditor extends Component { + + modsToOpts(pacMods) { + + return Object.keys(pacMods.exceptions || {}).sort().map( + (excHost) => [excHost, pacMods.exceptions[excHost]] + ); + + } + + constructor(props) { + + super(props); + + const pacMods = props.apis.pacKitchen.getPacMods(); + this.state = { + trimmedInputValueOrSpace: + props.currentTab && !props.currentTab.url.startsWith('chrome') ? new URL(props.currentTab.url).hostname : '', + sortedListOfOptions: this.modsToOpts(pacMods), + isHostHidden: {} + }; + + } + + hideAllOptions() { + + this.setState({ + isHostHidden: this.state.sortedListOfOptions.reduce( + (isHostHidden, [excHost]) => { + + isHostHidden[excHost] = true; + return isHostHidden; + + }, + {}), + }); + + } + + isHostValid(host) { + + const ValidHostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; + if(!ValidHostnameRegex.test(host)) { + this.props.funs.showErrors(new TypeError('Должно быть только доменное имя, без протокола, порта и пути. Попробуйте ещё раз.')); + return false; + } + return true; + + } + + handleRadioClick(event) { + + const host = this.state.trimmedInputValueOrSpace; + (() => { // `return` === `preventDefault`. + + if(!this.isHostValid(host)) { + return false; + } + + const pacMods = this.props.apis.pacKitchen.getPacMods(); + pacMods.exceptions = pacMods.exceptions || {}; + + let ifYesClicked = false; + switch(event.target.id) { + case 'this-auto': + delete pacMods.exceptions[host]; + break; + + case 'this-yes': + ifYesClicked = true; + case 'this-no': + if (ifYesClicked && !pacMods.filteredCustomsString) { + this.props.funs.showErrors( new TypeError( + 'Проксировать СВОИ сайты можно только при наличии СВОИХ прокси (см. «Модификаторы» ). Нет своих прокси, удовлетворяющих вашим требованиям.' + )); + return false; + } + + pacMods.exceptions[host] = ifYesClicked; + break; + + default: + throw new Error('Only yes/no/auto!'); + } + + this.props.funs.conduct( + 'Применяем исключения...', + (cb) => this.props.apis.pacKitchen.keepCookedNowAsync(pacMods, cb), + 'Исключения применены. Не забывайте о кэше!', + () => this.setState({sortedListOfOptions: this.modsToOpts(pacMods)}) + ); + + })(); + // Don't check before operation is finished. + event.preventDefault(); + + } + + handleKeyDown(event) { + + if(event.key === 'Enter') { + this.hideAllOptions(); + } + return true; + + } + + handleInputOrClick(event) { + + // Episode 1. + + const ifClick = event && event.type === 'click'; + + // If triangle button on right of datalist input clicked. + let ifTriangleClicked = false; + { + const maxIndentFromRightInPx = 15; + ifTriangleClicked = ifClick + && !this.rawInput.selectionStart && !this.rawInput.selectionEnd + && event.x > this.rawInput.getBoundingClientRect().right - maxIndentFromRightInPx; + } + + const setInputValue = (newValue) => { + + if (ifClick && !ifTriangleClicked) { + // Don't jerk cursor on simple clicks. + return; + } + // See bug in my comment to http://stackoverflow.com/a/32394157/521957 + // First click on empty input may be still ignored. + const newPos = this.rawInput.selectionStart + newValue.length - this.rawInput.value.length; + this.rawInput.value = newValue; + window.setTimeout(() => this.rawInput.setSelectionRange(newPos, newPos), 0); + + }; + + const trimmedInput = event.target.value.trim(); + const ifInputEmpty = !trimmedInput; + const ifInit = !event; + const currentHost = ifTriangleClicked ? '' : (trimmedInput || (ifInit ? '' : ' ')); + setInputValue(currentHost); + this.setState({trimmedInputValueOrSpace: currentHost}); + + // Episode 2. + + let exactHost, exactState; // Undefined. + let editedHost = false; + const hidden = this.state.sortedListOfOptions.reduce( + (hiddenAcc, [excHost, excState]) => { + + if (excState === undefined) { + editedHost = excHost; + } else if (excHost === trimmedInput) { + // Exact match found for input. + [exactHost, exactState] = [excHost, excState]; + } + hiddenAcc[excHost] = false; + return hiddenAcc; + + }, + {} + ); + let options = this.state.sortedListOfOptions; + const removeEditedHost = () => { + + options = options.filter(([excHost, excState]) => editedHost !== excHost); + delete hidden[editedHost]; + + }; + + + (() => {// `return` === setState + + if (ifTriangleClicked || ifInputEmpty) { + // Show all opts. + if (editedHost) { + // Example of editedOpt.value: 'abcde ' <- Mind the space (see unhideOptAndAddSpace)! + const ifBackspacedOneChar = ifInputEmpty && editedHost.length < 3; + if (ifBackspacedOneChar) { + removeEditedHost(); + } + // + } + return true; + } + + if (editedHost) { + const ifUpdateNeeded = editedHost !== trimmedInput; + if(!ifUpdateNeeded) { + hidden[editedHost] = true; + return true; + } + // Not exact! Update! + removeEditedHost(); + } + + if (!exactHost) { + editedHost = trimmedInput; + options.unshift([editedHost, undefined]); + if (!ifClick) { + // New value was typed -- don't show tooltip. + hidden[editedHost] = true; + } + } + + // Exact found! + // Do nothing. + + })(); + + this.setState({ + isHostHidden: hidden, + sortedListOfOptions: options, + }); + + } + + render(props) { + + const inputProxyingState = this.state.sortedListOfOptions.reduce((acc, [excHost, excState]) => { + + if ( acc !== undefined ) { + return acc; + } + return this.state.trimmedInputValueOrSpace === excHost ? excState : undefined; + + }, undefined); + + const onradio = this.handleRadioClick.bind(this); + const oninput = this.handleInputOrClick.bind(this); + + return ( +
+
Проксировать указанный сайт?
+
+
+ *. { this.rawInput = inputNode; }} + onKeyDown={this.handleKeyDown.bind(this)} + onInput={oninput} + onClick={oninput} + /> +
+ {/**/} + + +
+ + { + this.state.sortedListOfOptions.map(([excHost, excState]) => { + + // 1. Option's value may be changed to hide it from the tooltip. + // 2. Space is used in matching so even an empty input (replaced with space) has tooltip with prompts. + return +
    +
  1. {' '} + +
  2. +
  3. {' '}
  4. +
  5. {' '}
  6. +
+
+ ); + + } + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js new file mode 100644 index 0000000..7bf3f6d --- /dev/null +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/Exceptions.js @@ -0,0 +1,68 @@ +import Inferno from 'inferno'; +import createElement from 'inferno-create-element'; +import css from 'csjs-inject'; + +import getInfoLi from './InfoLi'; +import getExcEditor from './ExcEditor'; + +export default function getExceptions(theState) { + + const scopedCss = css` + + #excMods { + padding-top: 1em; + } + #excMods input#mods-if-mind-exceptions:not(:checked) + .label-container label { + color: red; + } + + `; + + const InfoLi = getInfoLi(theState); + const ExcEditor = getExcEditor(theState); + + return function Exceptions(props) { + + const applyMods = (newMods) => { + + props.apis.pacKitchen.keepCookedNowAsync(newMods, (err, ...warns) => + err + ? props.funs.showErrors(err, ...warns) + : props.funs.setStatusTo('Применено.') + ); + + }; + + return ( +
+ {createElement(ExcEditor, props)} + +
+ ); + + }; + +}; diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js index 1a91836..d959876 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/InfoLi.js @@ -9,6 +9,9 @@ export default function getInfoRow() { .labelContainer { flex-grow: 9; padding-left: 0.3em; + /* Vertical align to middle. */ + align-self: flex-end; + line-height: 100%; } /* INFO SIGNS */ @@ -122,7 +125,8 @@ export default function getInfoRow() {
) : (props.conf.url - && () + ? () + : ( ) // Affects vertical align of flexbox items. ) } diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js index de63edf..4ca2737 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/components/PacChooser.js @@ -32,7 +32,8 @@ export default function getPacChooser(theState) { input:checked + div .updateButton { visibility: inherit; } - label[for="onlyOwnSites"] + .updateButton { + label[for="onlyOwnSites"] + .updateButton, + label[for="none"] + .updateButton { display: none; } #none:checked + div label[for="none"] { @@ -115,7 +116,7 @@ export default function getPacChooser(theState) { {props.flags.ifInsideOptionsPage && (
PAC-скрипт:
)}
{ createElement(LastUpdateDate, props) } diff --git a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/index.js b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/index.js index 12759ef..444b9b4 100644 --- a/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/index.js +++ b/extensions/chromium/runet-censorship-bypass/src/extension-common/pages/options/src/index.js @@ -34,6 +34,7 @@ chrome.runtime.getBackgroundPage( (backgroundPage) => ); theState.flags.ifInsideOptionsPage = !currentTab || currentTab.url.startsWith('chrome://extensions/?options='); + theState.currentTab = currentTab; // STATE DEFINED, COMPOSE.