FEATURE: fullscreen composer mode on desktop

Adds keyboard shortcut and icon that allows expanding composer to full screen.
This commit is contained in:
Joe 2018-10-15 10:59:49 +08:00 committed by Sam
parent 57b52cd1de
commit 2acb885c72
11 changed files with 209 additions and 90 deletions

View File

@ -13,7 +13,7 @@ export default Ember.Component.extend({
_yourselfConfirm: null, _yourselfConfirm: null,
similarTopics: null, similarTopics: null,
hidden: Ember.computed.not("composer.viewOpen"), hidden: Ember.computed.not("composer.viewOpenOrFullscreen"),
didInsertElement() { didInsertElement() {
this._super(); this._super();

View File

@ -4,18 +4,28 @@ export default Ember.Component.extend({
tagName: "", tagName: "",
@computed("composeState") @computed("composeState")
title(composeState) { toggleTitle(composeState) {
if (composeState === "draft" || composeState === "saving") { return composeState === "draft" || composeState === "saving"
return "composer.abandon"; ? "composer.abandon"
} : "composer.collapse";
return "composer.collapse"; },
@computed("composeState")
fullscreenTitle(composeState) {
return composeState === "fullscreen"
? "composer.exit_fullscreen"
: "composer.enter_fullscreen";
}, },
@computed("composeState") @computed("composeState")
toggleIcon(composeState) { toggleIcon(composeState) {
if (composeState === "draft" || composeState === "saving") { return composeState === "draft" || composeState === "saving"
return "times"; ? "times"
} : "chevron-down";
return "chevron-down"; },
@computed("composeState")
fullscreenIcon(composeState) {
return composeState === "fullscreen" ? "compress" : "expand";
} }
}); });

View File

@ -231,7 +231,7 @@ export default Ember.Controller.extend({
@computed("model.composeState", "model.creatingTopic") @computed("model.composeState", "model.creatingTopic")
popupMenuOptions(composeState) { popupMenuOptions(composeState) {
if (composeState === "open") { if (composeState === "open" || composeState === "fullscreen") {
let options = []; let options = [];
options.push( options.push(
@ -386,7 +386,10 @@ export default Ember.Controller.extend({
) { ) {
this.close(); this.close();
} else { } else {
if (this.get("model.composeState") === Composer.OPEN) { if (
this.get("model.composeState") === Composer.OPEN ||
this.get("model.composeState") === Composer.FULLSCREEN
) {
this.shrink(); this.shrink();
} else { } else {
this.cancelComposer(); this.cancelComposer();
@ -396,6 +399,11 @@ export default Ember.Controller.extend({
return false; return false;
}, },
fullscreenComposer() {
this.toggleFullscreen();
return false;
},
// Import a quote from the post // Import a quote from the post
importQuote(toolbarEvent) { importQuote(toolbarEvent) {
const postStream = this.get("topic.postStream"); const postStream = this.get("topic.postStream");
@ -457,7 +465,7 @@ export default Ember.Controller.extend({
return; return;
} }
if (this.get("model.viewOpen")) { if (this.get("model.viewOpen") || this.get("model.viewFullscreen")) {
this.shrink(); this.shrink();
} }
}, },
@ -881,6 +889,10 @@ export default Ember.Controller.extend({
} }
]); ]);
} else { } else {
// in case the composer is
// cancelled while in fullscreen
$("html").removeClass("fullscreen-composer");
// it is possible there is some sort of crazy draft with no body ... just give up on it // it is possible there is some sort of crazy draft with no body ... just give up on it
this.destroyDraft(); this.destroyDraft();
this.get("model").clearState(); this.get("model").clearState();
@ -947,6 +959,15 @@ export default Ember.Controller.extend({
this.set("model.composeState", Composer.DRAFT); this.set("model.composeState", Composer.DRAFT);
}, },
toggleFullscreen() {
this._saveDraft();
if (this.get("model.composeState") === Composer.FULLSCREEN) {
this.set("model.composeState", Composer.OPEN);
} else {
this.set("model.composeState", Composer.FULLSCREEN);
}
},
close() { close() {
this.setProperties({ model: null, lastValidatedAt: null }); this.setProperties({ model: null, lastValidatedAt: null });
}, },

View File

@ -65,6 +65,7 @@ const bindings = {
"shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic "shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic
"shift+u": { handler: "goToUnreadPost" }, "shift+u": { handler: "goToUnreadPost" },
"shift+z shift+z": { handler: "logout" }, "shift+z shift+z": { handler: "logout" },
"shift+f11": { handler: "fullscreenComposer" },
t: { postAction: "replyAsNewTopic" }, t: { postAction: "replyAsNewTopic" },
u: { handler: "goBack", anonymous: true }, u: { handler: "goBack", anonymous: true },
"x r": { "x r": {
@ -212,6 +213,13 @@ export default {
} }
}, },
fullscreenComposer() {
const composer = this.container.lookup("controller:composer");
if (composer.get("model")) {
composer.toggleFullscreen();
}
},
pinUnpinTopic() { pinUnpinTopic() {
this.container.lookup("controller:topic").togglePinnedState(); this.container.lookup("controller:topic").togglePinnedState();
}, },

View File

@ -26,6 +26,7 @@ const CLOSED = "closed",
SAVING = "saving", SAVING = "saving",
OPEN = "open", OPEN = "open",
DRAFT = "draft", DRAFT = "draft",
FULLSCREEN = "fullscreen",
// When creating, these fields are moved into the post model from the composer model // When creating, these fields are moved into the post model from the composer model
_create_serializer = { _create_serializer = {
raw: "reply", raw: "reply",
@ -144,15 +145,24 @@ const Composer = RestModel.extend({
viewOpen: Em.computed.equal("composeState", OPEN), viewOpen: Em.computed.equal("composeState", OPEN),
viewDraft: Em.computed.equal("composeState", DRAFT), viewDraft: Em.computed.equal("composeState", DRAFT),
viewFullscreen: Em.computed.equal("composeState", FULLSCREEN),
viewOpenOrFullscreen: Em.computed.or("viewOpen", "viewFullscreen"),
composeStateChanged: function() { composeStateChanged: function() {
var oldOpen = this.get("composerOpened"); let oldOpen = this.get("composerOpened"),
elem = $("html");
if (this.get("composeState") === FULLSCREEN) {
elem.addClass("fullscreen-composer");
} else {
elem.removeClass("fullscreen-composer");
}
if (this.get("composeState") === OPEN) { if (this.get("composeState") === OPEN) {
this.set("composerOpened", oldOpen || new Date()); this.set("composerOpened", oldOpen || new Date());
} else { } else {
if (oldOpen) { if (oldOpen) {
var oldTotal = this.get("composerTotalOpened") || 0; let oldTotal = this.get("composerTotalOpened") || 0;
this.set("composerTotalOpened", oldTotal + (new Date() - oldOpen)); this.set("composerTotalOpened", oldTotal + (new Date() - oldOpen));
} }
this.set("composerOpened", null); this.set("composerOpened", null);
@ -160,9 +170,8 @@ const Composer = RestModel.extend({
}.observes("composeState"), }.observes("composeState"),
composerTime: function() { composerTime: function() {
var total = this.get("composerTotalOpened") || 0; let total = this.get("composerTotalOpened") || 0,
oldOpen = this.get("composerOpened");
var oldOpen = this.get("composerOpened");
if (oldOpen) { if (oldOpen) {
total += new Date() - oldOpen; total += new Date() - oldOpen;
} }
@ -183,7 +192,7 @@ const Composer = RestModel.extend({
// view detected user is typing // view detected user is typing
typing: _.throttle( typing: _.throttle(
function() { function() {
var typingTime = this.get("typingTime") || 0; let typingTime = this.get("typingTime") || 0;
this.set("typingTime", typingTime + 100); this.set("typingTime", typingTime + 100);
}, },
100, 100,
@ -1041,6 +1050,7 @@ Composer.reopenClass({
SAVING, SAVING,
OPEN, OPEN,
DRAFT, DRAFT,
FULLSCREEN,
// The actions the composer can take // The actions the composer can take
CREATE_TOPIC, CREATE_TOPIC,

View File

@ -10,5 +10,13 @@
class="toggler" class="toggler"
icon=toggleIcon icon=toggleIcon
action=toggleComposer action=toggleComposer
title=title}} title=toggleTitle}}
{{#unless site.mobileView}}
{{flat-button
class="toggle-fullscreen"
icon=fullscreenIcon
action=toggleFullscreen
title=fullscreenTitle}}
{{/unless}}
</div> </div>

View File

@ -9,73 +9,77 @@
{{composer-messages composer=model {{composer-messages composer=model
messageCount=messageCount messageCount=messageCount
addLinkLookup="addLinkLookup"}} addLinkLookup="addLinkLookup"}}
{{#if model.viewOpen}} {{#if model.viewOpenOrFullscreen}}
<div class="reply-area {{if canEditTags 'with-tags'}}"> <div class="reply-area {{if canEditTags 'with-tags'}}">
<div class='composer-fields'> <div class='composer-fields'>
{{plugin-outlet name="composer-open" args=(hash model=model)}} {{plugin-outlet name="composer-open" args=(hash model=model)}}
<div class='reply-to'> <div class='reply-to'>
<div class="reply-details"> {{#unless model.viewFullscreen}}
{{composer-action-title model=model canWhisper=canWhisper tabindex=8}} <div class="reply-details">
{{composer-action-title model=model canWhisper=canWhisper tabindex=8}}
{{#unless site.mobileView}} {{#unless site.mobileView}}
{{#if whisperOrUnlistTopicText}} {{#if whisperOrUnlistTopicText}}
<span class='whisper'>({{whisperOrUnlistTopicText}})</span> <span class='whisper'>({{whisperOrUnlistTopicText}})</span>
{{/if}} {{/if}}
{{#if model.noBump}} {{#if model.noBump}}
<span class="no-bump">{{d-icon "anchor"}}</span> <span class="no-bump">{{d-icon "anchor"}}</span>
{{/if}} {{/if}}
{{/unless}} {{/unless}}
{{#if canEdit}} {{#if canEdit}}
{{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason key="composer.show_edit_reason" class="display-edit-reason"}} {{#link-to-input onClick=(action "displayEditReason") showInput=showEditReason key="composer.show_edit_reason" class="display-edit-reason"}}
{{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} {{text-field value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}}
{{/link-to-input}} {{/link-to-input}}
{{/if}} {{/if}}
</div> </div>
{{/unless}}
{{composer-toggles composeState=model.composeState {{composer-toggles composeState=model.composeState
toggleComposer=(action "toggle") toggleComposer=(action "toggle")
toggleToolbar=(action "toggleToolbar")}} toggleToolbar=(action "toggleToolbar")
toggleFullscreen=(action "fullscreenComposer")}}
</div> </div>
{{#unless model.viewFullscreen}}
{{#if model.canEditTitle}}
{{#if model.creatingPrivateMessage}}
<div class='user-selector'>
{{composer-user-selector topicId=topicModel.id
usernames=model.targetUsernames
hasGroups=model.hasTargetGroups
focusTarget=focusTarget
class="users-input"}}
{{#if showWarning}}
<label class='add-warning'>
{{input type="checkbox" checked=model.isWarning tabindex="3"}}
{{i18n "composer.add_warning"}}
</label>
{{/if}}
</div>
{{/if}}
{{#if model.canEditTitle}} <div class="title-and-category {{if showPreview 'with-preview'}}">
{{#if model.creatingPrivateMessage}}
<div class='user-selector'> {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}}
{{composer-user-selector topicId=topicModel.id
usernames=model.targetUsernames {{#if model.showCategoryChooser}}
hasGroups=model.hasTargetGroups <div class="category-input">
focusTarget=focusTarget {{category-chooser
class="users-input"}} fullWidthOnMobile=true
{{#if showWarning}} value=model.categoryId
<label class='add-warning'> scopedCategoryId=scopedCategoryId
{{input type="checkbox" checked=model.isWarning tabindex="3"}} tabindex="3"}}
{{i18n "composer.add_warning"}} {{popup-input-tip validation=categoryValidation}}
</label> </div>
{{/if}}
{{#if canEditTags}}
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}}
{{popup-input-tip validation=tagValidation}}
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}
<div class="title-and-category {{if showPreview 'with-preview'}}"> {{plugin-outlet name="composer-fields" args=(hash model=model)}}
{{/unless}}
{{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}}
{{#if model.showCategoryChooser}}
<div class="category-input">
{{category-chooser
fullWidthOnMobile=true
value=model.categoryId
scopedCategoryId=scopedCategoryId
tabindex="3"}}
{{popup-input-tip validation=categoryValidation}}
</div>
{{/if}}
{{#if canEditTags}}
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}}
{{popup-input-tip validation=tagValidation}}
{{/if}}
</div>
{{/if}}
{{plugin-outlet name="composer-fields" args=(hash model=model)}}
</div> </div>
@ -104,21 +108,24 @@
{{plugin-outlet name="composer-fields-below" args=(hash model=model)}} {{plugin-outlet name="composer-fields-below" args=(hash model=model)}}
<div class='save-or-cancel'> <div class='save-or-cancel'>
{{composer-save-button action=(action "save") {{#unless model.viewFullscreen}}
icon=model.saveIcon {{composer-save-button action=(action "save")
label=model.saveLabel icon=model.saveIcon
disableSubmit=disableSubmit}} label=model.saveLabel
{{#if site.mobileView}} disableSubmit=disableSubmit}}
<a href {{action "cancel"}} class='cancel' tabindex="6" title="{{i18n 'cancel'}}">
{{#if canEdit}} {{#if site.mobileView}}
{{d-icon "times"}} <a href {{action "cancel"}} class='cancel' tabindex="6" title="{{i18n 'cancel'}}">
{{else}} {{#if canEdit}}
{{d-icon "trash-o"}} {{d-icon "times"}}
{{/if}} {{else}}
</a> {{d-icon "trash-o"}}
{{else}} {{/if}}
<a href {{action "cancel"}} class='cancel' tabindex="6" >{{i18n 'cancel'}}</a> </a>
{{/if}} {{else}}
<a href {{action "cancel"}} class='cancel' tabindex="6" >{{i18n 'cancel'}}</a>
{{/if}}
{{/unless}}
{{#if site.mobileView}} {{#if site.mobileView}}
@ -165,7 +172,7 @@
</div> </div>
</div> </div>
{{else}} {{else}}
<div class='saving-text'> <div class='saving-text'>
{{#if model.createdPost}} {{#if model.createdPost}}
{{i18n 'composer.saved'}} <a class='permalink' href="{{unbound createdPost.url}}" {{action "viewNewReply"}}>{{i18n 'composer.view_new_post'}}</a> {{i18n 'composer.saved'}} <a class='permalink' href="{{unbound createdPost.url}}" {{action "viewNewReply"}}>{{i18n 'composer.view_new_post'}}</a>
@ -183,6 +190,7 @@
</div> </div>
{{composer-toggles composeState=model.composeState {{composer-toggles composeState=model.composeState
toggleFullscreen=(action "fullscreenComposer")
toggleComposer=(action "toggle") toggleComposer=(action "toggle")
toggleToolbar=(action "toggleToolbar")}} toggleToolbar=(action "toggleToolbar")}}

View File

@ -40,6 +40,7 @@
<h4>{{i18n 'keyboard_shortcuts_help.composing.title'}}</h4> <h4>{{i18n 'keyboard_shortcuts_help.composing.title'}}</h4>
<ul> <ul>
<li>{{{i18n 'keyboard_shortcuts_help.composing.return'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.composing.return'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.composing.fullscreen'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.application.create'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.application.create'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.actions.reply_as_new_topic'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.actions.reply_as_new_topic'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.actions.reply_topic'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.actions.reply_topic'}}}</li>

View File

@ -147,7 +147,7 @@
margin-left: auto; margin-left: auto;
margin-right: -5px; margin-right: -5px;
button { button {
padding: 0 8px; padding: 0 2px;
} }
} }
} }

View File

@ -203,3 +203,53 @@
flex-grow: 1; flex-grow: 1;
text-align: right; text-align: right;
} }
// fullscreen composer styles
.fullscreen-composer {
overflow: hidden;
.profiler-results {
display: none;
}
#reply-control {
&.fullscreen {
// important needed because of inline styles when height is changed manually with grippie
height: 100vh !important;
z-index: z("header") + 1;
.d-editor-preview-wrapper {
margin-top: 1%;
}
.reply-to {
border-bottom: 1px solid $primary-low;
padding-bottom: 3px;
margin: 0;
.composer-controls {
margin-right: 0;
}
}
.d-editor-textarea-wrapper {
border: none;
}
&.show-preview .d-editor-textarea-wrapper {
border-right: 1px solid $primary-low;
}
#draft-status,
#file-uploading {
margin-left: 0;
text-align: initial;
}
.composer-popup {
top: 30px;
}
&:before {
content: "";
background: $secondary;
width: 100%;
height: 100%;
position: fixed;
z-index: -1;
left: 0;
}
}
}
}

View File

@ -1427,6 +1427,8 @@ en:
help: "Markdown Editing Help" help: "Markdown Editing Help"
collapse: "minimize the composer panel" collapse: "minimize the composer panel"
abandon: "close composer and discard draft" abandon: "close composer and discard draft"
enter_fullscreen: "enter fullscreen composer"
exit_fullscreen: "exit fullscreen composer"
modal_ok: "OK" modal_ok: "OK"
modal_cancel: "Cancel" modal_cancel: "Cancel"
cant_send_pm: "Sorry, you can't send a message to %{username}." cant_send_pm: "Sorry, you can't send a message to %{username}."
@ -2613,6 +2615,7 @@ en:
composing: composing:
title: 'Composing' title: 'Composing'
return: '<b>shift</b>+<b>c</b> Return to composer' return: '<b>shift</b>+<b>c</b> Return to composer'
fullscreen: '<b>shift</b>+<b>F11</b> Fullscreen composer'
actions: actions:
title: 'Actions' title: 'Actions'
bookmark_topic: '<b>f</b> Toggle bookmark topic' bookmark_topic: '<b>f</b> Toggle bookmark topic'