From 5d340864c44cd55c2194be2b2bad77285f94f6f3 Mon Sep 17 00:00:00 2001 From: hensm Date: Tue, 23 Oct 2018 00:56:11 +0100 Subject: [PATCH] Implement friendlier whitelist editing UI --- ext/src/options/index.css | 22 +++ ext/src/options/index.jsx | 325 +++++++++++++++++++++++++++++++------- 2 files changed, 290 insertions(+), 57 deletions(-) diff --git a/ext/src/options/index.css b/ext/src/options/index.css index bb3b744..60464dc 100644 --- a/ext/src/options/index.css +++ b/ext/src/options/index.css @@ -41,3 +41,25 @@ *:invalid { 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%; +} + diff --git a/ext/src/options/index.jsx b/ext/src/options/index.jsx index 4b0eee5..8291a3e 100644 --- a/ext/src/options/index.jsx +++ b/ext/src/options/index.jsx @@ -7,20 +7,268 @@ import ReactDOM from "react-dom"; const _ = browser.i18n.getMessage; -const MATCH_PATTERN_REGEX = /^(?:(\*|https?|file|ftp):\/\/((?:\*\.|[^\/\*])+)(\/.*)|)$/; +const MATCH_PATTERN_REGEX = /^(?:(?:(\*|https?|ftp):\/\/((?:\*\.|[^\/\*])+)|(file):\/\/\/?(?:\*\.|[^\/\*])+)(\/.*)|)$/; + +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 ( +
  • +
    + { this.state.editing + ? + : this.props.text } +
    + + +
  • + ); + } +} + +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 ( +
    + + { this.state.rawView && + } +
    + { + this.state.rawView + ? ( ) + : (
      + { items.map((item, i) => + )} + { this.state.addingNewItem && + } + +
    ) + } +
    + ); + } +} class OptionsApp extends Component { constructor (props) { super(props); this.state = { - isFormValid: true + ...props.options + , isFormValid: true }; - this.handleFormSubmit = this.handleFormSubmit.bind(this); - this.handleFormChange = this.handleFormChange.bind(this); - this.handleInputChange = this.handleInputChange.bind(this); - this.handleUAWhitelistChange = this.handleUAWhitelistChange.bind(this); + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.handleFormChange = this.handleFormChange.bind(this); + this.handleInputChange = this.handleInputChange.bind(this); + this.handleListChange = this.handleListChange.bind(this); } /** @@ -29,35 +277,14 @@ class OptionsApp extends Component { setStorage () { return browser.storage.sync.set({ options: { - option_localMediaEnabled : this.state.option_localMediaEnabled + option_localMediaEnabled : this.state.option_localMediaEnabled , option_localMediaServerPort : this.state.option_localMediaServerPort - , option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled - , option_uaWhitelist : this.state.option_uaWhitelist + , option_uaWhitelistEnabled : this.state.option_uaWhitelistEnabled + , 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) { ev.preventDefault(); @@ -108,24 +335,9 @@ class OptionsApp extends Component { }); } - handleUAWhitelistChange (ev) { - // 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(""); - } - + handleListChange (data) { this.setState({ - [ ev.target.name ]: matchPatterns + option_uaWhitelist: data }); } @@ -188,13 +400,10 @@ class OptionsApp extends Component {
    { _("options_option_uaWhitelist") }
    - + @@ -210,6 +419,8 @@ class OptionsApp extends Component { } -ReactDOM.render( - - , document.querySelector("#root")); +browser.storage.sync.get("options").then(({options}) => { + ReactDOM.render( + + , document.querySelector("#root")); +});