Switched to new component for labs feature image redesign

refs https://github.com/TryGhost/Team/issues/771

- added `<GhEditorFeatureImage>` for more flexibility than offered by `<GhImageUploaderWithPreview>`
  - updated to more closely match intended designs
- removed alt/caption support from `<GhImageUploaderWithPreview>` as it's no longer used
- fixed upload/delete/upload not working due to file input references getting out of sync
This commit is contained in:
Kevin Ansfield 2021-06-16 17:56:25 +01:00
parent 628a482c3a
commit 0d30077325
14 changed files with 200 additions and 51 deletions

View File

@ -0,0 +1,83 @@
<div
class="gh-editor-feature-image"
{{on "mouseover" (fn (mut this.isHovered) true)}}
{{on "mouseleave" (fn (mut this.isHovered) false)}}
>
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.setUploadedImage}}
as |uploader|
>
{{#if uploader.isUploading}}
{{!-- upload in progress --}}
{{uploader.progressBar}}
{{else if uploader.errors}}
{{!-- upload failed --}}
{{#each uploader.errors as |error|}}
<div class="gh-setting-error" data-test-error="feature-image">{{or error.context error.message}}</div>
{{/each}}
{{else if @image}}
{{!-- image is present --}}
<div>
<img src={{@image}}>
<button type="button" class="image-delete" title="Delete image" {{on "click" @clearImage}}>
{{svg-jar "trash"}}
</button>
</div>
<div class="relative">
{{#if this.isEditingAlt}}
<input
type="text"
placeholder={{@altPlaceholder}}
class="miw-100 tc bn form-text bg-transparent tracked-2 pr8 pl8"
name="alt"
value={{@alt}}
{{autofocus}}
{{on "input" this.onAltInput}}
>
{{else}}
<KoenigBasicHtmlInput
@html={{@caption}}
@placeholder={{if this.captionInputFocused "" "Add a caption to the feature image"}}
@class="miw-100 tc bn form-text bg-transparent pr8 pl8"
@name="caption"
@onChange={{@updateCaption}}
@onFocus={{fn (mut this.captionInputFocused) true}}
@onBlur={{fn (mut this.captionInputFocused) false}}
/>
{{/if}}
<button
title="Toggle between editing alt text and caption"
class="absolute right-0 bottom-0 ma2 pl1 pr1 ba br3 f8 sans-serif fw4 lh-title tracked-2 {{if this.isEditingAlt "bg-blue b--blue white" "bg-white b--midlightgrey midlightgrey"}}"
{{on "click" this.toggleAltEditing passive=true}}
>
Alt
</button>
</div>
{{else}}
{{!-- no-image state --}}
<div class="flex flex-row items-center {{if this.hideButton "invisible"}}">
<button type="button" class="gh-editor-feature-image-add-button" {{on "click" uploader.triggerFileDialog}}>+ Add feature image</button>
{{#if this.isHovered}}
<button type="button" class="gh-editor-feature-image-unsplash" {{on "click" this.toggleUnsplashSelector}}>{{svg-jar "unsplash"}}</button>
{{/if}}
</div>
{{/if}}
<div style="display:none">
<GhFileInput
@multiple={{false}}
@action={{uploader.setFiles}}
@accept={{uploader.imageMimeTypes}}
@onInsert={{uploader.registerFileInput}}
data-test-file-input="feature-image" />
</div>
</GhUploader>
</div>
{{#if this.showUnsplashSelector}}
<GhUnsplash
@select={{this.setUnsplashImage}}
@close={{this.toggleUnsplashSelector}}
/>
{{/if}}

View File

@ -0,0 +1,42 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class GhEditorFeatureImageComponent extends Component {
@tracked isEditingAlt = false;
@tracked isHovered = false;
@tracked captionInputFocused = false;
@tracked showUnsplashSelector = false;
get hideButton() {
return !this.isHovered && !this.args.forceButtonDisplay;
}
@action
setUploadedImage(results) {
if (results[0]) {
this.args.updateImage(results[0].url);
}
}
@action
setUnsplashImage({src, caption}) {
this.args.updateImage(src);
this.args.updateCaption(caption);
}
@action
toggleUnsplashSelector() {
this.showUnsplashSelector = !this.showUnsplashSelector;
}
@action
toggleAltEditing() {
this.isEditingAlt = !this.isEditingAlt;
}
@action
onAltInput(event) {
this.args.updateAlt(event.target.value);
}
}

View File

@ -41,6 +41,10 @@ export default XFileInput.extend({
let input = this.element.querySelector('.x-file--input');
input.removeAttribute('value');
input.value = null;
input.parentNode.replaceChild(input.cloneNode(true), input);
const clone = input.cloneNode(true);
input.parentNode.replaceChild(clone, input);
return clone;
}
});

View File

@ -1,7 +1,7 @@
{{#if @image}}
<div class="gh-image-uploader -with-image">
<div><img src={{@image}}></div>
<a class="image-cancel" title="Delete" {{on "click" @remove}}>
<a class="image-delete" title="Delete" {{on "click" @remove}}>
{{svg-jar "trash"}}
<span class="hidden">Delete</span>
</a>
@ -15,19 +15,4 @@
@uploadStarted={{optional @uploadStarted}}
@uploadFinished={{optional @uploadFinished}}
/>
{{/if}}
{{#if @includeMetadata}}
{{#if this.isEditingAlt}}
{{else}}
<KoenigBasicHtmlInput
@html={{@caption}}
@placeholder={{if this.captionInputFocused "" @captionPlaceholder}}
@class="miw-100 tc bn form-text bg-transparent pr8 pl8"
@name="caption"
@onChange={{@updateCaption}}
@onFocus={{fn (mut this.captionInputFocused) true}}
@onBlur={{fn (mut this.captionInputFocused) false}}
/>
{{/if}}
{{/if}}

View File

@ -1,7 +0,0 @@
import Component from '@glimmer/component';
import {tracked} from '@glimmer/tracking';
export default class GhImageUploaderWithPreviewComponent extends Component {
@tracked isEditingAlt = false;
@tracked captionInputFocused = false;
}

View File

@ -6,21 +6,16 @@
{{on "mouseup" this.focusEditor}}
>
{{#if (feature "featureImageMeta")}}
<div class="gh-editor-feature-image">
<GhImageUploaderWithPreview
@image={{@featureImage}}
@text="Upload feature image"
@allowUnsplash={{true}}
@includeMetadata={{true}}
@update={{@setFeatureImage}}
@remove={{fn @setFeatureImage null}}
@alt={{@featureImageAlt}}
@caption={{@featureImageCaption}}
@updateAlt={{@setFeatureImageAlt}}
@updateCaption={{@setFeatureImageCaption}}
@captionPlaceholder="Add a caption to the feature image"
/>
</div>
<GhEditorFeatureImage
@image={{@featureImage}}
@updateImage={{@setFeatureImage}}
@clearImage={{@clearFeatureImage}}
@alt={{@featureImageAlt}}
@updateAlt={{@setFeatureImageAlt}}
@caption={{@featureImageCaption}}
@updateCaption={{@setFeatureImageCaption}}
@forceButtonDisplay={{or this.titleIsHovered this.titleIsFocused}}
/>
{{/if}}
<GhTextarea
@ -34,6 +29,10 @@
@focus-out={{optional @onTitleBlur}}
@keyDown={{this.onTitleKeydown}}
@didCreateTextarea={{this.registerTitleElement}}
{{on "focus" (fn (mut this.titleIsFocused) true)}}
{{on "blur" (fn (mut this.titleIsFocused) false)}}
{{on "mouseover" (fn (mut this.titleIsHovered) true)}}
{{on "mouseleave" (fn (mut this.titleIsHovered) false)}}
data-test-editor-title-input={{true}}
/>

View File

@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class GhKoenigEditorComponent extends Component {
containerElement = null;
@ -7,6 +8,9 @@ export default class GhKoenigEditorComponent extends Component {
koenigEditor = null;
mousedownY = 0;
@tracked titleIsHovered = false;
@tracked titleIsFocused = false;
get title() {
return this.args.title === '(Untitled)' ? '' : this.args.title;
}
@ -27,6 +31,11 @@ export default class GhKoenigEditorComponent extends Component {
@action
registerTitleElement(element) {
this.titleElement = element;
// this is needed because focus event handler won't be fired if input has focus when rendering
if (this.titleElement === document.activeElement) {
this.titleIsFocused = true;
}
}
@action

View File

@ -134,7 +134,7 @@ export default Component.extend({
this._setFiles(files);
if (resetInput) {
resetInput();
this.fileInput = resetInput();
}
},

View File

@ -2,7 +2,7 @@
{{#if this.url}}
<div class="gh-image-uploader -with-image">
<div><img src={{this.url}}></div>
<a class="image-cancel" title="Delete" {{action 'removeImage'}}>
<a class="image-delete" title="Delete" {{action 'removeImage'}}>
{{svg-jar "trash"}}
<span class="hidden">Delete</span>
</a>

View File

@ -310,18 +310,36 @@ export default Controller.extend({
setFeatureImage(url) {
this.post.set('featureImage', url);
if (this.post.isDraft) {
this.autosave.perform();
}
},
clearFeatureImage() {
this.post.set('featureImage', null);
this.post.set('featureImageAlt', null);
this.post.set('featureImageCaption', null);
if (this.post.isDraft) {
this.autosave.perform();
}
},
setFeatureImageAlt(text) {
this.post.set('featureImageAlt', text);
if (this.post.isDraft) {
this.autosave.perform();
}
},
setFeatureImageCaption(html) {
this.post.set('featureImageCaption', html);
if (this.post.isDraft) {
this.autosave.perform();
}
}
},

View File

@ -32,7 +32,7 @@
line-height: 0;
}
.gh-image-uploader .image-cancel {
.image-delete {
position: absolute;
top: 10px;
right: 10px;
@ -50,12 +50,18 @@
box-shadow: rgba(255, 255, 255, 0.2) 0 0 0 1px;
}
.gh-image-uploader .image-cancel svg {
.image-delete svg {
width: 13px;
height: 13px;
fill: #fff;
}
.image-delete:hover {
color: #fff;
cursor: pointer;
background: var(--red);
}
.gh-image-uploader .upload-form {
flex-grow: 1;
display: flex;
@ -82,12 +88,6 @@
text-align: center;
}
.gh-image-uploader .image-cancel:hover {
color: #fff;
cursor: pointer;
background: var(--red);
}
.gh-image-uploader .failed {
margin: 1em 2em;
font-size: 16px;

View File

@ -381,13 +381,28 @@
}
.gh-editor-feature-image {
position: relative;
display: block;
width: 100%;
max-width: 740px;
min-height: auto;
margin-left: auto;
margin-right: auto;
margin-bottom: 1.6rem;
padding-bottom: 1.6rem;
}
.gh-editor-feature-image-add-button {
color: var(--midgrey);
}
.gh-editor-feature-image:hover .gh-editor-feature-image-add-button {
color: var(--green);
}
.gh-editor-feature-image-unsplash {
width: 1em;
height: 1em;
margin-left: 30px;
}
.gh-editor-title {

View File

@ -90,6 +90,7 @@
@setFeatureImage={{action "setFeatureImage"}}
@setFeatureImageAlt={{action "setFeatureImageAlt"}}
@setFeatureImageCaption={{action "setFeatureImageCaption"}}
@clearFeatureImage={{action "clearFeatureImage"}}
/>
<div class="gh-editor-wordcount-container {{if editor.headerClass "small"}}">

View File

@ -29,7 +29,7 @@ describe('Integration: Component: gh-image-uploader-with-preview', function () {
this.set('image', 'http://example.com/test.png');
await render(hbs`{{gh-image-uploader-with-preview image=image remove=(action remove)}}`);
await click('.image-cancel');
await click('.image-delete');
expect(remove.calledOnce).to.be.true;
});