blob: 653c049c9a03b05f599be23a44f14b3733ee6c30 [file] [log] [blame]
import {css, customElement, html, LitElement, property } from 'lit-element';
import {getDefaultRules, isInjectRule, Operator, Rule} from './utils';
const EMPTY_RULE = {
disabled: false,
target: '',
operator: Operator.BLOCK,
destination: '',
isNew: true,
};
/**
* GdhApp component.
*/
@customElement('gdh-app')
export class GdhApp extends LitElement {
@property({type: Array, attribute: false}) rules: Rule[] = [];
@property() changeId: string = '';
@property() announcementText = 'To get latest rules, click `reset`.';
@property({type: Boolean}) isImport = false;
@property() rulesStr = '';
@property() importError = '';
constructor() {
super();
this.loadRules();
const lastAnnouncement = window.localStorage.getItem('helper-announcement');
if (!lastAnnouncement || lastAnnouncement !== this.announcementText) {
window.localStorage.setItem('helper-announcement', this.announcementText);
setTimeout(() => {
this.announcementText = '';
}, 3 * 1000);
} else {
// only show it once, if showed, don't show them again
this.announcementText = '';
}
}
private loadRules() {
chrome.storage.sync.get(['rules', 'enabled'], (result) => {
if (!result['rules']) return;
this.rules =
(result['rules'] as Rule[]).map(rule => (rule.isNew = false, rule));
this.rulesStr = JSON.stringify(this.rules, null, 2);
});
}
saveRules() {
chrome.storage.sync.set({rules: this.rules.slice()});
chrome.runtime.sendMessage(
{type: 'updateRules', rules: this.rules.slice()});
this.refresh();
}
addNewRule() {
this.rules = [...this.rules, {...EMPTY_RULE}];
}
resetRules() {
getDefaultRules().then(rules => {
this.rules = [...rules];
this.rulesStr = JSON.stringify(this.rules, null, 2);
window.localStorage.removeItem('helper-announcement');
this.requestUpdate();
})
}
onRuleDeletion(event: CustomEvent<Rule>) {
this.rules = this.rules.filter(r => r !== event.detail);
this.rulesStr = JSON.stringify(this.rules, null, 2);
}
enableMeOnly(event: CustomEvent<Rule>) {
this.rules = this.rules.map(rule => {
const toggledRule = event.detail;
rule.disabled = rule !== toggledRule;
return {...rule};
});
}
onRuleChanged(event: CustomEvent<Rule>) {
this.rules = this.rules.map(rule => ({...rule}));
}
disableHelper() {
this.refresh(tab => {
chrome.runtime.sendMessage({type: 'disableHelper', tab});
return null;
});
window.close();
}
private refresh(runBefore?: (tab: chrome.tabs.Tab) => null) {
// refresh the tab now
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
if (!tabs[0] || !tabs[0].id) return;
if (runBefore) runBefore(tabs[0]);
chrome.tabs.update(tabs[0].id, {url: tabs[0].url});
});
}
startImport() {
this.isImport = true;
}
confirmImport() {
try {
const rules = JSON.parse(this.rulesStr);
this.rules = rules;
this.saveRules();
this.isImport = false;
} catch (e) {
this.importError = e.message;
}
}
cancelImport() {
this.isImport = false;
}
handleRulesInputChange(e: Event) {
this.rulesStr = (e.target as HTMLInputElement).value;
}
exportRules() {
const dataStr = 'data:text/json;charset=utf-8,' +
encodeURIComponent(JSON.stringify(this.rules));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute('href', dataStr);
downloadAnchorNode.setAttribute('download', 'rules.json');
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
render() {
return html`
<p ?hidden=${!this.announcementText}>${this.announcementText}</p>
<header>
<button @click=${this.disableHelper}>Disable</button>
</header>
<div ?hidden=${this.isImport}>
<gdh-rule-set
@enable-me-only=${this.enableMeOnly}
@rule-deleted=${this.onRuleDeletion}
@rule-changed=${this.onRuleChanged}
.rules=${this.rules}>
</gdh-rule-set>
<div class="buttons">
<button @click=${this.saveRules}>Save</button>
<button @click=${this.addNewRule}>Add</button>
<button @click=${this.resetRules}>Reset</button>
<button @click=${this.startImport}>Import</button>
<button @click=${this.exportRules}>Export</button>
</div>
</div>
<div ?hidden=${!this.isImport}>
<p ?hidden=${!this.importError}>${this.importError}</p>
<textarea
name="rules"
.value=${this.rulesStr}
@input=${this.handleRulesInputChange}>
</textarea>
<div class="buttons">
<button @click=${this.confirmImport}>Import</button>
<button @click=${this.cancelImport}>Cancel</button>
<button @click=${this.resetRules}>Reset</button>
</div>
</div>
`;
}
static get styles() {
return css`
p {
background: yellow;
padding: 10px;
}
button {
font-family: Roboto, sans-serif;
font-size: .875rem;
font-weight: 500;
text-transform: uppercase;
user-select: none;
box-sizing: content-box;
border-radius: 4px;
border: 1px solid #333;
padding: 5px;
cursor: pointer;
outline: none;
}
button:hover {
background-color: #f4f0fa;
}
header {
display: flex;
flex-direction: row-reverse;
}
textarea {
min-width: 500px;
min-height: 500px;
}
`;
}
}
/**
* GdhRuleSet component.
*/
@customElement('gdh-rule-set')
export class GdhRuleSet extends LitElement {
@property({type: Array, attribute: false}) rules: Rule[] = [];
render() {
return html`
<ul>
<li>
<span></span>
<span>Target</span>
<span>Operator</span>
<span>Destination</span>
</li>
${
this.rules.map(
rule =>
html`<li><gdh-rule-item .rule=${rule}></gdh-rule-item></li>`)}
</ul>
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: row;
}
ul {
list-style: none;
margin: 0;
padding: 10px;
width: 100%;
}
ul li {
display: flex;
margin: 5px 0;
width: 100%;
}
li span {
text-align: center;
}
li span:nth-child(1) {
flex-basis: 30px;
}
li span:nth-child(2) {
flex-basis: 220px;
}
li span:nth-child(3) {
flex-basis: 130px;
}
li span:nth-child(4) {
flex: 1;
}
`;
}
}
/**
* GdhRuleItem component.
*/
@customElement('gdh-rule-item')
export class GdhRuleItem extends LitElement {
@property({type: Object, attribute: false}) rule: Rule = {...EMPTY_RULE};
operators = [
Operator.BLOCK,
Operator.REDIRECT,
Operator.INJECT_HTML_PLUGIN,
Operator.INJECT_HTML_CODE,
Operator.INJECT_JS_PLUGIN,
Operator.INJECT_JS_MODULE_PLUGIN,
Operator.INJECT_JS_CODE,
Operator.INJECT_EXP,
Operator.ADD_REQUEST_HEADER,
Operator.ADD_RESPONSE_HEADER,
Operator.REMOVE_RESPONSE_HEADER,
];
handleInputOnTarget(e: Event) {
this.rule.target = (e.target as HTMLInputElement).value;
this.requestUpdate();
}
handleInputOnDestination(e: Event) {
this.rule.destination = (e.target as HTMLInputElement).value;
this.requestUpdate();
}
onSelectedChange(e: CustomEvent<number>) {
this.rule.operator = this.operators[e.detail];
this.requestUpdate();
}
onRuleDeletion(rule: Rule) {
this.dispatchEvent(new CustomEvent<Rule>('rule-deleted', {
detail: rule,
bubbles: true,
composed: true,
}));
}
toggleDisable(e: KeyboardEvent) {
this.rule.disabled = !this.rule.disabled;
// notify the change
this.dispatchEvent(new CustomEvent<Rule>('rule-changed', {
detail: this.rule,
bubbles: true,
composed: true,
}));
}
enableOnlyMe(e: KeyboardEvent) {
this.dispatchEvent(new CustomEvent<Rule>('enable-me-only', {
detail: this.rule,
bubbles: true,
composed: true,
}));
}
render() {
return html`
<input
type="checkbox"
.checked=${!this.rule.disabled}
@dblclick=${this.enableOnlyMe}
@click=${this.toggleDisable} />
<input
class="target"
.disabled=${isInjectRule(this.rule)}
type="text"
.value=${this.rule.target}
@input=${this.handleInputOnTarget} />
<gdh-dropdown
.selectedIndex=${this.operators.indexOf(this.rule.operator)}
@select-changed=${this.onSelectedChange}
.items=${this.operators}>
</gdh-dropdown>
<textarea
name="destination"
.value=${this.rule.destination}
@input=${this.handleInputOnDestination}
.disabled=${this.rule.operator === Operator.BLOCK}>
</textarea>
<span class="deleteBtn" @click=${
this.onRuleDeletion.bind(this, this.rule)}>x</span>
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: row;
}
.deleteBtn {
padding: 5px;
font-size: 18px;
cursor: pointer;
flex-basis: 20px;
}
input[type="checkbox"] {
margin: 10px;
flex-basis: 20px;
}
input[type="text"],
textarea {
flex: 1;
min-width: 220px;
outline: none;
border: none;
border-bottom: 1px solid #ccc;
}
input[type="text"]:focus,
textarea:focus {
border-bottom: 1px solid #3c88fd;
background: #9dc3fd;
}
input[disabled],
textarea[disabled] {
background: #ddd;
}
`;
}
}
/**
* GdhDropdown component.
*/
@customElement('gdh-dropdown')
export class GdhDropdown extends LitElement {
@property({type: Array, attribute: false}) items: unknown[] = [];
@property({type: Number}) selectedIndex: number = 0;
@property({type: Boolean}) isVisible = false;
handleSelect(idx: number) {
this.selectedIndex = idx;
this.dispatchEvent(new CustomEvent<number>('select-changed', {
detail: idx,
bubbles: true,
composed: true,
}));
this.isVisible = false;
}
toggleVisible(e: Event) {
this.isVisible = (e.target as HTMLInputElement).checked;
}
render() {
return html`
<label for="trigger" @click=${this.toggleVisible}>
<div class="selected-value">${this.items[this.selectedIndex]}</div>
<input
.checked=${this.isVisible}
@input=${this.toggleVisible}
type="checkbox"
name="trigger" />
<ul class="options">
${this.items.map((item, i) => html`
<li
@click=${this.handleSelect.bind(this, i)}
class="${i === this.selectedIndex ? 'active' : ''}">
${item}
</li>
`)}
</ul>
<i></i>
</label>
`;
}
static get styles() {
return css`
:host {
position: relative;
display: flex;
flex-direction: column;
}
label {
background-color: #f1f1f1;
margin: 0 5px;
}
i {
position: absolute;
top: 10px;
right: 15px;
border: solid #555;
border-width: 0 3px 3px 0;
display: inline-block;
padding: 3px;
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
.selected-value {
padding: 8px 15px;
min-width: 100px;
}
ul {
display: none;
list-style: none;
margin: 0;
padding: 0;
width: 100%;
position: absolute;
top: 100%;
left: 0;
background: #eee;
z-index: 1;
}
input {
opacity: 0;
position: absolute;
top: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
input:checked + ul {
display: block;
box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.2),0 1px 1px 0 rgba(0, 0, 0, 0.14),0 1px 3px 0 rgba(0,0,0,.12);
}
input:checked + ul li:hover {
background: #ddd;
}
input:checked + ul li {
padding: 5px 10px;
cursor: pointer;
}
input:checked + ul li.active,
input:checked + ul li.active:hover {
background-color: #fff;
}
input:checked + ul + i {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
}
`;
}
}