mirror of
https://github.com/flarum/framework.git
synced 2025-02-22 08:31:12 +08:00
Live preview of post editing/replying thanks to TextFormatter 👏
This commit is contained in:
parent
a0fe68272c
commit
017c258e46
@ -6,6 +6,7 @@ import PostEdited from 'flarum/components/PostEdited';
|
|||||||
import EditPostComposer from 'flarum/components/EditPostComposer';
|
import EditPostComposer from 'flarum/components/EditPostComposer';
|
||||||
import Composer from 'flarum/components/Composer';
|
import Composer from 'flarum/components/Composer';
|
||||||
import ItemList from 'flarum/utils/ItemList';
|
import ItemList from 'flarum/utils/ItemList';
|
||||||
|
import formatText from 'flarum/utils/formatText';
|
||||||
import listItems from 'flarum/helpers/listItems';
|
import listItems from 'flarum/helpers/listItems';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
|
|
||||||
@ -33,18 +34,32 @@ export default class CommentPost extends Post {
|
|||||||
// Create an instance of the component that displays the post's author so
|
// Create an instance of the component that displays the post's author so
|
||||||
// that we can force the post to rerender when the user card is shown.
|
// that we can force the post to rerender when the user card is shown.
|
||||||
this.postUser = new PostUser({post: this.props.post});
|
this.postUser = new PostUser({post: this.props.post});
|
||||||
this.subtree.check(() => this.postUser.cardVisible);
|
this.subtree.check(
|
||||||
|
() => this.postUser.cardVisible,
|
||||||
|
() => this.props.post.editedContent,
|
||||||
|
() => this.isEditing()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
|
const content = this.isEditing()
|
||||||
|
? formatText(this.props.post.editedContent)
|
||||||
|
: this.props.post.contentHtml();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<header className="Post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>,
|
<header className="Post-header"><ul>{listItems(this.headerItems().toArray())}</ul></header>,
|
||||||
<div className="Post-body">{m.trust(this.props.post.contentHtml())}</div>,
|
<div className="Post-body">{m.trust(content)}</div>,
|
||||||
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>,
|
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>,
|
||||||
<aside className="Post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside>
|
<aside className="Post-actions"><ul>{listItems(this.actionItems().toArray())}</ul></aside>
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isEditing() {
|
||||||
|
return app.composer.component instanceof EditPostComposer &&
|
||||||
|
app.composer.component.props.post === this.props.post &&
|
||||||
|
app.composer.position !== Composer.PositionEnum.MINIMIZED;
|
||||||
|
}
|
||||||
|
|
||||||
attrs() {
|
attrs() {
|
||||||
const post = this.props.post;
|
const post = this.props.post;
|
||||||
|
|
||||||
@ -54,9 +69,7 @@ export default class CommentPost extends Post {
|
|||||||
'hidden': post.isHidden(),
|
'hidden': post.isHidden(),
|
||||||
'edited': post.isEdited(),
|
'edited': post.isEdited(),
|
||||||
'revealContent': this.revealContent,
|
'revealContent': this.revealContent,
|
||||||
'editing': app.composer.component instanceof EditPostComposer &&
|
'editing': this.isEditing()
|
||||||
app.composer.component.props.post === post &&
|
|
||||||
app.composer.position !== Composer.PositionEnum.MINIMIZED
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,29 @@ export default class EditPostComposer extends ComposerBody {
|
|||||||
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit');
|
props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit');
|
||||||
props.originalContent = props.originalContent || props.post.content();
|
props.originalContent = props.originalContent || props.post.content();
|
||||||
props.user = props.user || props.post.user();
|
props.user = props.user || props.post.user();
|
||||||
|
|
||||||
|
props.post.editedContent = props.originalContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
super.config(isInitialized, context);
|
||||||
|
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
// Every 50ms, if the content has changed, then update the post's
|
||||||
|
// editedContent property and redraw. This will cause the preview in the
|
||||||
|
// post's component to update.
|
||||||
|
const updateInterval = setInterval(() => {
|
||||||
|
const post = this.props.post;
|
||||||
|
const content = this.content();
|
||||||
|
|
||||||
|
if (content === post.editedContent) return;
|
||||||
|
|
||||||
|
post.editedContent = content;
|
||||||
|
m.redraw();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
context.onunload = () => clearInterval(updateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
headerItems() {
|
headerItems() {
|
||||||
|
@ -229,8 +229,7 @@ class PostStream extends mixin(Component, evented) {
|
|||||||
// If we're viewing the end of the discussion, the user can reply, and
|
// If we're viewing the end of the discussion, the user can reply, and
|
||||||
// is not already doing so, then show a 'write a reply' placeholder.
|
// is not already doing so, then show a 'write a reply' placeholder.
|
||||||
this.viewingEnd &&
|
this.viewingEnd &&
|
||||||
(!app.session.user || this.discussion.canReply()) &&
|
(!app.session.user || this.discussion.canReply())
|
||||||
!app.composingReplyTo(this.discussion)
|
|
||||||
? (
|
? (
|
||||||
<div className="PostStream-item" key="reply">
|
<div className="PostStream-item" key="reply">
|
||||||
{ReplyPlaceholder.component({discussion: this.discussion})}
|
{ReplyPlaceholder.component({discussion: this.discussion})}
|
||||||
@ -517,8 +516,12 @@ class PostStream extends mixin(Component, evented) {
|
|||||||
const scrollBottom = scrollTop + $(window).height();
|
const scrollBottom = scrollTop + $(window).height();
|
||||||
|
|
||||||
// If the item is already in the viewport, we may not need to scroll.
|
// If the item is already in the viewport, we may not need to scroll.
|
||||||
|
// If we're scrolling to the bottom of an item, then we'll make sure the
|
||||||
|
// bottom will line up with the top of the composer.
|
||||||
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
|
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
|
||||||
const top = bottom ? itemBottom : ($item.is(':first-child') ? 0 : itemTop);
|
const top = bottom
|
||||||
|
? itemBottom - $(window).height() + app.composer.computedHeight()
|
||||||
|
: ($item.is(':first-child') ? 0 : itemTop);
|
||||||
|
|
||||||
if (noAnimation) {
|
if (noAnimation) {
|
||||||
$container.scrollTop(top);
|
$container.scrollTop(top);
|
||||||
|
@ -30,7 +30,7 @@ export default class PostUser extends Component {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<div className="PostUser">
|
<div className="PostUser">
|
||||||
<h3>{avatar(user)} {username(user)}</h3>
|
<h3>{avatar(user, {className: 'PostUser-avatar'})} {username(user)}</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,27 @@ export default class ReplyComposer extends ComposerBody {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config(isInitialized, context) {
|
||||||
|
super.config(isInitialized, context);
|
||||||
|
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
// Every 50ms, if the content has changed, then update the post's
|
||||||
|
// editedContent property and redraw. This will cause the preview in the
|
||||||
|
// post's component to update.
|
||||||
|
const updateInterval = setInterval(() => {
|
||||||
|
const discussion = this.props.discussion;
|
||||||
|
const content = this.content();
|
||||||
|
|
||||||
|
if (content === discussion.replyContent) return;
|
||||||
|
|
||||||
|
discussion.replyContent = content;
|
||||||
|
m.redraw();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
context.onunload = () => clearInterval(updateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the data to submit to the server when the reply is saved.
|
* Get the data to submit to the server when the reply is saved.
|
||||||
*
|
*
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import Component from 'flarum/Component';
|
import Component from 'flarum/Component';
|
||||||
import avatar from 'flarum/helpers/avatar';
|
import avatar from 'flarum/helpers/avatar';
|
||||||
|
import username from 'flarum/helpers/username';
|
||||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||||
|
import formatText from 'flarum/utils/formatText';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ReplyPlaceholder` component displays a placeholder for a reply, which,
|
* The `ReplyPlaceholder` component displays a placeholder for a reply, which,
|
||||||
@ -12,6 +14,24 @@ import DiscussionControls from 'flarum/utils/DiscussionControls';
|
|||||||
*/
|
*/
|
||||||
export default class ReplyPlaceholder extends Component {
|
export default class ReplyPlaceholder extends Component {
|
||||||
view() {
|
view() {
|
||||||
|
if (app.composingReplyTo(this.props.discussion)) {
|
||||||
|
return (
|
||||||
|
<article className="Post CommentPost editing">
|
||||||
|
<header className="Post-header">
|
||||||
|
<div className="PostUser">
|
||||||
|
<h3>
|
||||||
|
{avatar(app.session.user, {className: 'PostUser-avatar'})}
|
||||||
|
{username(app.session.user)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="Post-body">
|
||||||
|
{m.trust(formatText(this.props.discussion.replyContent))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function triggerClick(e) {
|
function triggerClick(e) {
|
||||||
$(this).trigger('click');
|
$(this).trigger('click');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
7
js/forum/src/utils/formatText.js
Normal file
7
js/forum/src/utils/formatText.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function formatText(text) {
|
||||||
|
const elm = document.createElement('div');
|
||||||
|
|
||||||
|
s9e.TextFormatter.preview(text || '', elm);
|
||||||
|
|
||||||
|
return elm.innerHTML;
|
||||||
|
}
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
&.editing {
|
&.editing {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
opacity: 0.2;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.Post-controls {
|
.Post-controls {
|
||||||
|
@ -29,7 +29,7 @@ class AssetManager
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new DomainException('Unsupported asset type: '.$ext);
|
throw new DomainException('Unsupported asset type: ' . $ext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,14 +38,14 @@ class AssetManager
|
|||||||
array_walk($files, [$this, 'addFile']);
|
array_walk($files, [$this, 'addFile']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addLess($string)
|
public function addLess(callable $callback)
|
||||||
{
|
{
|
||||||
$this->less->addString($string);
|
$this->less->addString($callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addJs($strings)
|
public function addJs(callable $callback)
|
||||||
{
|
{
|
||||||
$this->js->addString($string);
|
$this->js->addString($callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCssFile()
|
public function getCssFile()
|
||||||
|
@ -4,7 +4,7 @@ interface Compiler
|
|||||||
{
|
{
|
||||||
public function addFile($file);
|
public function addFile($file);
|
||||||
|
|
||||||
public function addString($string);
|
public function addString(callable $callback);
|
||||||
|
|
||||||
public function getFile();
|
public function getFile();
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@ class LessCompiler extends RevisionCompiler
|
|||||||
$parser->parseFile($file);
|
$parser->parseFile($file);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->strings as $string) {
|
foreach ($this->strings as $callback) {
|
||||||
$parser->parse($string);
|
$parser->parse($callback());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $parser->getCss();
|
return $parser->getCss();
|
||||||
|
@ -19,9 +19,9 @@ class RevisionCompiler implements Compiler
|
|||||||
$this->files[] = $file;
|
$this->files[] = $file;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addString($string)
|
public function addString(callable $callback)
|
||||||
{
|
{
|
||||||
$this->strings[] = $string;
|
$this->strings[] = $callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFile()
|
public function getFile()
|
||||||
@ -63,8 +63,8 @@ class RevisionCompiler implements Compiler
|
|||||||
$output .= $this->format(file_get_contents($file));
|
$output .= $this->format(file_get_contents($file));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->strings as $string) {
|
foreach ($this->strings as $callback) {
|
||||||
$output .= $this->format($string);
|
$output .= $this->format($callback());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
|
@ -131,4 +131,15 @@ abstract class ClientAction extends BaseClientAction
|
|||||||
'core.write_a_post',
|
'core.write_a_post',
|
||||||
'core.write_a_reply'
|
'core.write_a_reply'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected function getAssets()
|
||||||
|
{
|
||||||
|
$assets = parent::getAssets();
|
||||||
|
|
||||||
|
$assets->addJs(function () {
|
||||||
|
return app('flarum.formatter')->getJS();
|
||||||
|
});
|
||||||
|
|
||||||
|
return $assets;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,11 +160,17 @@ abstract class ClientAction extends HtmlAction
|
|||||||
*/
|
*/
|
||||||
protected function addCustomizations(AssetManager $assets)
|
protected function addCustomizations(AssetManager $assets)
|
||||||
{
|
{
|
||||||
foreach ($this->getLessVariables() as $name => $value) {
|
$assets->addLess(function () {
|
||||||
$assets->addLess("@$name: $value;");
|
$less = '';
|
||||||
}
|
|
||||||
|
|
||||||
$assets->addLess($this->settings->get('custom_less'));
|
foreach ($this->getLessVariables() as $name => $value) {
|
||||||
|
$less .= "@$name: $value;";
|
||||||
|
}
|
||||||
|
|
||||||
|
$less .= $this->settings->get('custom_less');
|
||||||
|
|
||||||
|
return $less;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user