FEATURE: experimental fast edit (#14340)

Fast edit allows you to quickly edit a typo in a post, this is experimental ATM and behind a site setting: `enable_fast_edit` (default false)
This commit is contained in:
Joffrey JAFFEUX 2021-09-15 17:10:30 +02:00 committed by GitHub
parent 27bad28c53
commit b83868bfb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 31 deletions

View File

@ -1,3 +1,6 @@
import { propertyEqual } from "discourse/lib/computed";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
postUrl,
selectedElement,
@ -28,11 +31,27 @@ function getQuoteTitle(element) {
return titleEl.textContent.trim().replace(/:$/, "");
}
function fixQuotes(str) {
return str.replace(/||„|“|«|»|”/g, '"');
}
function regexSafeStr(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export default Component.extend({
classNames: ["quote-button"],
classNameBindings: ["visible"],
visible: false,
privateCategory: alias("topic.category.read_restricted"),
editPost: null,
_isFastEditable: false,
_displayFastEditInput: false,
_fastEditInitalSelection: null,
_fastEditNewSelection: null,
_isSavingFastEdit: false,
_canEditPost: false,
_isMouseDown: false,
_reselected: false,
@ -40,9 +59,18 @@ export default Component.extend({
_hideButton() {
this.quoteState.clear();
this.set("visible", false);
this.set("_isFastEditable", false);
this.set("_displayFastEditInput", false);
this.set("_fastEditInitalSelection", null);
this.set("_fastEditNewSelection", null);
},
_selectionChanged() {
if (this._displayFastEditInput) {
return;
}
const quoteState = this.quoteState;
const selection = window.getSelection();
@ -104,6 +132,33 @@ export default Component.extend({
quoteState.selected(postId, _selectedText, opts);
this.set("visible", quoteState.buffer.length > 0);
if (this.siteSettings.enable_fast_edit) {
this.set(
"_canEditPost",
this.topic.postStream.findLoadedPost(postId)?.can_edit
);
// if we have a linebreak, the selection is probably too complex to be handled
// by fast edit, so ignore it
// if the selection is present multiple times, we also consider it too complex
// and ignore it, note this specific case could probably be handled in the future
const regexp = new RegExp(regexSafeStr(quoteState.buffer), "gi");
const matches = postBody.match(regexp);
if (
quoteState.buffer.length < 1 ||
quoteState.buffer.match(/\n/g) ||
matches?.length > 1
) {
this.set("_isFastEditable", false);
this.set("_fastEditInitalSelection", null);
this.set("_fastEditNewSelection", null);
} else if (matches?.length === 1) {
this.set("_isFastEditable", true);
this.set("_fastEditInitalSelection", quoteState.buffer);
this.set("_fastEditNewSelection", quoteState.buffer);
}
}
// avoid hard loops in quote selection unconditionally
// this can happen if you triple click text in firefox
if (this._prevSelection === _selectedText) {
@ -192,6 +247,12 @@ export default Component.extend({
this._prevSelection = null;
this._isMouseDown = true;
this._reselected = false;
// prevents fast-edit input event to trigger mousedown
if (e.target.classList.contains("fast-edit-input")) {
return;
}
if (
$(e.target).closest(".quote-button, .create, .share, .reply-new")
.length === 0
@ -199,7 +260,12 @@ export default Component.extend({
this._hideButton();
}
})
.on("mouseup.quote-button", () => {
.on("mouseup.quote-button", (e) => {
// prevents fast-edit input event to trigger mouseup
if (e.target.classList.contains("fast-edit-input")) {
return;
}
this._prevSelection = null;
this._isMouseDown = false;
onSelectionChanged();
@ -264,11 +330,56 @@ export default Component.extend({
);
},
_saveFastEditDisabled: propertyEqual(
"_fastEditInitalSelection",
"_fastEditNewSelection"
),
@action
insertQuote() {
this.attrs.selectText().then(() => this._hideButton());
},
@action
_toggleFastEditForm() {
if (this._isFastEditable) {
this.toggleProperty("_displayFastEditInput");
schedule("afterRender", () => {
document.querySelector("#fast-edit-input")?.focus();
});
} else {
const postId = this.quoteState.postId;
const postModel = this.topic.postStream.findLoadedPost(postId);
this?.editPost(postModel);
}
},
@action
_saveFastEdit() {
const postId = this.quoteState?.postId;
const postModel = this.topic.postStream.findLoadedPost(postId);
this.set("_isSavingFastEdit", true);
return ajax(`/posts/${postModel.id}`, { type: "GET", cache: false })
.then((result) => {
const newRaw = result.raw.replace(
fixQuotes(this._fastEditInitalSelection),
fixQuotes(this._fastEditNewSelection)
);
postModel
.save({ raw: newRaw })
.catch(popupAjaxError)
.finally(() => {
this.set("_isSavingFastEdit", false);
this._hideButton();
});
})
.catch(popupAjaxError);
},
@action
share(source) {
Sharing.shareSource(source, {

View File

@ -1,31 +1,64 @@
{{#if embedQuoteButton}}
{{d-button
class="btn-flat insert-quote"
action=(action "insertQuote")
icon="quote-left"
label="post.quote_reply"}}
{{/if}}
<div class="buttons">
{{#if embedQuoteButton}}
{{d-button
class="btn-flat insert-quote"
action=(action "insertQuote")
icon="quote-left"
label="post.quote_reply"}}
{{/if}}
{{#if quoteSharingEnabled}}
<span class="quote-sharing">
{{#if quoteSharingShowLabel}}
{{d-button
icon="share"
label="post.quote_share"
class="btn-flat quote-share-label"}}
{{/if}}
<span class="quote-share-buttons">
{{#each quoteSharingSources as |source|}}
{{#if quoteSharingEnabled}}
<span class="quote-sharing">
{{#if quoteSharingShowLabel}}
{{d-button
class="btn-flat"
action=(action "share" source)
translatedTitle=source.title
icon=source.icon}}
{{/each}}
{{plugin-outlet name="quote-share-buttons-after" tagName=""}}
</span>
</span>
{{/if}}
icon="share"
label="post.quote_share"
class="btn-flat quote-share-label"}}
{{/if}}
{{plugin-outlet name="quote-button-after" tagName=""}}
<span class="quote-share-buttons">
{{#each quoteSharingSources as |source|}}
{{d-button
class="btn-flat"
action=(action "share" source)
translatedTitle=source.title
icon=source.icon}}
{{/each}}
{{plugin-outlet name="quote-share-buttons-after" tagName=""}}
</span>
</span>
{{/if}}
{{#if siteSettings.enable_fast_edit}}
{{#if _canEditPost}}
{{d-button
icon="pencil-alt"
action=(action "_toggleFastEditForm")
label="post.quote_edit"
class="btn-flat quote-edit-label"
}}
{{/if}}
{{/if}}
</div>
<div class="extra">
{{#if siteSettings.enable_fast_edit}}
{{#if _displayFastEditInput}}
<div class="fast-edit-container">
{{textarea
id="fast-edit-input"
value=_fastEditNewSelection
}}
{{d-button
action=(action "_saveFastEdit")
class="btn-default btn-primary save-fast-edit"
label="save"
disabled=_saveFastEditDisabled
isLoading=_isSavingFastEdit
}}
</div>
{{/if}}
{{/if}}
{{plugin-outlet name="quote-button-after" tagName=""}}
</div>

View File

@ -404,5 +404,12 @@
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
{{quote-button quoteState=quoteState selectText=(action "selectText") topic=model composerVisible=composer.visible}}
{{quote-button
quoteState=quoteState
selectText=(action "selectText")
editPost=(action "editPost")
topic=model
composerVisible=composer.visible
}}
{{/discourse-topic}}

View File

@ -378,9 +378,37 @@ aside.quote {
z-index: z("dropdown");
opacity: 0.9;
background-color: var(--secondary-high);
flex-direction: column;
&.visible {
display: inline-flex;
display: flex;
}
.buttons {
display: flex;
}
.extra {
display: flex;
flex-direction: column;
width: 100%;
}
.fast-edit-container {
display: flex;
padding: 0.25em;
flex-direction: column;
align-items: flex-start;
#fast-edit-input {
margin: 0;
width: 300px;
height: 90px;
}
.save-fast-edit {
margin-top: 0.25em;
}
}
.btn,

View File

@ -2973,6 +2973,7 @@ en:
post:
quote_reply: "Quote"
quote_edit: "Edit"
quote_share: "Share"
edit_reason: "Reason: "
post_number: "post %{number}"

View File

@ -2212,6 +2212,7 @@ en:
watched_words_regular_expressions: "Watched words are regular expressions."
enable_diffhtml_preview: "Experimental feature which uses diffHTML to sync preview instead of full re-render"
enable_fast_edit: "Experimental feature which enables small selection of a post text to be edited inline."
old_post_notice_days: "Days before post notice becomes old"
new_user_notice_tl: "Minimum trust level required to see new user post notices."

View File

@ -1010,6 +1010,9 @@ posting:
enable_diffhtml_preview:
default: false
client: true
enable_fast_edit:
default: false
client: true
old_post_notice_days:
default: 14
max: 36500