From 017c258e46fa9d2e51a8b3835f1b80e34521b38c Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Wed, 22 Jul 2015 16:05:00 +0930 Subject: [PATCH] =?UTF-8?q?Live=20preview=20of=20post=20editing/replying?= =?UTF-8?q?=20thanks=20to=20TextFormatter=20=F0=9F=91=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/forum/src/components/CommentPost.js | 23 ++++++++++++++++----- js/forum/src/components/EditPostComposer.js | 23 +++++++++++++++++++++ js/forum/src/components/PostStream.js | 9 +++++--- js/forum/src/components/PostUser.js | 2 +- js/forum/src/components/ReplyComposer.js | 21 +++++++++++++++++++ js/forum/src/components/ReplyPlaceholder.js | 20 ++++++++++++++++++ js/forum/src/utils/formatText.js | 7 +++++++ less/forum/Post.less | 2 +- src/Assets/AssetManager.php | 10 ++++----- src/Assets/Compiler.php | 2 +- src/Assets/LessCompiler.php | 4 ++-- src/Assets/RevisionCompiler.php | 8 +++---- src/Forum/Actions/ClientAction.php | 11 ++++++++++ src/Support/ClientAction.php | 14 +++++++++---- 14 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 js/forum/src/utils/formatText.js diff --git a/js/forum/src/components/CommentPost.js b/js/forum/src/components/CommentPost.js index 73e873591..aca1e97aa 100644 --- a/js/forum/src/components/CommentPost.js +++ b/js/forum/src/components/CommentPost.js @@ -6,6 +6,7 @@ import PostEdited from 'flarum/components/PostEdited'; import EditPostComposer from 'flarum/components/EditPostComposer'; import Composer from 'flarum/components/Composer'; import ItemList from 'flarum/utils/ItemList'; +import formatText from 'flarum/utils/formatText'; import listItems from 'flarum/helpers/listItems'; 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 // that we can force the post to rerender when the user card is shown. 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() { + const content = this.isEditing() + ? formatText(this.props.post.editedContent) + : this.props.post.contentHtml(); + return [
, -
{m.trust(this.props.post.contentHtml())}
, +
{m.trust(content)}
, , ]; } + isEditing() { + return app.composer.component instanceof EditPostComposer && + app.composer.component.props.post === this.props.post && + app.composer.position !== Composer.PositionEnum.MINIMIZED; + } + attrs() { const post = this.props.post; @@ -54,9 +69,7 @@ export default class CommentPost extends Post { 'hidden': post.isHidden(), 'edited': post.isEdited(), 'revealContent': this.revealContent, - 'editing': app.composer.component instanceof EditPostComposer && - app.composer.component.props.post === post && - app.composer.position !== Composer.PositionEnum.MINIMIZED + 'editing': this.isEditing() }) }; } diff --git a/js/forum/src/components/EditPostComposer.js b/js/forum/src/components/EditPostComposer.js index 25d4d60cd..7859c808c 100644 --- a/js/forum/src/components/EditPostComposer.js +++ b/js/forum/src/components/EditPostComposer.js @@ -19,6 +19,29 @@ export default class EditPostComposer extends ComposerBody { props.confirmExit = props.confirmExit || app.trans('core.confirm_discard_edit'); props.originalContent = props.originalContent || props.post.content(); 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() { diff --git a/js/forum/src/components/PostStream.js b/js/forum/src/components/PostStream.js index 506fe9fcf..d7b03d146 100644 --- a/js/forum/src/components/PostStream.js +++ b/js/forum/src/components/PostStream.js @@ -229,8 +229,7 @@ class PostStream extends mixin(Component, evented) { // 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. this.viewingEnd && - (!app.session.user || this.discussion.canReply()) && - !app.composingReplyTo(this.discussion) + (!app.session.user || this.discussion.canReply()) ? (
{ReplyPlaceholder.component({discussion: this.discussion})} @@ -517,8 +516,12 @@ class PostStream extends mixin(Component, evented) { const scrollBottom = scrollTop + $(window).height(); // 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) { - 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) { $container.scrollTop(top); diff --git a/js/forum/src/components/PostUser.js b/js/forum/src/components/PostUser.js index abc7e2503..0bbaf1420 100644 --- a/js/forum/src/components/PostUser.js +++ b/js/forum/src/components/PostUser.js @@ -30,7 +30,7 @@ export default class PostUser extends Component { if (!user) { return (
-

{avatar(user)} {username(user)}

+

{avatar(user, {className: 'PostUser-avatar'})} {username(user)}

); } diff --git a/js/forum/src/components/ReplyComposer.js b/js/forum/src/components/ReplyComposer.js index 684fcebdb..b887f70c7 100644 --- a/js/forum/src/components/ReplyComposer.js +++ b/js/forum/src/components/ReplyComposer.js @@ -34,6 +34,27 @@ export default class ReplyComposer extends ComposerBody { 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. * diff --git a/js/forum/src/components/ReplyPlaceholder.js b/js/forum/src/components/ReplyPlaceholder.js index a5b2005bb..1ec54251b 100644 --- a/js/forum/src/components/ReplyPlaceholder.js +++ b/js/forum/src/components/ReplyPlaceholder.js @@ -1,6 +1,8 @@ import Component from 'flarum/Component'; import avatar from 'flarum/helpers/avatar'; +import username from 'flarum/helpers/username'; import DiscussionControls from 'flarum/utils/DiscussionControls'; +import formatText from 'flarum/utils/formatText'; /** * 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 { view() { + if (app.composingReplyTo(this.props.discussion)) { + return ( +
+
+
+

+ {avatar(app.session.user, {className: 'PostUser-avatar'})} + {username(app.session.user)} +

+
+
+
+ {m.trust(formatText(this.props.discussion.replyContent))} +
+
+ ); + } + function triggerClick(e) { $(this).trigger('click'); e.preventDefault(); diff --git a/js/forum/src/utils/formatText.js b/js/forum/src/utils/formatText.js new file mode 100644 index 000000000..1c21de3f4 --- /dev/null +++ b/js/forum/src/utils/formatText.js @@ -0,0 +1,7 @@ +export default function formatText(text) { + const elm = document.createElement('div'); + + s9e.TextFormatter.preview(text || '', elm); + + return elm.innerHTML; +} diff --git a/less/forum/Post.less b/less/forum/Post.less index 9ac808baf..9e85ac4c9 100644 --- a/less/forum/Post.less +++ b/less/forum/Post.less @@ -9,7 +9,7 @@ &.editing { top: 5px; - opacity: 0.2; + opacity: 0.5; } } .Post-controls { diff --git a/src/Assets/AssetManager.php b/src/Assets/AssetManager.php index 78ae0f8e1..3c9e45da3 100644 --- a/src/Assets/AssetManager.php +++ b/src/Assets/AssetManager.php @@ -29,7 +29,7 @@ class AssetManager break; 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']); } - 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() diff --git a/src/Assets/Compiler.php b/src/Assets/Compiler.php index 11db8d5e4..e26357a1a 100644 --- a/src/Assets/Compiler.php +++ b/src/Assets/Compiler.php @@ -4,7 +4,7 @@ interface Compiler { public function addFile($file); - public function addString($string); + public function addString(callable $callback); public function getFile(); } diff --git a/src/Assets/LessCompiler.php b/src/Assets/LessCompiler.php index 4483e1c0d..4338c8535 100644 --- a/src/Assets/LessCompiler.php +++ b/src/Assets/LessCompiler.php @@ -17,8 +17,8 @@ class LessCompiler extends RevisionCompiler $parser->parseFile($file); } - foreach ($this->strings as $string) { - $parser->parse($string); + foreach ($this->strings as $callback) { + $parser->parse($callback()); } return $parser->getCss(); diff --git a/src/Assets/RevisionCompiler.php b/src/Assets/RevisionCompiler.php index 6d01b6e69..148f2901c 100644 --- a/src/Assets/RevisionCompiler.php +++ b/src/Assets/RevisionCompiler.php @@ -19,9 +19,9 @@ class RevisionCompiler implements Compiler $this->files[] = $file; } - public function addString($string) + public function addString(callable $callback) { - $this->strings[] = $string; + $this->strings[] = $callback; } public function getFile() @@ -63,8 +63,8 @@ class RevisionCompiler implements Compiler $output .= $this->format(file_get_contents($file)); } - foreach ($this->strings as $string) { - $output .= $this->format($string); + foreach ($this->strings as $callback) { + $output .= $this->format($callback()); } return $output; diff --git a/src/Forum/Actions/ClientAction.php b/src/Forum/Actions/ClientAction.php index b63afaf02..b4390fce0 100644 --- a/src/Forum/Actions/ClientAction.php +++ b/src/Forum/Actions/ClientAction.php @@ -131,4 +131,15 @@ abstract class ClientAction extends BaseClientAction 'core.write_a_post', 'core.write_a_reply' ]; + + protected function getAssets() + { + $assets = parent::getAssets(); + + $assets->addJs(function () { + return app('flarum.formatter')->getJS(); + }); + + return $assets; + } } diff --git a/src/Support/ClientAction.php b/src/Support/ClientAction.php index 64025e29f..be3d09c90 100644 --- a/src/Support/ClientAction.php +++ b/src/Support/ClientAction.php @@ -160,11 +160,17 @@ abstract class ClientAction extends HtmlAction */ protected function addCustomizations(AssetManager $assets) { - foreach ($this->getLessVariables() as $name => $value) { - $assets->addLess("@$name: $value;"); - } + $assets->addLess(function () { + $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; + }); } /**