From 9e9bd0dcf6b356d926ff46dc57b76b0d3734d8a6 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 24 Oct 2020 16:01:13 -0400 Subject: Profile controller refactor (#953) * Udpate selectors * Optional remove button * Add menu * Refactor ProfileController * Fix selector --- ext/bg/css/settings.css | 36 +- ext/bg/js/settings/profile-conditions-ui.js | 45 +- ext/bg/js/settings/profile-controller.js | 671 ++++++++++++++++++++-------- ext/bg/settings.html | 26 +- 4 files changed, 557 insertions(+), 221 deletions(-) diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index fbea04c0..5d522209 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -66,66 +66,66 @@ html:root:not([data-options-general-result-output-mode=merge]) #dictionary-main- display: flex; -flex-wrap: wrap; } -.condition-input { +.profile-condition-input-container { flex: 1 1 auto; } -.condition-line-break { +.profile-condition-line-break { flex: 1 0 100%; display: none; } .condition>.input-group-btn { width: auto; } -.condition>.condition-prefix:after { +.condition>.profile-condition-prefix:after { content: "IF"; } -.condition:nth-child(n+2)>.condition-prefix:after { +.condition:nth-child(n+2)>.profile-condition-prefix:after { content: "AND"; } -.condition-prefix { +.profile-condition-prefix { flex: 0 0 auto; } -.condition-prefix, -.condition-group-separator-label { +.profile-condition-prefix, +.profile-condition-group-separator-label { width: 60px; text-align: center; } -.condition-group-separator-label { +.profile-condition-group-separator-label { padding: 6px 12px; font-weight: bold; display: inline-block; } -.condition-type, -.condition-operator { +.profile-condition-type, +.profile-condition-operator { width: auto; text-align: center; text-align-last: center; } -.condition-list>.condition>*:first-child, +.profile-condition-list>.condition>*:first-child, .audio-source-list>.audio-source>*:first-child { border-bottom-left-radius: 0; } -.condition-list>.condition:nth-child(n+2)>*:first-child, +.profile-condition-list>.condition:nth-child(n+2)>*:first-child, .audio-source-list>.audio-source:nth-child(n+2)>*:first-child { border-top-left-radius: 0; } -.condition-list>.condition:nth-child(n+2)>div:last-child>button, +.profile-condition-list>.condition:nth-child(n+2)>div:last-child>button, .audio-source-list>.audio-source:nth-child(n+2)>*:last-child>button { border-top-right-radius: 0; } -.condition-list>.condition:nth-last-child(n+2)>div:last-child>button, +.profile-condition-list>.condition:nth-last-child(n+2)>div:last-child>button, .audio-source-list>.audio-source:nth-last-child(n+2)>*:last-child>button { border-bottom-right-radius: 0; } -.condition-group-options>.condition-add, +.profile-condition-group-options>.profile-condition-add-button, .audio-source-options>.audio-source-add { border-top-left-radius: 0; border-top-right-radius: 0; } -.condition-groups>.condition-group:last-child>.condition-group-separator-label { +.profile-condition-groups>.profile-condition-group:last-child>.profile-condition-group-separator-label { display: none; } @@ -438,10 +438,10 @@ html:root[data-operating-system=openbsd] [data-hide-for-operating-system~=openbs .condition { flex-wrap: wrap; } - .condition-input { + .profile-condition-input-container { order: 2; } - .condition-line-break { + .profile-condition-line-break { display: block; order: 1; } diff --git a/ext/bg/js/settings/profile-conditions-ui.js b/ext/bg/js/settings/profile-conditions-ui.js index 4494e51a..78155467 100644 --- a/ext/bg/js/settings/profile-conditions-ui.js +++ b/ext/bg/js/settings/profile-conditions-ui.js @@ -28,6 +28,7 @@ class ProfileConditionsUI { this._children = []; this._eventListeners = new EventListenerCollection(); this._defaultType = 'popupLevel'; + this._profileIndex = 0; this._descriptors = new Map([ [ 'popupLevel', @@ -76,7 +77,7 @@ class ProfileConditionsUI { } get profileIndex() { - return this._settingsController.profileIndex; + return this._profileIndex; } get os() { @@ -87,7 +88,8 @@ class ProfileConditionsUI { this._os = value; } - prepare(conditionGroups) { + prepare(profileIndex, conditionGroups) { + this._profileIndex = profileIndex; this._conditionGroupsContainer = document.querySelector('#profile-condition-groups'); this._addConditionGroupButton = document.querySelector('#profile-add-condition-group'); @@ -301,9 +303,9 @@ class ProfileConditionGroupUI { } prepare(conditionGroup) { - this._node = this._parent.instantiateTemplate('condition-group'); - this._conditionContainer = this._node.querySelector('.condition-list'); - this._addConditionButton = this._node.querySelector('.condition-add'); + this._node = this._parent.instantiateTemplate('profile-condition-group'); + this._conditionContainer = this._node.querySelector('.profile-condition-list'); + this._addConditionButton = this._node.querySelector('.profile-condition-add-button'); const conditions = conditionGroup.conditions; for (let i = 0, ii = conditions.length; i < ii; ++i) { @@ -403,6 +405,8 @@ class ProfileConditionUI { this._valueInputContainer = null; this._removeButton = null; this._mouseButton = null; + this._mouseButtonContainer = null; + this._menuButton = null; this._value = ''; this._kbmInputField = null; this._eventListeners = new EventListenerCollection(); @@ -432,14 +436,16 @@ class ProfileConditionUI { prepare(condition) { const {type, operator, value} = condition; - this._node = this._parent.parent.instantiateTemplate('condition'); - this._typeInput = this._node.querySelector('.condition-type'); + this._node = this._parent.parent.instantiateTemplate('profile-condition'); + this._typeInput = this._node.querySelector('.profile-condition-type'); this._typeOptionContainer = this._typeInput.querySelector('optgroup'); - this._operatorInput = this._node.querySelector('.condition-operator'); + this._operatorInput = this._node.querySelector('.profile-condition-operator'); this._operatorOptionContainer = this._operatorInput.querySelector('optgroup'); - this._valueInput = this._node.querySelector('.condition-input-inner'); - this._removeButton = this._node.querySelector('.condition-remove'); + this._valueInput = this._node.querySelector('.profile-condition-input'); + this._removeButton = this._node.querySelector('.profile-condition-remove'); this._mouseButton = this._node.querySelector('.mouse-button'); + this._mouseButtonContainer = this._node.querySelector('.mouse-button-container'); + this._menuButton = this._node.querySelector('.profile-condition-menu-button'); const operatorDetails = this._getOperatorDetails(type, operator); this._updateTypes(type); @@ -448,7 +454,8 @@ class ProfileConditionUI { this._eventListeners.addEventListener(this._typeInput, 'change', this._onTypeChange.bind(this), false); this._eventListeners.addEventListener(this._operatorInput, 'change', this._onOperatorChange.bind(this), false); - this._eventListeners.addEventListener(this._removeButton, 'click', this._onRemoveButtonClick.bind(this), false); + if (this._removeButton !== null) { this._eventListeners.addEventListener(this._removeButton, 'click', this._onRemoveButtonClick.bind(this), false); } + if (this._menuButton !== null) { this._eventListeners.addEventListener(this._menuButton, 'menuClosed', this._onMenuClosed.bind(this), false); } } cleanup() { @@ -528,7 +535,15 @@ class ProfileConditionUI { } _onRemoveButtonClick() { - this._parent.removeCondition(this); + this._removeSelf(); + } + + _onMenuClosed({detail: {action}}) { + switch (action) { + case 'delete': + this._removeSelf(); + break; + } } _getDescriptorTypes() { @@ -609,7 +624,7 @@ class ProfileConditionUI { } else { node.removeAttribute('step'); } - this._mouseButton.hidden = mouseButtonHidden; + this._mouseButtonContainer.hidden = mouseButtonHidden; for (const args of events) { this._inputEventListeners.addGeneric(...args); } @@ -626,4 +641,8 @@ class ProfileConditionUI { _normalizeValue(value, normalize) { return (normalize !== null ? normalize(value) : value); } + + _removeSelf() { + this._parent.removeCondition(this); + } } diff --git a/ext/bg/js/settings/profile-controller.js b/ext/bg/js/settings/profile-controller.js index af7b2318..149238b1 100644 --- a/ext/bg/js/settings/profile-controller.js +++ b/ext/bg/js/settings/profile-controller.js @@ -26,6 +26,7 @@ class ProfileController { this._settingsController = settingsController; this._modalController = modalController; this._profileConditionsUI = new ProfileConditionsUI(settingsController); + this._profileConditionsIndex = null; this._profileActiveSelect = null; this._profileTargetSelect = null; this._profileCopySourceSelect = null; @@ -38,9 +39,23 @@ class ProfileController { this._profileCopyConfirmButton = null; this._profileMoveUpButton = null; this._profileMoveDownButton = null; + this._profileEntryListContainer = null; + this._profileConditionsProfileName = null; this._profileRemoveModal = null; this._profileCopyModal = null; - this._optionsFull = null; + this._profileConditionsModal = null; + this._profileEntriesSupported = false; + this._profileEntryList = []; + this._profiles = []; + this._profileCurrent = 0; + } + + get profileCount() { + return this._profiles.length; + } + + get profileCurrentIndex() { + return this._profileCurrent; } async prepare() { @@ -59,160 +74,407 @@ class ProfileController { this._profileCopyConfirmButton = document.querySelector('#profile-copy-confirm-button'); this._profileMoveUpButton = document.querySelector('#profile-move-up-button'); this._profileMoveDownButton = document.querySelector('#profile-move-down-button'); + this._profileEntryListContainer = document.querySelector('#profile-entry-list'); + this._profileConditionsProfileName = document.querySelector('#profile-conditions-profile-name'); this._profileRemoveModal = this._modalController.getModal('profile-remove'); this._profileCopyModal = this._modalController.getModal('profile-copy'); + this._profileConditionsModal = this._modalController.getModal('profile-conditions'); - this._profileActiveSelect.addEventListener('change', this._onProfileActiveChange.bind(this), false); - this._profileTargetSelect.addEventListener('change', this._onProfileTargetChange.bind(this), false); - this._profileNameInput.addEventListener('change', this._onNameChanged.bind(this), false); - this._profileAddButton.addEventListener('click', this._onAdd.bind(this), false); - this._profileRemoveButton.addEventListener('click', this._onRemove.bind(this), false); - this._profileRemoveConfirmButton.addEventListener('click', this._onRemoveConfirm.bind(this), false); - this._profileCopyButton.addEventListener('click', this._onCopy.bind(this), false); - this._profileCopyConfirmButton.addEventListener('click', this._onCopyConfirm.bind(this), false); - this._profileMoveUpButton.addEventListener('click', this._onMove.bind(this, -1), false); - this._profileMoveDownButton.addEventListener('click', this._onMove.bind(this, 1), false); + this._profileEntriesSupported = (this._profileEntryListContainer !== null); + + if (this._profileActiveSelect !== null) { this._profileActiveSelect.addEventListener('change', this._onProfileActiveChange.bind(this), false); } + if (this._profileTargetSelect !== null) { this._profileTargetSelect.addEventListener('change', this._onProfileTargetChange.bind(this), false); } + if (this._profileNameInput !== null) { this._profileNameInput.addEventListener('change', this._onNameChanged.bind(this), false); } + if (this._profileAddButton !== null) { this._profileAddButton.addEventListener('click', this._onAdd.bind(this), false); } + if (this._profileRemoveButton !== null) { this._profileRemoveButton.addEventListener('click', this._onDelete.bind(this), false); } + if (this._profileRemoveConfirmButton !== null) { this._profileRemoveConfirmButton.addEventListener('click', this._onDeleteConfirm.bind(this), false); } + if (this._profileCopyButton !== null) { this._profileCopyButton.addEventListener('click', this._onCopy.bind(this), false); } + if (this._profileCopyConfirmButton !== null) { this._profileCopyConfirmButton.addEventListener('click', this._onCopyConfirm.bind(this), false); } + if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.addEventListener('click', this._onMove.bind(this, -1), false); } + if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.addEventListener('click', this._onMove.bind(this, 1), false); } this._settingsController.on('optionsChanged', this._onOptionsChanged.bind(this)); this._onOptionsChanged(); } - // Private + async moveProfile(profileIndex, offset) { + if (this._getProfile(profileIndex) === null) { return; } - async _onOptionsChanged() { - this._optionsFull = await this._settingsController.getOptionsFull(); + const profileIndexNew = Math.max(0, Math.min(this._profiles.length - 1, profileIndex + offset)); + if (profileIndex === profileIndexNew) { return; } + + await this.swapProfiles(profileIndex, profileIndexNew); + } + + async setProfileName(profileIndex, value) { + const profile = this._getProfile(profileIndex); + if (profile === null) { return; } + + profile.name = value; + this._updateSelectName(profileIndex, value); + + const profileEntry = this._getProfileEntry(profileIndex); + if (profileEntry !== null) { profileEntry.setName(value); } + + await this._settingsController.setGlobalSetting(`profiles[${profileIndex}].name`, value); + } + + async setDefaultProfile(profileIndex) { + const profile = this._getProfile(profileIndex); + if (profile === null) { return; } + + this._profileActiveSelect.value = `${profileIndex}`; + this._profileCurrent = profileIndex; + + const profileEntry = this._getProfileEntry(profileIndex); + if (profileEntry !== null) { profileEntry.setIsDefault(true); } + + await this._settingsController.setGlobalSetting('profileCurrent', profileIndex); + } + + async copyProfile(sourceProfileIndex, destinationProfileIndex) { + const sourceProfile = this._getProfile(sourceProfileIndex); + if (sourceProfile === null || !this._getProfile(destinationProfileIndex)) { return; } + + const options = clone(sourceProfile.options); + this._profiles[destinationProfileIndex].options = options; + + this._updateProfileSelectOptions(); + + const destinationProfileEntry = this._getProfileEntry(destinationProfileIndex); + if (destinationProfileEntry !== null) { + destinationProfileEntry.updateState(); + } + + await this._settingsController.modifyGlobalSettings([{ + action: 'set', + path: `profiles[${destinationProfileIndex}].options`, + value: options + }]); + + await this._settingsController.refresh(); + } + + async duplicateProfile(profileIndex) { + const profile = this._getProfile(profileIndex); + if (this.profile === null) { return; } + + // Create new profile + const newProfile = clone(profile); + newProfile.name = this._createCopyName(profile.name, this._profiles, 100); + + // Update state + const index = this._profiles.length; + this._profiles.push(newProfile); + this._addProfileEntry(index); + this._updateProfileSelectOptions(); + + // Modify settings + await this._settingsController.modifyGlobalSettings([{ + action: 'splice', + path: 'profiles', + start: index, + deleteCount: 0, + items: [newProfile] + }]); + + // Update profile index + this._settingsController.profileIndex = index; + } + + async deleteProfile(profileIndex) { + const profile = this._getProfile(profileIndex); + if (profile === null || this.profileCount <= 1) { return; } + + // Get indices + let profileCurrentNew = this._profileCurrent; + const settingsProfileIndex = this._settingsController.profileIndex; + + // Construct settings modifications + const modifications = [{ + action: 'splice', + path: 'profiles', + start: profileIndex, + deleteCount: 1, + items: [] + }]; + if (profileCurrentNew >= profileIndex) { + profileCurrentNew = Math.min(profileCurrentNew - 1, this._profiles.length - 1); + modifications.push({ + action: 'set', + path: 'profileCurrent', + value: profileCurrentNew + }); + } + + // Update state + this._profileCurrent = profileCurrentNew; + + this._profiles.splice(profileIndex, 1); + + if (profileIndex < this._profileEntryList.length) { + const profileEntry = this._profileEntryList[profileIndex]; + profileEntry.cleanup(); + this._profileEntryList.splice(profileIndex, 1); + + for (let i = profileIndex, ii = this._profileEntryList.length; i < ii; ++i) { + this._profileEntryList[i].index = i; + } + } + + const profileEntry2 = this._getProfileEntry(profileCurrentNew); + if (profileEntry2 !== null) { + profileEntry2.setIsDefault(true); + } + + this._updateProfileSelectOptions(); + + // Modify settings + await this._settingsController.modifyGlobalSettings(modifications); + + // Update profile index + if (settingsProfileIndex === profileIndex) { + this._settingsController.profileIndex = profileCurrentNew; + } + } + + async swapProfiles(index1, index2) { + const profile1 = this._getProfile(index1); + const profile2 = this._getProfile(index2); + if (profile1 === null || profile2 === null || index1 === index2) { return; } + + // Get swapped indices + const profileCurrent = this._profileCurrent; + const profileCurrentNew = this._getSwappedValue(profileCurrent, index1, index2); + + const settingsProfileIndex = this._settingsController.profileIndex; + const settingsProfileIndexNew = this._getSwappedValue(settingsProfileIndex, index1, index2); + + // Construct settings modifications + const modifications = [{ + action: 'swap', + path1: `profiles[${index1}]`, + path2: `profiles[${index2}]` + }]; + if (profileCurrentNew !== profileCurrent) { + modifications.push({ + action: 'set', + path: 'profileCurrent', + value: profileCurrentNew + }); + } + + // Update state + this._profileCurrent = profileCurrentNew; + + this._profiles[index1] = profile2; + this._profiles[index2] = profile1; + + const entry1 = this._getProfileEntry(index1); + const entry2 = this._getProfileEntry(index2); + if (entry1 !== null && entry2 !== null) { + entry1.index = index2; + entry2.index = index1; + this._swapDomNodes(entry1.node, entry2.node); + this._profileEntryList[index1] = entry2; + this._profileEntryList[index2] = entry1; + } + + this._updateProfileSelectOptions(); + + // Modify settings + await this._settingsController.modifyGlobalSettings(modifications); + + // Update profile index + if (settingsProfileIndex !== settingsProfileIndexNew) { + this._settingsController.profileIndex = settingsProfileIndexNew; + } + } + + openDeleteProfileModal(profileIndex) { + const profile = this._getProfile(profileIndex); + if (profile === null || this.profileCount <= 1) { return; } + + this._removeProfileNameElement.textContent = profile.name; + this._profileRemoveModal.node.dataset.profileIndex = profileIndex; + this._profileRemoveModal.setVisible(true); + } + + openCopyProfileModal(profileIndex) { + const profile = this._getProfile(profileIndex); + if (profile === null || this.profileCount <= 1) { return; } + + let copyFromIndex = this._profileCurrent; + if (copyFromIndex === profileIndex) { + if (profileIndex !== 0) { + copyFromIndex = 0; + } else if (this.profileCount > 1) { + copyFromIndex = 1; + } + } + + const profileIndexString = `${profileIndex}`; + for (const option of this._profileCopySourceSelect.querySelectorAll('option')) { + const {value} = option; + option.disabled = (value === profileIndexString); + } + this._profileCopySourceSelect.value = `${copyFromIndex}`; + + this._profileCopyModal.node.dataset.profileIndex = `${profileIndex}`; + this._profileCopyModal.setVisible(true); + } + + openProfileConditionsModal(profileIndex) { + const profile = this._getProfile(profileIndex); + if (profile === null) { return; } + + if (this._profileConditionsModal === null) { return; } + this._profileConditionsModal.setVisible(true); this._profileConditionsUI.cleanup(); + this._profileConditionsIndex = profileIndex; + this._profileConditionsUI.prepare(profileIndex, profile.conditionGroups); + if (this._profileConditionsProfileName !== null) { + this._profileConditionsProfileName.textContent = profile.name; + } + } - const {profiles, profileCurrent} = this._optionsFull; - const profileIndex = this._settingsController.profileIndex; + // Private + + async _onOptionsChanged() { + // Update state + const {profiles, profileCurrent} = await this._settingsController.getOptionsFull(); + this._profiles = profiles; + this._profileCurrent = profileCurrent; - this._updateSelectOptions(this._profileActiveSelect); - this._updateSelectOptions(this._profileTargetSelect); - this._updateSelectOptions(this._profileCopySourceSelect, [profileIndex]); + const settingsProfileIndex = this._settingsController.profileIndex; + const settingsProfile = this._getProfile(settingsProfileIndex); + + // Udpate UI + this._updateProfileSelectOptions(); this._profileActiveSelect.value = `${profileCurrent}`; - this._profileTargetSelect.value = `${profileIndex}`; + this._profileTargetSelect.value = `${settingsProfileIndex}`; + + if (this._profileRemoveButton !== null) { this._profileRemoveButton.disabled = (profiles.length <= 1); } + if (this._profileCopyButton !== null) { this._profileCopyButton.disabled = (profiles.length <= 1); } + if (this._profileMoveUpButton !== null) { this._profileMoveUpButton.disabled = (settingsProfileIndex <= 0); } + if (this._profileMoveDownButton !== null) { this._profileMoveDownButton.disabled = (settingsProfileIndex >= profiles.length - 1); } - this._profileRemoveButton.disabled = (profiles.length <= 1); - this._profileCopyButton.disabled = (profiles.length <= 1); - this._profileMoveUpButton.disabled = (profileIndex <= 0); - this._profileMoveDownButton.disabled = (profileIndex >= profiles.length - 1); + if (this._profileNameInput !== null && settingsProfile !== null) { this._profileNameInput.value = settingsProfile.name; } - if (profileIndex >= 0 && profileIndex < profiles.length) { - const currentProfile = profiles[profileIndex]; - this._profileNameInput.value = currentProfile.name; + // Update profile conditions + this._profileConditionsUI.cleanup(); + const conditionsProfile = this._getProfile(this._profileConditionsIndex !== null ? this._profileConditionsIndex : settingsProfileIndex); + if (conditionsProfile !== null) { + this._profileConditionsUI.prepare(settingsProfileIndex, conditionsProfile.conditionGroups); + } - const {conditionGroups} = currentProfile; - this._profileConditionsUI.prepare(conditionGroups); + // Udpate profile entries + for (const entry of this._profileEntryList) { + entry.cleanup(); + } + this._profileEntryList = []; + if (this._profileEntriesSupported) { + for (let i = 0, ii = profiles.length; i < ii; ++i) { + this._addProfileEntry(i); + } } } _onProfileActiveChange(e) { - const max = this._optionsFull.profiles.length; - const value = this._tryGetIntegerValue(e.currentTarget.value, 0, max); + const value = this._tryGetValidProfileIndex(e.currentTarget.value); if (value === null) { return; } - this._settingsController.setGlobalSetting('profileCurrent', value); + this.setDefaultProfile(value); } _onProfileTargetChange(e) { - const max = this._optionsFull.profiles.length; - const value = this._tryGetIntegerValue(e.currentTarget.value, 0, max); + const value = this._tryGetValidProfileIndex(e.currentTarget.value); if (value === null) { return; } this._settingsController.profileIndex = value; } _onNameChanged(e) { - const value = e.currentTarget.value; - const profileIndex = this._settingsController.profileIndex; - this._settingsController.setGlobalSetting(`profiles[${profileIndex}].name`, value); - this._updateSelectName(profileIndex, value); + this.setProfileName(this._settingsController.profileIndex, e.currentTarget.value); } _onAdd() { - this._addProfile(); + this.duplicateProfile(this._settingsController.profileIndex); } - _onRemove(e) { + _onDelete(e) { + const profileIndex = this._settingsController.profileIndex; if (e.shiftKey) { - return this._onRemoveConfirm(); + this.deleteProfile(profileIndex); + } else { + this.openDeleteProfileModal(profileIndex); } + } - if (this._optionsFull.profiles.length <= 1) { return; } + _onDeleteConfirm() { + const modal = this._profileRemoveModal; + modal.setVisible(false); + const {node} = modal; + let profileIndex = node.dataset.profileIndex; + delete node.dataset.profileIndex; - const profileIndex = this._settingsController.profileIndex; - const profile = this._optionsFull.profiles[profileIndex]; - this._removeProfileNameElement.textContent = profile.name; - this._profileRemoveModal.setVisible(true); - } + profileIndex = this._tryGetValidProfileIndex(profileIndex); + if (profileIndex === null) { return; } - _onRemoveConfirm() { - this._profileRemoveModal.setVisible(false); - if (this._optionsFull.profiles.length <= 1) { return; } - const profileIndex = this._settingsController.profileIndex; - this._removeProfile(profileIndex); + this.deleteProfile(profileIndex); } _onCopy() { - if (this._optionsFull.profiles.length <= 1) { return; } - - const {profiles, profileCurrent} = this._optionsFull; - const profileIndex = this._settingsController.profileIndex; - let copyFromIndex = profileCurrent; - if (copyFromIndex === profileIndex) { - if (profileIndex !== 0) { - copyFromIndex = 0; - } else if (profiles.length > 1) { - copyFromIndex = 1; - } - } - this._profileCopySourceSelect.value = `${copyFromIndex}`; - - this._profileCopyModal.setVisible(true); + this.openCopyProfileModal(this._settingsController.profileIndex); } _onCopyConfirm() { - this._profileCopyModal.setVisible(false); + const modal = this._profileCopyModal; + modal.setVisible(false); + const {node} = modal; + let destinationProfileIndex = node.dataset.profileIndex; + delete node.dataset.profileIndex; - const profileIndex = this._settingsController.profileIndex; - const max = this._optionsFull.profiles.length; - const index = this._tryGetIntegerValue(this._profileCopySourceSelect.value, 0, max); - if (index === null || index === profileIndex) { return; } + destinationProfileIndex = this._tryGetValidProfileIndex(destinationProfileIndex); + if (destinationProfileIndex === null) { return; } - this._copyProfile(profileIndex, index); + const sourceProfileIndex = this._tryGetValidProfileIndex(this._profileCopySourceSelect.value); + if (sourceProfileIndex === null) { return; } + + this.copyProfile(sourceProfileIndex, destinationProfileIndex); } _onMove(offset) { - const profileIndex = this._settingsController.profileIndex; - const max = this._optionsFull.profiles.length; - const profileIndexNew = Math.max(0, Math.min(max - 1, profileIndex + offset)); - if (profileIndex === profileIndexNew) { return; } - this._swapProfiles(profileIndex, profileIndexNew); - } - - _updateSelectOptions(select, disabled) { - const {profiles} = this._optionsFull; - const fragment = document.createDocumentFragment(); - for (let i = 0; i < profiles.length; ++i) { - const profile = profiles[i]; - const option = document.createElement('option'); - option.value = `${i}`; - option.textContent = profile.name; - option.disabled = (Array.isArray(disabled) && disabled.includes(i)); - fragment.appendChild(option); + this.moveProfile(this._settingsController.profileIndex, offset); + } + + _addProfileEntry(profileIndex) { + const profile = this._profiles[profileIndex]; + const node = this._settingsController.instantiateTemplate('profile-entry'); + const entry = new ProfileEntry(this, node); + this._profileEntryList.push(entry); + entry.prepare(profile, profileIndex); + this._profileEntryListContainer.appendChild(node); + } + + _updateProfileSelectOptions() { + for (const select of this._getAllProfileSelects()) { + const fragment = document.createDocumentFragment(); + for (let i = 0; i < this._profiles.length; ++i) { + const profile = this._profiles[i]; + const option = document.createElement('option'); + option.value = `${i}`; + option.textContent = profile.name; + fragment.appendChild(option); + } + select.textContent = ''; + select.appendChild(fragment); } - select.textContent = ''; - select.appendChild(fragment); } _updateSelectName(index, name) { - const selects = [ - this._profileActiveSelect, - this._profileTargetSelect, - this._profileCopySourceSelect - ]; const optionValue = `${index}`; - for (const select of selects) { + for (const select of this._getAllProfileSelects()) { for (const option of select.querySelectorAll('option')) { if (option.value === optionValue) { option.textContent = name; @@ -221,15 +483,23 @@ class ProfileController { } } - _tryGetIntegerValue(value, min, max) { - value = parseInt(value, 10); + _getAllProfileSelects() { + return [ + this._profileActiveSelect, + this._profileTargetSelect, + this._profileCopySourceSelect + ]; + } + + _tryGetValidProfileIndex(stringValue) { + if (typeof stringValue !== 'string') { return null; } + const intValue = parseInt(stringValue, 10); return ( - typeof value === 'number' && - Number.isFinite(value) && - Math.floor(value) === value && - value >= min && - value < max - ) ? value : null; + Number.isFinite(intValue) && + intValue >= 0 && + intValue < this.profileCount ? + intValue : null + ); } _createCopyName(name, profiles, maxUniqueAttempts) { @@ -270,98 +540,145 @@ class ProfileController { _getSwappedValue(currentValue, value1, value2) { if (currentValue === value1) { return value2; } if (currentValue === value2) { return value1; } - return null; + return currentValue; } - async _addProfile() { - const profileIndex = this._settingsController.profileIndex; - const profiles = this._optionsFull.profiles; - const profile = profiles[profileIndex]; - const newProfile = clone(profile); - newProfile.name = this._createCopyName(profile.name, profiles, 100); + _getProfile(profileIndex) { + return (profileIndex >= 0 && profileIndex < this._profiles.length ? this._profiles[profileIndex] : null); + } - const index = profiles.length; - await this._settingsController.modifyGlobalSettings([{ - action: 'splice', - path: 'profiles', - start: index, - deleteCount: 0, - items: [newProfile] - }]); + _getProfileEntry(profileIndex) { + return (profileIndex >= 0 && profileIndex < this._profileEntryList.length ? this._profileEntryList[profileIndex] : null); + } - this._settingsController.profileIndex = index; + _swapDomNodes(node1, node2) { + const parent1 = node1.parentNode; + const parent2 = node2.parentNode; + const next1 = node1.nextSibling; + const next2 = node2.nextSibling; + if (node2 !== next1) { parent1.insertBefore(node2, next1); } + if (node1 !== next2) { parent2.insertBefore(node1, next2); } } +} - async _removeProfile(index) { - const {profiles, profileCurrent} = this._optionsFull; - let newProfileCurrent = profileCurrent; - const modifications = [{ - action: 'splice', - path: 'profiles', - start: index, - deleteCount: 1, - items: [] - }]; +class ProfileEntry { + constructor(profileController, node) { + this._profileController = profileController; + this._node = node; + this._profile = null; + this._index = 0; + this._isDefaultRadio = null; + this._nameInput = null; + this._countLink = null; + this._countText = null; + this._menuButton = null; + this._eventListeners = new EventListenerCollection(); + } - if (profileCurrent >= index) { - newProfileCurrent = Math.min(newProfileCurrent - 1, profiles.length - 1); - modifications.push({ - action: 'set', - path: 'profileCurrent', - value: newProfileCurrent - }); - } + get index() { + return this._index; + } - const profileIndex = this._settingsController.profileIndex; + set index(value) { + this._index = value; + } - await this._settingsController.modifyGlobalSettings(modifications); + get node() { + return this._node; + } - if (profileIndex === index) { - this._settingsController.profileIndex = newProfileCurrent; - } else { - await this._onOptionsChanged(); + prepare(profile, index) { + this._profile = profile; + this._index = index; + + const node = this._node; + this._isDefaultRadio = node.querySelector('.profile-entry-is-default-radio'); + this._nameInput = node.querySelector('.profile-entry-name-input'); + this._countLink = node.querySelector('.profile-entry-condition-count-link'); + this._countText = node.querySelector('.profile-entry-condition-count'); + this._menuButton = node.querySelector('.profile-entry-menu-button'); + + this.updateState(); + + this._eventListeners.addEventListener(this._isDefaultRadio, 'change', this._onIsDefaultRadioChange.bind(this), false); + this._eventListeners.addEventListener(this._nameInput, 'input', this._onNameInputInput.bind(this), false); + this._eventListeners.addEventListener(this._countLink, 'click', this._onConditionsCountLinkClick.bind(this), false); + this._eventListeners.addEventListener(this._menuButton, 'menuOpened', this._onMenuOpened.bind(this), false); + this._eventListeners.addEventListener(this._menuButton, 'menuClosed', this._onMenuClosed.bind(this), false); + } + + cleanup() { + this._eventListeners.removeAllEventListeners(); + if (this._node.parentNode !== null) { + this._node.parentNode.removeChild(this._node); } } - async _swapProfiles(index1, index2) { - const {profileCurrent} = this._optionsFull; + setName(value) { + if (this._nameInput.value === value) { return; } + this._nameInput.value = value; + } - const modifications = [{ - action: 'swap', - path1: `profiles[${index1}]`, - path2: `profiles[${index2}]` - }]; + setIsDefault(value) { + this._isDefaultRadio.checked = value; + } - const newProfileCurrent = this._getSwappedValue(profileCurrent, index1, index2); - if (newProfileCurrent !== profileCurrent) { - modifications.push({ - action: 'set', - path: 'profileCurrent', - value: newProfileCurrent - }); - } + updateState() { + this._nameInput.value = this._profile.name; + this._countText.textContent = this._profile.conditionGroups.length; + this._isDefaultRadio.checked = (this._index === this._profileController.profileCurrentIndex); + } - const profileIndex = this._settingsController.profileIndex; - const newProfileIndex = this._getSwappedValue(profileIndex, index1, index2); + // Private - await this._settingsController.modifyGlobalSettings(modifications); + _onIsDefaultRadioChange(e) { + if (!e.currentTarget.checked) { return; } + this._profileController.setDefaultProfile(this._index); + } - if (profileIndex !== newProfileIndex) { - this._settingsController.profileIndex = newProfileIndex; - } + _onNameInputInput(e) { + const name = e.currentTarget.value; + this._profileController.setProfileName(this._index, name); } - async _copyProfile(index, copyFromIndex) { - const profiles = this._optionsFull.profiles; - const copyFromProfile = profiles[copyFromIndex]; - const options = clone(copyFromProfile.options); + _onConditionsCountLinkClick() { + this._profileController.openProfileConditionsModal(this._index); + } - await this._settingsController.modifyGlobalSettings([{ - action: 'set', - path: `profiles[${index}].options`, - value: options - }]); + _onMenuOpened({detail: {menu}}) { + const count = this._profileController.profileCount; + this._setMenuActionEnabled(menu, 'moveUp', this._index > 0); + this._setMenuActionEnabled(menu, 'moveDown', this._index < count - 1); + this._setMenuActionEnabled(menu, 'copyFrom', count > 1); + this._setMenuActionEnabled(menu, 'delete', count > 1); + } - await this._settingsController.refresh(); + _onMenuClosed({detail: {action}}) { + switch (action) { + case 'moveUp': + this._profileController.moveProfile(this._index, -1); + break; + case 'moveDown': + this._profileController.moveProfile(this._index, 1); + break; + case 'copyFrom': + this._profileController.openCopyProfileModal(this._index); + break; + case 'editConditions': + this._profileController.openProfileConditionsModal(this._index); + break; + case 'duplicate': + this._profileController.duplicateProfile(this._index); + break; + case 'delete': + this._profileController.openDeleteProfileModal(this._index); + break; + } + } + + _setMenuActionEnabled(menu, action, enabled) { + const element = menu.querySelector(`[data-menu-action="${action}"]`); + if (element === null) { return; } + element.disabled = !enabled; } } diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 0679bcba..e385d074 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -69,7 +69,7 @@ If no conditions are specified, the profile will only be used if it is selected as the Active profile.

-
+
@@ -112,20 +112,20 @@
-