mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 06:35:49 +03:00
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:
parent
7cd09eee96
commit
3f3b66b668
@ -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")}}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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']
|
||||
},
|
||||
|
@ -0,0 +1 @@
|
||||
export {default} from 'koenig-editor/components/koenig-card-image/selector-tenor';
|
Loading…
Reference in New Issue
Block a user