Added image card placeholder-style image selector and switched gifs card

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

- the `imageSelector` options in the image card have been expanded to have both a `component` and a `type` property. If the `type` is set to "placeholder" the related image selector component will be rendered in place of the default upload placeholder
  - updated `isEmpty()` so the card is always cleaned up if no src has been selected - prevents image selectors popping up when opening a post if it was saved whilst the image selector was open
  - updated close-selector behaviour to exit back to a blank paragraph so a different image embed type can be selected easily instead of leaving an image card that you then have to delete, create a new paragraph, and choose the embed type
- added `koenig-image-card/selector-tenor` child component
  - the same as `koenig-media-selector-tenor` except with the "Escape" key handling added
  - added as a separate component for now to allow for easy switching until we're settled on the selector type we want
This commit is contained in:
Kevin Ansfield 2021-11-24 16:17:02 +00:00
parent 7cd09eee96
commit 3f3b66b668
6 changed files with 246 additions and 77 deletions

View File

@ -6,91 +6,101 @@
@selectCard={{action this.selectCard}}
@deselectCard={{action this.deselectCard}}
@editCard={{action this.editCard}}
@saveAsSnippet={{this.saveAsSnippet}}
@saveAsSnippet={{if this.payload.src this.saveAsSnippet}}
@toolbar={{this.toolbar}}
@hasEditMode={{false}}
@onDeselect={{action "onDeselect"}}
@addParagraphAfterCard={{this.addParagraphAfterCard}}
@moveCursorToPrevSection={{this.moveCursorToPrevSection}}
@moveCursorToNextSection={{this.moveCursorToNextSection}}
@editor={{this.editor}}
as |card|
>
<GhUploader
@files={{this.files}}
@accept={{this.imageMimeTypes}}
@extensions={{this.imageExtensions}}
@onStart={{action "setPreviewSrc"}}
@onComplete={{action "updateSrc"}}
@onFailed={{action "resetSrcs"}}
as |uploader|
>
<div class="relative{{unless (or this.previewSrc this.payload.src) " bg-whitegrey-l2"}}">
{{#if (or this.previewSrc this.payload.src)}}
<img src={{or this.previewSrc this.payload.src}} class="{{kg-style this.kgImgStyle sidebar=this.ui.hasSideNav}}" alt={{this.payload.alt}}>
{{#if this.isDraggedOver}}
<div class="absolute absolute--fill flex items-center bg-black-60 pe-none">
<span class="db center sans-serif fw7 f7 white">
Drop to replace image
</span>
{{#if (eq this.imageSelector.type "placeholder")}}
{{!-- image selector placeholder (eg, gif browser) --}}
{{component this.imageSelector.component
searchTerm=this.payload.searchTerm
select=(action "selectFromImageSelector")
close=(action "closeImageSelector")}}
{{else}}
{{!-- standard image upload placeholder --}}
<GhUploader
@files={{this.files}}
@accept={{this.imageMimeTypes}}
@extensions={{this.imageExtensions}}
@onStart={{action "setPreviewSrc"}}
@onComplete={{action "updateSrc"}}
@onFailed={{action "resetSrcs"}}
as |uploader|
>
<div class="relative{{unless (or this.previewSrc this.payload.src) " bg-whitegrey-l2"}}">
{{#if (or this.previewSrc this.payload.src)}}
<img src={{or this.previewSrc this.payload.src}} class="{{kg-style this.kgImgStyle sidebar=this.ui.hasSideNav}}" alt={{this.payload.alt}}>
{{#if this.isDraggedOver}}
<div class="absolute absolute--fill flex items-center bg-black-60 pe-none">
<span class="db center sans-serif fw7 f7 white">
Drop to replace image
</span>
</div>
{{/if}}
{{/if}}
{{#if (or uploader.errors uploader.isUploading (not this.payload.src))}}
<div class="relative miw-100 flex items-center {{if (not this.previewSrc this.payload.src) "kg-media-placeholder ba b--whitegrey" "absolute absolute--fill bg-white-50"}}">
{{#if uploader.errors}}
<span class="db absolute top-0 right-0 left-0 pl2 pr2 bg-red white sans-serif f7">
{{uploader.errors.firstObject.message}}
</span>
{{/if}}
{{#if this.isDraggedOver}}
<span class="db center sans-serif fw7 f7 middarkgrey">
Drop it like it's hot 🔥
</span>
{{else if uploader.isUploading}}
{{uploader.progressBar}}
{{else if (not this.previewSrc this.payload.src)}}
<button class="flex flex-column items-center center sans-serif fw4 f7 middarkgrey pa16 pt14 pb14 kg-image-button" onclick={{action "triggerFileDialog"}}>
{{svg-jar this.placeholder class="kg-placeholder-image"}}
<span class="mt2 midgrey">Click to select an image</span>
</button>
{{/if}}
</div>
{{/if}}
</div>
<div style="display:none">
<GhFileInput @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
{{#if (or this.isSelected (clean-basic-html this.payload.caption))}}
{{#if this.isEditingAlt}}
<card.AltInput
@alt={{this.payload.alt}}
@update={{this.updateAlt}}
@placeholder="Type alt text for image (optional)" />
{{else}}
<card.CaptionInput
@caption={{this.payload.caption}}
@update={{this.updateCaption}}
@placeholder="Type caption for image (optional)" />
{{/if}}
{{#if (or uploader.errors uploader.isUploading (not this.payload.src))}}
<div class="relative miw-100 flex items-center {{if (not this.previewSrc this.payload.src) "kg-media-placeholder ba b--whitegrey" "absolute absolute--fill bg-white-50"}}">
{{#if uploader.errors}}
<span class="db absolute top-0 right-0 left-0 pl2 pr2 bg-red white sans-serif f7">
{{uploader.errors.firstObject.message}}
</span>
{{/if}}
{{#if this.isDraggedOver}}
<span class="db center sans-serif fw7 f7 middarkgrey">
Drop it like it's hot 🔥
</span>
{{else if uploader.isUploading}}
{{uploader.progressBar}}
{{else if (not this.previewSrc this.payload.src)}}
<button class="flex flex-column items-center center sans-serif fw4 f7 middarkgrey pa16 pt14 pb14 kg-image-button" onclick={{action "triggerFileDialog"}}>
{{svg-jar this.placeholder class="kg-placeholder-image"}}
<span class="mt2 midgrey">Click to select an image</span>
</button>
{{/if}}
</div>
{{#if this.isSelected}}
<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-green b--green white" "bg-white b--midlightgrey midlightgrey"}}"
{{on "click" this.toggleAltEditing passive=true}}
>
Alt
</button>
{{/if}}
</div>
<div style="display:none">
<GhFileInput @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
{{#if (or this.isSelected (clean-basic-html this.payload.caption))}}
{{#if this.isEditingAlt}}
<card.AltInput
@alt={{this.payload.alt}}
@update={{this.updateAlt}}
@placeholder="Type alt text for image (optional)" />
{{else}}
<card.CaptionInput
@caption={{this.payload.caption}}
@update={{this.updateCaption}}
@placeholder="Type caption for image (optional)" />
{{/if}}
{{#if this.isSelected}}
<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-green b--green white" "bg-white b--midlightgrey midlightgrey"}}"
{{on "click" this.toggleAltEditing passive=true}}
>
Alt
</button>
{{/if}}
{{/if}}
{{#if this.imageSelector}}
{{component this.imageSelector
{{#if (eq this.imageSelector.type "modal")}}
{{component this.imageSelector.component
searchTerm=this.payload.searchTerm
select=(action "selectFromImageSelector")
close=(action "closeImageSelector")}}

View File

@ -4,6 +4,7 @@ import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {NO_CURSOR_MOVEMENT} from './koenig-editor';
import {action, computed, set, setProperties} from '@ember/object';
import {utils as ghostHelperUtils} from '@tryghost/helpers';
import {isEmpty} from '@ember/utils';
@ -39,15 +40,19 @@ export default Component.extend({
addParagraphAfterCard() {},
registerComponent() {},
isEmpty: computed('payload.{imageSelector,src}', function () {
return !this.payload.imageSelector && !this.payload.src;
}),
isEmpty: computed.not('payload.src'),
imageSelector: computed('payload.imageSelector', function () {
let selector = this.payload.imageSelector;
let imageSelectors = {
unsplash: 'gh-unsplash',
tenor: 'gh-tenor'
unsplash: {
component: 'gh-unsplash',
type: 'modal'
},
tenor: {
component: 'koenig-card-image/selector-tenor',
type: 'placeholder'
}
};
return imageSelectors[selector];
@ -247,9 +252,19 @@ export default Component.extend({
});
},
closeImageSelector() {
closeImageSelector(reselectParagraph = true) {
if (!this.payload.src) {
return this.deleteCard();
return this.editor.run((postEditor) => {
let {builder} = postEditor;
let cardSection = this.env.postModel;
let p = builder.createMarkupSection('p');
postEditor.replaceSection(cardSection, p);
if (reselectParagraph) {
postEditor.setRange(p.tailPosition());
}
});
}
set(this.payload, 'imageSelector', undefined);
@ -266,6 +281,12 @@ export default Component.extend({
cancelEditLink() {
this.set('isEditing', false);
this.set('isEditingLink', false);
},
onDeselect() {
if (this.imageSelector?.type === 'placeholder' && !this.payload.src) {
this.send('closeImageSelector', false);
}
}
},

View File

@ -0,0 +1,66 @@
<div class="kg-media-selector-browser" {{did-insert this.didInsertContainer}} {{on-key "Escape" @close}}>
{{!-- static header --}}
<header class="kg-media-selector-heading">
<span class="gh-input-icon">
{{svg-jar "search"}}
<GhTextInput
@class="kg-media-selector-searchbox"
@name="searchKeyword"
@placeholder="Search Tenor for GIFs"
@tabindex="1"
@shouldFocus={{true}}
@autocorrect="off"
@value={{readonly this.tenor.searchTerm}}
@input={{this.search}}
/>
</span>
</header>
{{!-- content container --}}
<div class="kg-media-selector-content">
{{!-- scrollable image container --}}
<div class="kg-media-selector-mediagrid">
{{#if this.tenor.gifs}}
<section class="gh-unsplash-grid">
{{#each this.tenor.columns as |gifs|}}
<div class="gh-unsplash-grid-column">
{{#each gifs as |gif|}}
<GhTenor::Gif
@gif={{gif}}
@select={{fn this.select gif}} />
{{/each}}
</div>
{{/each}}
</section>
{{else if (and this.tenor.searchTerm (not this.tenor.error this.tenor.isLoading))}}
<section class="gh-unsplash-error h-100 flex items-center justify-center pb30">
<div>
<img class="gh-unsplash-error-404" src="assets/img/unsplash-404.png" alt="No photos found" />
<h4>No gifs found for '{{this.tenor.searchTerm}}'</h4>
</div>
</section>
{{/if}}
{{#if this.tenor.error}}
<section class="gh-unsplash-error h-100 flex items-center justify-center pb30">
<div>
<img class="gh-unsplash-error-404" src="assets/img/unsplash-404.png" alt="Network error" />
<h4>{{this.tenor.error}} (<a href="#" {{on "click" this.tenor.retry}}>retry</a>)</h4>
</div>
</section>
{{/if}}
{{#if this.tenor.isLoading}}
<div class="gh-unsplash-loading h-100 flex items-center justify-center pb30">
<div class="gh-loading-spinner"></div>
</div>
{{/if}}
{{#unless this.tenor.isLoading}}
<GhScrollTrigger
@enter={{this.tenor.loadNextPage}}
@triggerOffset={{1000}} />
{{/unless}}
</div>
</div>
</div>

View File

@ -0,0 +1,66 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
// number of columns based on selector container width
const TWO_COLUMN_WIDTH = 540;
const THREE_COLUMN_WIDTH = 940;
export default class KoenigCardImageTenorSelector extends Component {
@service tenor;
willDestroy() {
super.willDestroy(...arguments);
this._resizeObserver?.disconnect();
}
@action
search(event) {
const term = event.target.value;
this.tenor.updateSearch(term);
}
@action
didInsertContainer(containerElem) {
if (this.args.searchTerm !== this.tenor.searchTerm) {
this.tenor.updateSearch(this.args.searchTerm);
}
this._resizeObserver = new ResizeObserver((entries) => {
const [containerEntry] = entries;
const contentBoxSize = Array.isArray(containerEntry.contentBoxSize) ? containerEntry.contentBoxSize[0] : containerEntry.contentBoxSize;
const width = contentBoxSize.inlineSize;
let columns = 4;
if (width <= TWO_COLUMN_WIDTH) {
columns = 2;
} else if (width <= THREE_COLUMN_WIDTH) {
columns = 3;
}
this.tenor.changeColumnCount(columns);
});
this._resizeObserver.observe(containerElem);
}
@action
select(gif, event) {
event?.preventDefault();
event?.stopPropagation();
const media = gif.media[0].gif;
const payload = {
src: media.url,
width: media.dims[0],
height: media.dims[1],
caption: '',
type: 'gif'
};
this.args.selector.insertCard('image', payload);
this.args.selector.close();
}
}

View File

@ -185,7 +185,12 @@ export const CARD_MENU = [
desc: '/gif [search term]',
iconClass: 'kg-card-type-unsplash',
matches: ['gif', 'giphy', 'tenor'],
type: 'selector',
type: 'card',
replaceArg: 'image',
params: ['searchTerm'],
payload: {
imageSelector: 'tenor'
},
selectorComponent: 'koenig-media-selector-tenor',
isAvailable: ['feature.gifsCard', 'config.tenor.apiKey']
},

View File

@ -0,0 +1 @@
export {default} from 'koenig-editor/components/koenig-card-image/selector-tenor';