From 526eab2467823d6e336b3f0f0f67fa65def3e051 Mon Sep 17 00:00:00 2001 From: Tomek Date: Sat, 2 Apr 2016 17:51:24 +0200 Subject: [PATCH] #9 Add confirmation when closing a group --- src/data/action_creators.js | 6 +++++ src/data/assets/css/groupspanel.css | 22 ++++++++++++++++ src/data/components/group.js | 35 +++++++++++++++++++++++-- src/data/components/groupcontrols.js | 38 +++++++++++++++++++++++----- src/data/components/grouplist.js | 5 +++- src/data/groupspanel.js | 4 +++ src/data/reducer.js | 6 +++-- src/index.js | 36 +++++++++++++++++++++++++- src/lib/tabmanager.js | 4 +++ src/lib/utils.js | 15 +++++++++++ src/locale/en-US.properties | 6 +++++ src/package.json | 6 +++++ 12 files changed, 171 insertions(+), 12 deletions(-) diff --git a/src/data/action_creators.js b/src/data/action_creators.js index 1910412..6218c4d 100644 --- a/src/data/action_creators.js +++ b/src/data/action_creators.js @@ -4,5 +4,11 @@ const ActionCreators = { type: "TABGROUPS_RECEIVE", tabgroups: tabgroups }; + }, + setGroupCloseTimeout: function(timeout) { + return { + type: "GROUP_CLOSE_TIMEOUT_RECIEVE", + closeTimeout: timeout + }; } }; diff --git a/src/data/assets/css/groupspanel.css b/src/data/assets/css/groupspanel.css index 7a0a1db..174e51a 100644 --- a/src/data/assets/css/groupspanel.css +++ b/src/data/assets/css/groupspanel.css @@ -117,6 +117,18 @@ ul, li { top: 6px; } + .group.closing .group-controls .group-close, + .group.closing:hover .group-controls .group-close, + .group.closing .group-controls .fa-chevron-down, + .group.closing .group-controls .group-edit { + display: none; + } + + .group.closing .group-title { + color: #CCC; + text-decoration: line-through; + } + .group:hover .group-title { padding-right: 55px; } @@ -130,3 +142,13 @@ ul, li { .group:hover .group-controls .group-edit { display: inline-block; } + + .group.closing .group-controls .group-close-undo-timer, + .group.closing .group-controls .group-close-undo { + color: #666; + } + + .group.closing .group-controls:hover .group-close-undo-timer, + .group.closing .group-controls:hover .group-close-undo { + color: #000; + } diff --git a/src/data/components/group.js b/src/data/components/group.js index e382f04..9654305 100644 --- a/src/data/components/group.js +++ b/src/data/components/group.js @@ -1,5 +1,6 @@ const Group = React.createClass({ propTypes: { + closeTimeout: React.PropTypes.number, group: React.PropTypes.object.isRequired, onGroupClick: React.PropTypes.func, onGroupDrop: React.PropTypes.func, @@ -13,6 +14,8 @@ const Group = React.createClass({ getInitialState: function() { return { + closeTimer: this.props.closeTimeout, + closing: false, editing: false, expanded: false, draggingOverCounter: 0, @@ -54,6 +57,7 @@ const Group = React.createClass({ let groupClasses = classNames({ active: this.props.group.active, editing: this.state.editing, + closing: this.state.closing, draggingOver: this.state.draggingOverCounter !== 0, dragSourceGroup: this.state.dragSourceGroup, expanded: this.state.expanded, @@ -78,13 +82,16 @@ const Group = React.createClass({ React.createElement( GroupControls, { + closeTimer: this.state.closeTimer, + closing: this.state.closing, editing: this.state.editing, expanded: this.state.expanded, onClose: this.handleGroupCloseClick, onEdit: this.handleGroupEditClick, onEditAbort: this.handleGroupEditAbortClick, onEditSave: this.handleGroupEditSaveClick, - onExpand: this.handleGroupExpandClick + onExpand: this.handleGroupExpandClick, + onUndoCloseClick: this.handleGroupCloseAbortClick } ) ), @@ -104,7 +111,24 @@ const Group = React.createClass({ handleGroupCloseClick: function(event) { event.stopPropagation(); - this.props.onGroupCloseClick(this.props.group.id); + this.setState({editing: false}); + this.setState({closing: true}); + this.setState({closeTimer: this.props.closeTimeout}); + + let group = this; + + if (this.props.closeTimeout == 0) { + group.props.onGroupCloseClick(group.props.group.id); + return; + } + + let timer = setInterval(function() { + group.setState({closeTimer: --group.state.closeTimer}); + if (group.state.closeTimer <= 0) { + group.props.onGroupCloseClick(group.props.group.id); + clearInterval(timer); + } + }, 1000); }, handleGroupClick: function(event) { @@ -184,5 +208,12 @@ const Group = React.createClass({ } return false; + }, + + handleGroupCloseAbortClick: function(event) { + event.stopPropagation(); + + this.setState({closing: false}); + this.setState({closeTimer: this.props.closeTimeout}); } }); diff --git a/src/data/components/groupcontrols.js b/src/data/components/groupcontrols.js index 3aec071..4085788 100644 --- a/src/data/components/groupcontrols.js +++ b/src/data/components/groupcontrols.js @@ -1,17 +1,19 @@ const GroupControls = React.createClass({ propTypes: { + closeTimer: React.PropTypes.number, expanded: React.PropTypes.bool.isRequired, onClose: React.PropTypes.func, onEdit: React.PropTypes.func, onEditAbort: React.PropTypes.func, onEditSave: React.PropTypes.func, - onExpand: React.PropTypes.func + onExpand: React.PropTypes.func, + onUndoCloseClick: React.PropTypes.func }, - render: function() { - let editControls; + getEditControls: function() { + let controls; if (this.props.editing) { - editControls = [ + controls = [ React.DOM.i({ className: "group-edit fa fa-fw fa-check", onClick: this.props.onEditSave @@ -22,12 +24,36 @@ const GroupControls = React.createClass({ }) ]; } else { - editControls = React.DOM.i({ + controls = React.DOM.i({ className: "group-edit fa fa-fw fa-pencil", onClick: this.props.onEdit }); } + return controls; + }, + + getClosingControls: function() { + return [ + React.DOM.span( + {className: "group-close-undo-timer"}, + this.props.closeTimer + ), + React.DOM.i({ + className: "group-close-undo fa fa-fw fa-undo", + onClick: this.props.onUndoCloseClick + }) + ]; + }, + + render: function() { + let groupControls; + if (this.props.closing) { + groupControls = this.getClosingControls(); + } else { + groupControls = this.getEditControls(); + } + let expanderClasses = classNames({ "group-expand": true, "fa": true, @@ -40,7 +66,7 @@ const GroupControls = React.createClass({ { className: "group-controls" }, - editControls, + groupControls, React.DOM.i({ className: "group-close fa fa-fw fa-times", onClick: this.props.onClose diff --git a/src/data/components/grouplist.js b/src/data/components/grouplist.js index 9a62052..55a8e72 100644 --- a/src/data/components/grouplist.js +++ b/src/data/components/grouplist.js @@ -2,6 +2,7 @@ const GroupList = (() => { const GroupListStandalone = React.createClass({ propTypes: { groups: React.PropTypes.object.isRequired, + closeTimeout: React.PropTypes.number, onGroupAddClick: React.PropTypes.func, onGroupAddDrop: React.PropTypes.func, onGroupClick: React.PropTypes.func, @@ -25,6 +26,7 @@ const GroupList = (() => { return React.createElement(Group, { key: group.id, group: group, + closeTimeout: this.props.closeTimeout, onGroupClick: this.props.onGroupClick, onGroupDrop: this.props.onGroupDrop, onGroupCloseClick: this.props.onGroupCloseClick, @@ -48,7 +50,8 @@ const GroupList = (() => { return ReactRedux.connect((state) => { return { - groups: state.get("tabgroups") + groups: state.get("tabgroups"), + closeTimeout: state.get("closeTimeout") }; }, ActionCreators)(GroupListStandalone); })(); diff --git a/src/data/groupspanel.js b/src/data/groupspanel.js index 574bfb5..7cbb96b 100644 --- a/src/data/groupspanel.js +++ b/src/data/groupspanel.js @@ -70,3 +70,7 @@ document.addEventListener("DOMContentLoaded", () => { addon.port.on("Groups:Changed", (tabgroups) => { store.dispatch(ActionCreators.setTabgroups(tabgroups)); }); + +addon.port.on("Groups:CloseTimeoutChanged", (timeout) => { + store.dispatch(ActionCreators.setGroupCloseTimeout(timeout)); +}); diff --git a/src/data/reducer.js b/src/data/reducer.js index b68a3ca..76937ff 100644 --- a/src/data/reducer.js +++ b/src/data/reducer.js @@ -1,12 +1,14 @@ const INITIAL_STATE = Immutable.fromJS({ - tabgroups: [] + tabgroups: [], + closeTimeout: 0 }); const Reducer = function(state = INITIAL_STATE, action) { switch (action.type) { case "TABGROUPS_RECEIVE": return state.set("tabgroups", Immutable.fromJS(action.tabgroups)); + case "GROUP_CLOSE_TIMEOUT_RECIEVE": + return state.set("closeTimeout", action.closeTimeout); } - return state; }; diff --git a/src/index.js b/src/index.js index eabb78b..dbd38bc 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,7 @@ TabGroups.prototype = { bindEvents: function() { this.bindHotkeyPreference(); + this.bindGroupPreference(); this.bindPanelButtonEvents(); this.bindPanelEvents(); this.bindTabEvents(); @@ -50,7 +51,8 @@ TabGroups.prototype = { l10n: Utils.getL10nStrings([ "add_group", "unnamed_group" - ]) + ]), + groupCloseTimeout: Prefs.prefs.groupCloseTimeout } }); }, @@ -161,6 +163,16 @@ TabGroups.prototype = { }); }, + bindGroupPreference: function() { + let emitCloseTimeoutChange = () => { + this._groupsPanel.port.emit("Groups:CloseTimeoutChanged", Prefs.prefs.groupCloseTimeout); + }; + + Prefs.on("groupCloseTimeout", emitCloseTimeoutChange); + + emitCloseTimeoutChange(); + }, + bindPanelButtonEvents: function() { this._panelButton.on("change", (state) => { if (!state.checked) { @@ -231,6 +243,21 @@ TabGroups.prototype = { }, onGroupClose: function(event) { + let groupTabCount = this._tabs.getGroupTabCount( + this._getTabBrowser(), + event.groupID + ); + + if (groupTabCount > 0) { + let closeGroupConfirmed = this._closeGroupConfirmation(); + + this._groupsPanel.show({position: this._panelButton}); + + if (!closeGroupConfirmed) { + return; + } + } + this._tabs.closeGroup( this._getWindow(), this._getTabBrowser(), @@ -283,6 +310,13 @@ TabGroups.prototype = { _getTabBrowser: function() { return TabsUtils.getTabBrowser(this._getWindow()); + }, + + _closeGroupConfirmation: function() { + let promptTitle = _("close_group_prompt_title"); + let promptMessage = _("close_group_prompt_message"); + + return Utils.confirm(promptTitle, promptMessage); } }; diff --git a/src/lib/tabmanager.js b/src/lib/tabmanager.js index 70ee737..fc26438 100644 --- a/src/lib/tabmanager.js +++ b/src/lib/tabmanager.js @@ -206,6 +206,10 @@ TabManager.prototype = { return recentlyAddedGroup; }, + getGroupTabCount: function(tabBrowser, groupID) { + return this._storage.getTabIndexesByGroup(tabBrowser, groupID).length; + }, + /** * Closes a group and all attached tabs * diff --git a/src/lib/utils.js b/src/lib/utils.js index aa41e26..eeffb0e 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,5 +1,6 @@ const _ = require("sdk/l10n").get; const PrefService = require("sdk/preferences/service"); +const {Cc, Ci} = require("chrome"); /** * Returns an object of translated strings for the use in the frontend. @@ -37,3 +38,17 @@ exports.themeSwitch = function(object) { return retValue; }; + +/** + * Confirm prompt service. + * + * @param {String} title - title of confirm prompt + * @param {String} message - question + * @returns {boolean} TRUE if user confirm, FALSE otherwise + */ +exports.confirm = function(title, message) { + let prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService); + + return prompts.confirm(null, title, message); +}; diff --git a/src/locale/en-US.properties b/src/locale/en-US.properties index 03df40b..385c053 100644 --- a/src/locale/en-US.properties +++ b/src/locale/en-US.properties @@ -1,6 +1,12 @@ bindPanoramaShortcut_title = Listen to Ctrl/Cmd-Shift-E enableAlphabeticSort_title = Enable alphabetic sorting +groupUndoCloseTimeout_title = Closing group timeout +groupUndoCloseTimeout_description = Delay before close group in seconds add_group = Create new group +close_group_prompt_title = Close group +close_group_prompt_message = Do you want to close this group? panelButton_label = Group Tabs unnamed_group = Unnamed Group +close_group_prompt_title = Close group +close_group_prompt_message = Do you want to close this group? diff --git a/src/package.json b/src/package.json index 9833295..df075b3 100644 --- a/src/package.json +++ b/src/package.json @@ -29,6 +29,12 @@ "title": "Enable alphabetic sorting", "type": "bool", "value": false + }, + { + "name": "groupCloseTimeout", + "title": "Closing group timeout", + "type": "integer", + "value": 0 } ] }