From c24ca57f215efa4c0d41eb92ac25f86ae0caed7f Mon Sep 17 00:00:00 2001 From: Hypolite Petovan <hypolite@mrpetovan.com> Date: Sun, 14 Jul 2019 21:49:38 -0400 Subject: [PATCH] Add frio-specific /compose module --- src/App/Router.php | 1 + src/Module/Item/Compose.php | 176 +++++++++++++++++++ view/templates/item/compose-footer.tpl | 227 +++++++++++++++++++++++++ view/templates/item/compose.tpl | 129 ++++++++++++++ view/theme/frio/css/style.css | 6 +- 5 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 src/Module/Item/Compose.php create mode 100644 view/templates/item/compose-footer.tpl create mode 100644 view/templates/item/compose.tpl diff --git a/src/App/Router.php b/src/App/Router.php index a54f3a711e..50b208792b 100644 --- a/src/App/Router.php +++ b/src/App/Router.php @@ -94,6 +94,7 @@ class Router $this->routeCollector->addRoute(['GET'], '/attach/{item:\d+}', Module\Attach::class); $this->routeCollector->addRoute(['GET'], '/babel', Module\Debug\Babel::class); $this->routeCollector->addRoute(['GET'], '/bookmarklet', Module\Bookmarklet::class); + $this->routeCollector->addRoute(['GET', 'POST'], '/compose[/{type}]', Module\Item\Compose::class); $this->routeCollector->addGroup('/contact', function (RouteCollector $collector) { $collector->addRoute(['GET'], '[/]', Module\Contact::class); $collector->addRoute(['GET', 'POST'], '/{id:\d+}[/]', Module\Contact::class); diff --git a/src/Module/Item/Compose.php b/src/Module/Item/Compose.php new file mode 100644 index 0000000000..750a29c49a --- /dev/null +++ b/src/Module/Item/Compose.php @@ -0,0 +1,176 @@ +<?php + +namespace Friendica\Module\Item; + +use Friendica\BaseModule; +use Friendica\Content\Feature; +use Friendica\Core\Hook; +use Friendica\Core\L10n; +use Friendica\Core\Renderer; +use Friendica\Model\Contact; +use Friendica\Model\FileTag; +use Friendica\Model\Group; +use Friendica\Model\Item; +use Friendica\Model\User; +use Friendica\Module\Login; +use Friendica\Network\HTTPException\NotImplementedException; +use Friendica\Util\Crypto; + +class Compose extends BaseModule +{ + public static function post() + { + if (!empty($_REQUEST['body'])) { + $_REQUEST['return'] = 'network'; + require_once 'mod/item.php'; + item_post(self::getApp()); + } else { + notice(L10n::t('Please enter a post body.')); + } + } + + public static function content() + { + if (!local_user()) { + return Login::form('compose', false); + } + + $a = self::getApp(); + + if ($a->getCurrentTheme() !== 'frio') { + throw new NotImplementedException(L10n::t('This feature is only available with the frio theme.')); + } + + /// @TODO Retrieve parameter from router + $posttype = $a->argv[1] ?? Item::PT_ARTICLE; + if (!in_array($posttype, [Item::PT_ARTICLE, Item::PT_PERSONAL_NOTE])) { + switch ($posttype) { + case 'note': $posttype = Item::PT_PERSONAL_NOTE; break; + default: $posttype = Item::PT_ARTICLE; break; + } + } + + $user = User::getById(local_user(), ['allow_cid', 'allow_gid', 'deny_cid', 'deny_gid', 'default-location']); + + switch ($posttype) { + case Item::PT_PERSONAL_NOTE: + $compose_title = L10n::t('Compose new personal note'); + $type = 'note'; + $contact_allow = $a->contact['id']; + $group_allow = ''; + break; + default: + $compose_title = L10n::t('Compose new post'); + $type = 'post'; + $contact_allow = implode(',', expand_acl($user['allow_cid'])); + $group_allow = implode(',', expand_acl($user['allow_gid'])) ?: Group::FOLLOWERS; + break; + } + + $title = $_REQUEST['title'] ?? ''; + $category = $_REQUEST['category'] ?? ''; + $body = $_REQUEST['body'] ?? ''; + $location = $_REQUEST['location'] ?? $user['default-location']; + $wall = $_REQUEST['wall'] ?? $type == 'post'; + $contact_allow = $_REQUEST['contact_allow'] ?? $contact_allow; + $group_allow = $_REQUEST['group_allow'] ?? $group_allow; + $contact_deny = $_REQUEST['contact_deny'] ?? implode(',', expand_acl($user['deny_cid'])); + $group_deny = $_REQUEST['group_deny'] ?? implode(',', expand_acl($user['deny_gid'])); + $visibility = ($contact_allow . $user['allow_gid'] . $user['deny_cid'] . $user['deny_gid']) ? 'custom' : 'public'; + + $acl_contacts = Contact::select(['id', 'name', 'addr', 'micro'], ['uid' => local_user(), 'pending' => false, 'rel' => [Contact::FOLLOWER, Contact::FRIEND]]); + array_walk($acl_contacts, function (&$value) { + $value['type'] = 'contact'; + }); + + $acl_groups = [ + [ + 'id' => Group::FOLLOWERS, + 'name' => L10n::t('Followers'), + 'addr' => '', + 'micro' => 'images/twopeople.png', + 'type' => 'group', + ], + [ + 'id' => Group::MUTUALS, + 'name' => L10n::t('Mutuals'), + 'addr' => '', + 'micro' => 'images/twopeople.png', + 'type' => 'group', + ] + ]; + foreach (Group::getByUserId(local_user()) as $group) { + $acl_groups[] = [ + 'id' => $group['id'], + 'name' => $group['name'], + 'addr' => '', + 'micro' => 'images/twopeople.png', + 'type' => 'group', + ]; + } + + $acl = array_merge($acl_groups, $acl_contacts); + + $jotplugins = ''; + Hook::callAll('jot_tool', $jotplugins); + + // Output + + $a->registerFooterScript('view/js/ajaxupload.js'); + $a->registerFooterScript('view/js/linkPreview.js'); + $a->registerFooterScript('view/asset/typeahead.js/dist/typeahead.bundle.js'); + $a->registerFooterScript('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput.js'); + $a->registerStylesheet('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput.css'); + $a->registerStylesheet('view/theme/frio/frameworks/friendica-tagsinput/friendica-tagsinput-typeahead.css'); + + $tpl = Renderer::getMarkupTemplate('item/compose-footer.tpl'); + $a->page['footer'] .= Renderer::replaceMacros($tpl, [ + '$acl_contacts' => $acl_contacts, + '$acl_groups' => $acl_groups, + '$acl' => $acl, + ]); + + $tpl = Renderer::getMarkupTemplate('item/compose.tpl'); + return Renderer::replaceMacros($tpl, [ + '$compose_title'=> $compose_title, + '$id' => 0, + '$posttype' => $posttype, + '$type' => $type, + '$wall' => $wall, + '$default' => L10n::t(''), + '$mylink' => $a->removeBaseURL($a->contact['url']), + '$mytitle' => L10n::t('This is you'), + '$myphoto' => $a->removeBaseURL($a->contact['thumb']), + '$submit' => L10n::t('Submit'), + '$edbold' => L10n::t('Bold'), + '$editalic' => L10n::t('Italic'), + '$eduline' => L10n::t('Underline'), + '$edquote' => L10n::t('Quote'), + '$edcode' => L10n::t('Code'), + '$edimg' => L10n::t('Image'), + '$edurl' => L10n::t('Link'), + '$edattach' => L10n::t('Link or Media'), + '$prompttext' => L10n::t('Please enter a image/video/audio/webpage URL:'), + '$preview' => L10n::t('Preview'), + '$location_set' => L10n::t('Set your location'), + '$location_clear' => L10n::t('Clear the location'), + '$location_unavailable' => L10n::t('Location services are unavailable on your device'), + '$location_disabled' => L10n::t('Location services are disabled. Please check the website\'s permissions on your device'), + '$wait' => L10n::t('Please wait'), + '$placeholdertitle' => L10n::t('Set title'), + '$placeholdercategory' => (Feature::isEnabled(local_user(),'categories') ? L10n::t('Categories (comma-separated list)') : ''), + '$title' => $title, + '$category' => $category, + '$body' => $body, + '$location' => $location, + '$visibility' => $visibility, + '$contact_allow'=> $contact_allow, + '$group_allow' => $group_allow, + '$contact_deny' => $contact_deny, + '$group_deny' => $group_deny, + '$jotplugins' => $jotplugins, + '$sourceapp' => L10n::t($a->sourcename), + '$rand_num' => Crypto::randomDigits(12) + ]); + } +} diff --git a/view/templates/item/compose-footer.tpl b/view/templates/item/compose-footer.tpl new file mode 100644 index 0000000000..8a854238a1 --- /dev/null +++ b/view/templates/item/compose-footer.tpl @@ -0,0 +1,227 @@ +<script type="text/javascript"> + function updateLocationButtonDisplay(location_button, location_input) + { + location_button.classList.remove('btn-primary'); + if (location_input.value) { + location_button.disabled = false; + location_button.classList.add('btn-primary'); + location_button.title = location_button.dataset.titleClear; + } else if (!"geolocation" in navigator) { + location_button.disabled = true; + location_button.title = location_button.dataset.titleUnavailable; + } else if (location_button.disabled) { + location_button.title = location_button.dataset.titleDisabled; + } else { + location_button.title = location_button.dataset.titleSet; + } + } + + $(function() { + // Jot attachment live preview. + let $textarea = $('#comment-edit-text-0'); + $textarea.linkPreview(); + $textarea.keyup(function(){ + var textlen = $(this).val().length; + $('#character-counter').text(textlen); + }); + $textarea.editor_autocomplete(baseurl+"/acl"); + $textarea.bbco_autocomplete('bbcode'); + + let $acl_allow_input = $('#acl_allow'); + let $group_allow_input = $('[name=group_allow]'); + let $contact_allow_input = $('[name=contact_allow]'); + let $acl_deny_input = $('#acl_deny'); + let $group_deny_input = $('[name=group_deny]'); + let $contact_deny_input = $('[name=contact_deny]'); + + // Visibility accordion + + // Prevents open panel to collapse + // @see https://stackoverflow.com/a/43593116 + $('[data-toggle="collapse"]').click(function(e) { + target = $(this).attr('href'); + if ($(target).hasClass('in')) { + e.preventDefault(); // to stop the page jump to the anchor target. + e.stopPropagation() + } + }); + + $('#visibility-public-panel').on('show.bs.collapse', function() { + $('#visibility-public').prop('checked', true); + $group_allow_input.prop('disabled', true); + $contact_allow_input.prop('disabled', true); + $group_deny_input.prop('disabled', true); + $contact_deny_input.prop('disabled', true); + }); + + $('#visibility-custom-panel').on('show.bs.collapse', function() { + $('#visibility-custom').prop('checked', true); + $group_allow_input.prop('disabled', false); + $contact_allow_input.prop('disabled', false); + $group_deny_input.prop('disabled', false); + $contact_deny_input.prop('disabled', false); + }); + + if (document.querySelector('input[name="visibility"]:checked').value === 'custom') { + $('#visibility-custom-panel').collapse({parent: '#visibility-accordion'}); + } + + // Custom visibility tags inputs + + let acl_groups = new Bloodhound({ + local: {{$acl_groups|@json_encode nofilter}}, + identify: function(obj) { return obj.id; }, + datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name']), + queryTokenizer: Bloodhound.tokenizers.whitespace, + }); + let acl_contacts = new Bloodhound({ + local: {{$acl_contacts|@json_encode nofilter}}, + identify: function(obj) { return obj.id; }, + datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name', 'addr']), + queryTokenizer: Bloodhound.tokenizers.whitespace, + }); + let acl = new Bloodhound({ + local: {{$acl|@json_encode nofilter}}, + identify: function(obj) { return obj.id; }, + datumTokenizer: Bloodhound.tokenizers.obj.whitespace(['name', 'addr']), + queryTokenizer: Bloodhound.tokenizers.whitespace, + }); + acl.initialize(); + + let suggestionTemplate = function (item) { + return '<div><img src="' + item.micro + '" alt="" style="float: left; width: auto; height: 2.8em; margin-right: 0.5em;"> <strong>' + item.name + '</strong><br /><em>' + item.addr + '</em></div>'; + }; + + $acl_allow_input.tagsinput({ + confirmKeys: [13, 44], + cancelConfirmKeysOnEmpty: true, + freeInput: false, + tagClass: function(item) { + switch (item.type) { + case 'group' : return 'label label-primary'; + case 'contact' : + default: + return 'label label-info'; + } + }, + itemValue: 'id', + itemText: 'name', + itemThumb: 'micro', + itemTitle: function(item) { + return item.addr; + }, + typeaheadjs: { + name: 'contacts', + displayKey: 'name', + templates: { + suggestion: suggestionTemplate + }, + source: acl.ttAdapter() + } + }); + + $acl_deny_input + .tagsinput({ + confirmKeys: [13, 44], + freeInput: false, + tagClass: function(item) { + switch (item.type) { + case 'group' : return 'label label-primary'; + case 'contact' : + default: + return 'label label-info'; + } + }, + itemValue: 'id', + itemText: 'name', + itemThumb: 'micro', + itemTitle: function(item) { + return item.addr; + }, + typeaheadjs: { + name: 'contacts', + displayKey: 'name', + templates: { + suggestion: suggestionTemplate + }, + source: acl.ttAdapter() + } + }); + + // Import existing ACL into the tags input fields. + + $group_allow_input.val().split(',').forEach(function (val) { + $acl_allow_input.tagsinput('add', acl_groups.get(val)[0]); + }); + $contact_allow_input.val().split(',').forEach(function (val) { + $acl_allow_input.tagsinput('add', acl_contacts.get(val)[0]); + }); + $group_deny_input.val().split(',').forEach(function (val) { + $acl_deny_input.tagsinput('add', acl_groups.get(val)[0]); + }); + $contact_deny_input.val().split(',').forEach(function (val) { + $acl_deny_input.tagsinput('add', acl_contacts.get(val)[0]); + }); + + // Anti-duplicate callback + acl fields value generation + + $acl_allow_input.on('itemAdded', function (event) { + // Removes duplicate in the opposite acl box + $acl_deny_input.tagsinput('remove', event.item); + + // Update the real acl field + $group_allow_input.val(''); + $contact_allow_input.val(''); + [].forEach.call($acl_allow_input.tagsinput('items'), function (item) { + if (item.type === 'group') { + $group_allow_input.val($group_allow_input.val() + '<' + item.id + '>'); + } else { + $contact_allow_input.val($contact_allow_input.val() + '<' + item.id + '>'); + } + }); + }); + + $acl_deny_input.on('itemAdded', function (event) { + // Removes duplicate in the opposite acl box + $acl_allow_input.tagsinput('remove', event.item); + + // Update the real acl field + $group_deny_input.val(''); + $contact_deny_input.val(''); + [].forEach.call($acl_deny_input.tagsinput('items'), function (item) { + if (item.type === 'group') { + $group_deny_input.val($group_allow_input.val() + '<' + item.id + '>'); + } else { + $contact_deny_input.val($contact_allow_input.val() + '<' + item.id + '>'); + } + }); + }); + + let location_button = document.getElementById('profile-location'); + let location_input = document.getElementById('jot-location'); + + updateLocationButtonDisplay(location_button, location_input); + + location_input.addEventListener('change', function () { + updateLocationButtonDisplay(location_button, location_input); + }); + location_input.addEventListener('keyup', function () { + updateLocationButtonDisplay(location_button, location_input); + }); + + location_button.addEventListener('click', function() { + if (location_input.value) { + location_input.value = ''; + updateLocationButtonDisplay(location_button, location_input); + } else if ("geolocation" in navigator) { + navigator.geolocation.getCurrentPosition(function(position) { + location_input.value = position.coords.latitude + ', ' + position.coords.longitude; + updateLocationButtonDisplay(location_button, location_input); + }, function (error) { + location_button.disabled = true; + updateLocationButtonDisplay(location_button, location_input); + }); + } + }); + }) +</script> diff --git a/view/templates/item/compose.tpl b/view/templates/item/compose.tpl new file mode 100644 index 0000000000..9594645624 --- /dev/null +++ b/view/templates/item/compose.tpl @@ -0,0 +1,129 @@ +<div class="generic-page-wrapper"> + <h2>{{$compose_title}}</h2> + <div id="profile-jot-wrapper"> + <form class="comment-edit-form" data-item-id="{{$id}}" id="comment-edit-form-{{$id}}" action="compose/{{$type}}" method="post"> + {{*<!--<input type="hidden" name="return" value="{{$return_path}}" />-->*}} + <input type="hidden" name="preview" id="comment-preview-inp-{{$id}}" value="0" /> + <input type="hidden" name="post_id_random" value="{{$rand_num}}" /> + <input type="hidden" name="post_type" value="{{$posttype}}" /> + <input type="hidden" name="wall" value="{{$wall}}" /> + + <div id="jot-title-wrap"> + <input type="text" name="title" id="jot-title" class="jothidden jotforms form-control" placeholder="{{$placeholdertitle}}" title="{{$placeholdertitle}}" value="{{$title}}" tabindex="1"/> + </div> + {{if $placeholdercategory}} + <div id="jot-category-wrap"> + <input name="category" id="jot-category" class="jothidden jotforms form-control" type="text" placeholder="{{$placeholdercategory}}" title="{{$placeholdercategory}}" value="{{$category}}" tabindex="2"/> + </div> + {{/if}} + + <p class="comment-edit-bb-{{$id}} comment-icon-list"> + <span> + <button type="button" class="btn btn-sm icon bb-img" aria-label="{{$edimg}}" title="{{$edimg}}" data-role="insert-formatting" data-bbcode="img" data-id="{{$id}}" tabindex="7"> + <i class="fa fa-picture-o"></i> + </button> + <button type="button" class="btn btn-sm icon bb-attach" aria-label="{{$edattach}}" title="{{$edattach}}" ondragenter="return commentLinkDrop(event, {{$id}});" ondragover="return commentLinkDrop(event, {{$id}});" ondrop="commentLinkDropper(event);" onclick="commentGetLink({{$id}}, '{{$prompttext}}');" tabindex="8"> + <i class="fa fa-paperclip"></i> + </button> + </span> + <span> + <button type="button" class="btn btn-sm icon bb-url" aria-label="{{$edurl}}" title="{{$edurl}}" onclick="insertFormatting('url',{{$id}});" tabindex="9"> + <i class="fa fa-link"></i> + </button> + <button type="button" class="btn btn-sm icon underline" aria-label="{{$eduline}}" title="{{$eduline}}" onclick="insertFormatting('u',{{$id}});" tabindex="10"> + <i class="fa fa-underline"></i> + </button> + <button type="button" class="btn btn-sm icon italic" aria-label="{{$editalic}}" title="{{$editalic}}" onclick="insertFormatting('i',{{$id}});" tabindex="11"> + <i class="fa fa-italic"></i> + </button> + <button type="button" class="btn btn-sm icon bold" aria-label="{{$edbold}}" title="{{$edbold}}" onclick="insertFormatting('b',{{$id}});" tabindex="12"> + <i class="fa fa-bold"></i> + </button> + <button type="button" class="btn btn-sm icon quote" aria-label="{{$edquote}}" title="{{$edquote}}" onclick="insertFormatting('quote',{{$id}});" tabindex="13"> + <i class="fa fa-quote-left"></i> + </button> + </span> + </p> + <p> + <textarea id="comment-edit-text-{{$id}}" class="comment-edit-text form-control text-autosize" name="body" placeholder="{{$default}}" rows="7" tabindex="3">{{$body}}</textarea> + </p> + + <p class="comment-edit-submit-wrapper"> +{{if $type == 'post'}} + <span role="presentation" class="form-inline"> + <input type="text" name="location" class="form-control" id="jot-location" value="{{$location}}" placeholder="{{$location_set}}"/> + <button type="button" class="btn btn-sm icon" id="profile-location" + data-title-set="{{$location_set}}" + data-title-disabled="{{$location_disabled}}" + data-title-unavailable="{{$location_unavailable}}" + data-title-clear="{{$location_clear}}" + title="{{$location_set}}" + tabindex="6"> + <i class="fa fa-map-marker" aria-hidden="true"></i> + </button> + </span> +{{/if}} + <span role="presentation" id="profile-rotator-wrapper"> + <img role="presentation" id="profile-rotator" src="images/rotator.gif" alt="{{$wait}}" title="{{$wait}}" style="display: none;" /> + </span> + <span role="presentation" id="character-counter" class="grey text-info"></span> + {{if $preview}} + <button type="button" class="btn btn-defaul btn-sm" onclick="preview_comment({{$id}});" id="comment-edit-preview-link-{{$id}}" tabindex="5"><i class="fa fa-eye"></i> {{$preview}}</button> + {{/if}} + <button type="submit" class="btn btn-primary btn-sm" id="comment-edit-submit-{{$id}}" name="submit" tabindex="4"><i class="fa fa-envelope"></i> {{$submit}}</button> + </p> + + <div id="comment-edit-preview-{{$id}}" class="comment-edit-preview" style="display:none;"></div> + + <input type="hidden" name="group_allow" value="{{$group_allow}}" {{if $visibility == 'public'}}disabled{{/if}}/> + <input type="hidden" name="contact_allow" value="{{$contact_allow}}" {{if $visibility == 'public'}}disabled{{/if}}/> + <input type="hidden" name="group_deny" value="{{$group_deny}}" {{if $visibility == 'public'}}disabled{{/if}}/> + <input type="hidden" name="contact_deny" value="{{$contact_deny}}" {{if $visibility == 'public'}}disabled{{/if}}/> +{{if $type == 'post'}} + <h3>Visibility</h3> + <div class="panel-group" id="visibility-accordion" role="tablist" aria-multiselectable="true"> + <div class="panel panel-success"> + <div class="panel-heading" role="tab" id="visibility-public-heading" class="" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-public-panel" aria-expanded="true" aria-controls="visibility-public-panel"> + <label> + <input type="radio" name="visibility" id="visibility-public" value="public" {{if $visibility == 'public'}}checked{{/if}} style="display:none"> + <i class="fa fa-globe"></i> Public + </label> + </div> + <div id="visibility-public-panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="visibility-public-heading"> + <div class="panel-body"> + <p>This post will be sent to all your followers and can be seen in the community pages and by anyone with its link.</p> + </div> + </div> + </div> + <div class="panel panel-info"> + <div class="panel-heading" role="tab" id="visibility-custom-heading" class="collapsed" role="button" data-toggle="collapse" data-parent="#visibility-accordion" href="#visibility-custom-panel" aria-expanded="true" aria-controls="visibility-custom-panel"> + <label> + <input type="radio" name="visibility" id="visibility-custom" value="custom" {{if $visibility == 'custom'}}checked{{/if}} style="display:none"> + <i class="fa fa-lock"></i> Custom + </label> + </div> + <div id="visibility-custom-panel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="visibility-custom-heading"> + <div class="panel-body"> + <p>This post will be sent only to the people in the first box, to the exception of the people mentioned in the second box. + It won't be visible in the community pages nor with its link.</p> + + <div class="form-group"> + <label for="acl_allow">Deliver to:</label> + <input type="text" class="form-control input-lg" id="acl_allow"> + </div> + + <div class="form-group"> + <label for="acl_deny">Except to:</label> + <input type="text" class="form-control input-lg" id="acl_deny"> + </div> + </div> + </div> + </div> + </div> + <div class="jotplugins"> + {{$jotplugins nofilter}} + </div> +{{/if}} + </form> + </div> +</div> \ No newline at end of file diff --git a/view/theme/frio/css/style.css b/view/theme/frio/css/style.css index 19c4035f8a..30268701a0 100644 --- a/view/theme/frio/css/style.css +++ b/view/theme/frio/css/style.css @@ -1371,7 +1371,8 @@ section #jotOpen { #jot-text-wrap .preview textarea { width: 100%; } -#preview_profile-jot-text { +#preview_profile-jot-text, +.comment-edit-form .preview { position: relative; padding: 0px 10px; margin-top: -2px; @@ -1382,7 +1383,8 @@ section #jotOpen { background: #fff; color: #555; } -textarea#profile-jot-text:focus + #preview_profile-jot-text { +textarea#profile-jot-text:focus + #preview_profile-jot-text, +textarea.comment-edit-text:focus + .comment-edit-form .preview { border: 2px solid #6fdbe8; border-top: none; }