mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-12 02:29:59 +00:00
Implement friendlier whitelist editing UI
This commit is contained in:
@@ -41,3 +41,25 @@
|
|||||||
*:invalid {
|
*:invalid {
|
||||||
box-shadow: 0 0 1.5px 1px red;
|
box-shadow: 0 0 1.5px 1px red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editable-list {
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
.editable-list__item {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
.editable-list__title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.editable-list__edit-field {
|
||||||
|
width: -moz-available;
|
||||||
|
margin-inline-end: 1em;
|
||||||
|
}
|
||||||
|
.editable-list__raw-view {
|
||||||
|
max-height: 300px;
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,268 @@ import ReactDOM from "react-dom";
|
|||||||
const _ = browser.i18n.getMessage;
|
const _ = browser.i18n.getMessage;
|
||||||
|
|
||||||
|
|
||||||
const MATCH_PATTERN_REGEX = /^(?:(\*|https?|file|ftp):\/\/((?:\*\.|[^\/\*])+)(\/.*)|<all_urls>)$/;
|
const MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/((?:\*\.|[^\/\*])+)|(file):\/\/\/?(?:\*\.|[^\/\*])+)(\/.*)|<all_urls>)$/;
|
||||||
|
|
||||||
|
class EditableListItem extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
editing: this.props.editing || false
|
||||||
|
, editValue: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleRemove = this.handleRemove.bind(this);
|
||||||
|
this.handleEditBegin = this.handleEditBegin.bind(this);
|
||||||
|
this.handleEditEnd = this.handleEditEnd.bind(this);
|
||||||
|
this.handleInputChange = this.handleInputChange.bind(this);
|
||||||
|
this.handleInputKeyPress = this.handleInputKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRemove () {
|
||||||
|
this.props.onRemove(this.props.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditBegin () {
|
||||||
|
this.setState({
|
||||||
|
editing: true
|
||||||
|
, editValue: this.props.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEditEnd (ev) {
|
||||||
|
if (this.props.editing
|
||||||
|
&& !this.props.itemPattern.test(this.state.editValue)) {
|
||||||
|
ev.target.setCustomValidity(this.props.itemPatternError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ev.target.validity.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onEdit(this.props.text, this.state.editValue);
|
||||||
|
this.setState({
|
||||||
|
editing: false
|
||||||
|
, editValue: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputChange (ev) {
|
||||||
|
this.setState({
|
||||||
|
editValue: ev.target.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.props.itemPattern.test(ev.target.value)) {
|
||||||
|
ev.target.setCustomValidity(this.props.itemPatternError);
|
||||||
|
} else {
|
||||||
|
ev.target.setCustomValidity("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleInputKeyPress (ev) {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
this.handleEditEnd({ target: ev.target });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<li className="editable-list__item">
|
||||||
|
<div className="editable-list__title"
|
||||||
|
onDoubleClick={this.handleEditBegin}>
|
||||||
|
{ this.state.editing
|
||||||
|
? <input className="editable-list__edit-field"
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
value={this.state.editValue}
|
||||||
|
onBlur={this.handleEditEnd}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
onKeyPress={this.handleInputKeyPress}/>
|
||||||
|
: this.props.text }
|
||||||
|
</div>
|
||||||
|
<button onClick={this.handleEditBegin}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button onClick={this.handleRemove}>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditableList extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
items: new Set(this.props.data)
|
||||||
|
, addingNewItem: false
|
||||||
|
, rawView: false
|
||||||
|
, rawViewValue: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleItemRemove = this.handleItemRemove.bind(this);
|
||||||
|
this.handleItemEdit = this.handleItemEdit.bind(this);
|
||||||
|
this.handleSwitchView = this.handleSwitchView.bind(this);
|
||||||
|
this.handleSaveRaw = this.handleSaveRaw.bind(this);
|
||||||
|
this.handleRawViewTextAreaChange = this.handleRawViewTextAreaChange.bind(this);
|
||||||
|
this.handleAddItem = this.handleAddItem.bind(this);
|
||||||
|
this.handleNewItemRemove = this.handleNewItemRemove.bind(this);
|
||||||
|
this.handleNewItemEdit = this.handleNewItemEdit.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemRemove (item) {
|
||||||
|
this.setState(currentState => {
|
||||||
|
const newItems = new Set(currentState.items);
|
||||||
|
newItems.delete(item);
|
||||||
|
return {
|
||||||
|
items: newItems
|
||||||
|
};
|
||||||
|
}, () => {
|
||||||
|
this.props.onListChange(Array.from(this.state.items));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleItemEdit (item, newValue) {
|
||||||
|
this.setState(currentState => ({
|
||||||
|
items: new Set([...currentState.items]
|
||||||
|
.map(item_ => item_ === item ? newValue : item_))
|
||||||
|
}), () => {
|
||||||
|
this.props.onListChange(Array.from(this.state.items));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSwitchView () {
|
||||||
|
this.setState(currentState => {
|
||||||
|
if (currentState.rawView) {
|
||||||
|
return {
|
||||||
|
rawView: false
|
||||||
|
, rawViewValue: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawView: true
|
||||||
|
, rawViewValue: [...currentState.items.values()].join("\n")
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveRaw () {
|
||||||
|
this.setState(currentState => {
|
||||||
|
const newItems = currentState.rawViewValue.split("\n")
|
||||||
|
.filter(item => item !== "");
|
||||||
|
|
||||||
|
if ("itemPattern" in this.props) {
|
||||||
|
for (const item of newItems) {
|
||||||
|
if (!this.props.itemPattern.test(item)) {
|
||||||
|
this.rawViewTextArea.setCustomValidity(
|
||||||
|
`${this.props.itemPatternError}: ${item}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: new Set(newItems)
|
||||||
|
};
|
||||||
|
}, () => {
|
||||||
|
this.props.onListChange(Array.from(this.state.items));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRawViewTextAreaChange (ev) {
|
||||||
|
if (this.rawViewTextArea.scrollHeight > this.rawViewTextArea.clientHeight) {
|
||||||
|
this.rawViewTextArea.style.height = `${this.rawViewTextArea.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
rawViewValue: ev.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddItem () {
|
||||||
|
this.setState({
|
||||||
|
addingNewItem: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewItemRemove () {
|
||||||
|
this.setState({
|
||||||
|
addingNewItem: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewItemEdit (item, newItem) {
|
||||||
|
this.setState(currentState => ({
|
||||||
|
items: [ ...currentState.items, newItem ]
|
||||||
|
, addingNewItem: false
|
||||||
|
}), () => {
|
||||||
|
this.props.onListChange(Array.from(this.state.items));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const items = Array.from(this.state.items.values());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="editable-list">
|
||||||
|
<button className="editable-list__view-button"
|
||||||
|
onClick={this.handleSwitchView}>
|
||||||
|
{ this.state.rawView ? "Basic" : "Raw" } View
|
||||||
|
</button>
|
||||||
|
{ this.state.rawView &&
|
||||||
|
<button className="editable-list__save-raw-button"
|
||||||
|
onClick={this.handleSaveRaw}>
|
||||||
|
Save Raw
|
||||||
|
</button> }
|
||||||
|
<hr />
|
||||||
|
{
|
||||||
|
this.state.rawView
|
||||||
|
? ( <textarea className="editable-list__raw-view"
|
||||||
|
rows={items.length}
|
||||||
|
value={this.state.rawViewValue}
|
||||||
|
onChange={this.handleRawViewTextAreaChange}
|
||||||
|
ref={el => { this.rawViewTextArea = el }}>
|
||||||
|
</textarea> )
|
||||||
|
: ( <ul className="editable-list__items">
|
||||||
|
{ items.map((item, i) =>
|
||||||
|
<EditableListItem text={item}
|
||||||
|
itemPattern={this.props.itemPattern}
|
||||||
|
itemPatternError={this.props.itemPatternError}
|
||||||
|
onRemove={this.handleItemRemove}
|
||||||
|
onEdit={this.handleItemEdit}
|
||||||
|
key={i} /> )}
|
||||||
|
{ this.state.addingNewItem &&
|
||||||
|
<EditableListItem text={""}
|
||||||
|
itemPattern={this.props.itemPattern}
|
||||||
|
itemPatternError={this.props.itemPatternError}
|
||||||
|
onRemove={this.handleNewItemRemove}
|
||||||
|
onEdit={this.handleNewItemEdit}
|
||||||
|
editing={true} /> }
|
||||||
|
<button className="editable-list__add-button"
|
||||||
|
onClick={this.handleAddItem}>
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
</ul> )
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class OptionsApp extends Component {
|
class OptionsApp extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isFormValid: true
|
...props.options
|
||||||
|
, isFormValid: true
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
this.handleFormSubmit = this.handleFormSubmit.bind(this);
|
||||||
this.handleFormChange = this.handleFormChange.bind(this);
|
this.handleFormChange = this.handleFormChange.bind(this);
|
||||||
this.handleInputChange = this.handleInputChange.bind(this);
|
this.handleInputChange = this.handleInputChange.bind(this);
|
||||||
this.handleUAWhitelistChange = this.handleUAWhitelistChange.bind(this);
|
this.handleListChange = this.handleListChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,35 +277,14 @@ class OptionsApp extends Component {
|
|||||||
setStorage () {
|
setStorage () {
|
||||||
return browser.storage.sync.set({
|
return browser.storage.sync.set({
|
||||||
options: {
|
options: {
|
||||||
option_localMediaEnabled : this.state.option_localMediaEnabled
|
option_localMediaEnabled : this.state.option_localMediaEnabled
|
||||||
, option_localMediaServerPort : this.state.option_localMediaServerPort
|
, option_localMediaServerPort : this.state.option_localMediaServerPort
|
||||||
, option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled
|
, option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled
|
||||||
, option_uaWhitelist : this.state.option_uaWhitelist
|
, option_uaWhitelist : this.state.option_uaWhitelist
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current options state from storage and set initial
|
|
||||||
*/
|
|
||||||
async componentDidMount () {
|
|
||||||
const { options } = await browser.storage.sync.get("options");
|
|
||||||
|
|
||||||
if (options) {
|
|
||||||
this.setState({
|
|
||||||
...options
|
|
||||||
, isFormValid: this.form.checkValidity()
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.setStorage();
|
|
||||||
} catch (err) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async handleFormSubmit (ev) {
|
async handleFormSubmit (ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
@@ -108,24 +335,9 @@ class OptionsApp extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUAWhitelistChange (ev) {
|
handleListChange (data) {
|
||||||
// Split patterns by newline
|
|
||||||
const matchPatterns = ev.target.value.split("\n");
|
|
||||||
|
|
||||||
// Validate each pattern against a regexp
|
|
||||||
for (const pattern of matchPatterns) {
|
|
||||||
if (!MATCH_PATTERN_REGEX.test(pattern)) {
|
|
||||||
// Set as invalid
|
|
||||||
ev.target.setCustomValidity(`Match pattern invalid: ${pattern}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set as valid
|
|
||||||
ev.target.setCustomValidity("");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
[ ev.target.name ]: matchPatterns
|
option_uaWhitelist: data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,13 +400,10 @@ class OptionsApp extends Component {
|
|||||||
<div className="option-label">
|
<div className="option-label">
|
||||||
{ _("options_option_uaWhitelist") }
|
{ _("options_option_uaWhitelist") }
|
||||||
</div>
|
</div>
|
||||||
<textarea name="option_uaWhitelist"
|
<EditableList data={ this.state.option_uaWhitelist }
|
||||||
value={ this.state.option_uaWhitelist
|
onListChange={ this.handleListChange }
|
||||||
&& this.state.option_uaWhitelist.join("\n") }
|
itemPattern={MATCH_PATTERN_REGEX}
|
||||||
required
|
itemPatternError="Invalid match pattern"/>
|
||||||
rows="8"
|
|
||||||
onChange={ this.handleUAWhitelistChange }>
|
|
||||||
</textarea>
|
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
@@ -210,6 +419,8 @@ class OptionsApp extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ReactDOM.render(
|
browser.storage.sync.get("options").then(({options}) => {
|
||||||
<OptionsApp />
|
ReactDOM.render(
|
||||||
, document.querySelector("#root"));
|
<OptionsApp options={options} />
|
||||||
|
, document.querySelector("#root"));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user