Live preview of post editing/replying thanks to TextFormatter 👏

This commit is contained in:
Toby Zerner 2015-07-22 16:05:00 +09:30
parent a0fe68272c
commit 017c258e46
14 changed files with 130 additions and 26 deletions

View File

@ -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
}) })
}; };
} }

View File

@ -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() {

View File

@ -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);

View File

@ -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>
); );
} }

View File

@ -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.
* *

View File

@ -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();

View File

@ -0,0 +1,7 @@
export default function formatText(text) {
const elm = document.createElement('div');
s9e.TextFormatter.preview(text || '', elm);
return elm.innerHTML;
}

View File

@ -9,7 +9,7 @@
&.editing { &.editing {
top: 5px; top: 5px;
opacity: 0.2; opacity: 0.5;
} }
} }
.Post-controls { .Post-controls {

View File

@ -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()

View File

@ -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();
} }

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
} }

View File

@ -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;
});
} }
/** /**