mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
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:
parent
628a482c3a
commit
0d30077325
83
ghost/admin/app/components/gh-editor-feature-image.hbs
Normal file
83
ghost/admin/app/components/gh-editor-feature-image.hbs
Normal 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}}
|
42
ghost/admin/app/components/gh-editor-feature-image.js
Normal file
42
ghost/admin/app/components/gh-editor-feature-image.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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}}
|
@ -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;
|
||||
}
|
@ -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}}
|
||||
/>
|
||||
|
||||
|
@ -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
|
||||
|
@ -134,7 +134,7 @@ export default Component.extend({
|
||||
this._setFiles(files);
|
||||
|
||||
if (resetInput) {
|
||||
resetInput();
|
||||
this.fileInput = resetInput();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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"}}">
|
||||
|
@ -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;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user