0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-24 23:48:13 -05:00

Updated the first version of the product card

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

- Moved to in-line editing for the title/description
- Only the button is clickable. The button dissapears if the text/href isn't defined
- Added an image (the image can't be deleted for now)
This commit is contained in:
Thibaut Patel 2021-11-29 17:50:56 +01:00
parent 09cae883a8
commit 93dba3cb9a
2 changed files with 238 additions and 41 deletions

View file

@ -21,44 +21,99 @@
as |card|
>
{{#if @isEditing}}
<a href={{@payload.productUrl}} target="_blank" rel="noopener noreferrer">
<div><p>{{@payload.productTitle}}</p></div>
<div><p>{{@payload.productDescription}}</p></div>
</a>
<div>
<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 @payload.productImageSrc) " bg-whitegrey-l2"}}">
{{#if (or this.previewSrc @payload.productImageSrc)}}
<img src={{or this.previewSrc @payload.productImageSrc}} class="{{kg-style this.kgImgStyle sidebar=this.ui.hasSideNav}}" alt={{@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 @payload.productImageSrc))}}
<div class="relative miw-100 flex items-center {{if (not this.previewSrc @payload.productImageSrc) "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 @payload.productImageSrc)}}
<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>
<KoenigBasicHtmlInput
@html={{@payload.productTitle}}
@placeholder="Add a product title..."
@class="w-100 fw4 bn bg-transparent kg-product-card-title"
@name="title"
@defaultTag="h4"
@onChange={{action "setProductTitle"}}
@didCreateEditor={{action "registerEditor"}}
/>
<KoenigBasicHtmlInput
@html={{@payload.productDescription}}
@placeholder="Add a product description..."
@class="w-100 fw4 bn bg-transparent kg-product-card-description"
@name="description"
@defaultTag="p"
@onChange={{action "setProductDescription"}}
@didCreateEditor={{action "registerEditor"}}
/>
{{#if (and @payload.productButton @payload.productUrl)}}
<div>
<a href={{@payload.productUrl}} class="gh-btn gh-btn-accent" style="width: 100%" target="_blank" rel="noopener noreferrer"><span>{{@payload.productButton}}</span></a>
</div>
{{/if}}
</div>
<KoenigSettingsPanel>
<div class="kg-settings-panel-control">
<label class="kg-settings-panel-control-label" for="product-title-input">Product title</label>
<label class="kg-settings-panel-control-label" for="product-button-input">Button title</label>
<div class="kg-settings-panel-control-input">
<input
type="text"
class="gh-input"
id="product-title-input"
name="product-title"
value={{@payload.productTitle}}
placeholder="Add product title"
{{on "input" this.setProductTitle}}
{{on-key "Enter" (fn this.focusElement "#product-title-input")}}
id="product-button-input"
name="product-button"
value={{@payload.productButton}}
placeholder="Add product button"
{{on "input" this.setProductButton}}
{{on-key "Enter" (fn this.focusElement "#product-button-input")}}
>
</div>
</div>
<div class="kg-settings-panel-control">
<label class="kg-settings-panel-control-label" for="product-description-input">Product description</label>
<div class="kg-settings-panel-control-input">
<input
type="text"
class="gh-input"
id="product-description-input"
name="product-description"
value={{@payload.productDescription}}
placeholder="Add product description"
{{on "input" this.setProductDescription}}
{{on-key "Enter" (fn this.focusElement "#product-description-input")}}
>
</div>
</div>
<div class="kg-settings-panel-control">
<label class="kg-settings-panel-control-label" for="product-url-input">Product url</label>
<label class="kg-settings-panel-control-label" for="product-url-input">Button url</label>
<div class="kg-settings-panel-control-input">
<input
type="text"
@ -76,10 +131,17 @@
{{else}}
<a href={{@payload.productUrl}} target="_blank" rel="noopener noreferrer">
<div><p>{{@payload.productTitle}}</p></div>
<div><p>{{@payload.productDescription}}</p></div>
</a>
<div>
{{#if @payload.productImageSrc}}
<img src={{@payload.productImageSrc}} />
{{/if}}
<h4>{{@payload.productTitle}}</h4>
<p>{{@payload.productDescription}}</p>
{{#if (and @payload.productButton @payload.productUrl)}}
<div>
<a href={{@payload.productUrl}} class="gh-btn gh-btn-accent" style="width: 100%" target="_blank" rel="noopener noreferrer"><span>{{@payload.productButton}}</span></a>
</div>
{{/if}}
</div>
{{/if}}
</KoenigCard>

View file

@ -1,4 +1,10 @@
import $ from 'jquery';
import Browser from 'mobiledoc-kit/utils/browser';
import Component from '@glimmer/component';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
@ -12,16 +18,20 @@ export default class KoenigCardProductComponent extends Component {
@service membersUtils;
@service ui;
get isEmpty() {
const {productTitle, productDescription, productUrl} = this.args.payload;
files= null;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;
return isBlank(productTitle) && isBlank(productDescription) && isBlank(productUrl);
get isEmpty() {
const {productTitle, productDescription, productUrl, productButton, productImageSrc} = this.args.payload;
return isBlank(productTitle) && isBlank(productDescription) && isBlank(productUrl) && isBlank(productButton) && isBlank(productImageSrc);
}
get isIncomplete() {
const {productTitle, productDescription, productUrl} = this.args.payload;
const {productTitle, productDescription, productUrl, productButton, productImageSrc} = this.args.payload;
return isBlank(productTitle) || isBlank(productDescription) || isBlank(productUrl);
return isBlank(productTitle) || isBlank(productDescription) || isBlank(productUrl) || isBlank(productButton) || isBlank(productImageSrc);
}
get toolbar() {
@ -62,13 +72,40 @@ export default class KoenigCardProductComponent extends Component {
}
@action
setProductTitle(event) {
this._updatePayloadAttr('productTitle', event.target.value);
registerEditor(textReplacementEditor) {
let commands = {
'META+ENTER': run.bind(this, this._enter, 'meta'),
'CTRL+ENTER': run.bind(this, this._enter, 'ctrl')
};
Object.keys(commands).forEach((str) => {
textReplacementEditor.registerKeyCommand({
str,
run() {
return commands[str](textReplacementEditor, str);
}
});
});
this._textReplacementEditor = textReplacementEditor;
run.scheduleOnce('afterRender', this, this._afterRender);
}
_enter(modifier) {
if (this.args.isEditing && (modifier === 'meta' || (modifier === 'crtl' && Browser.isWin()))) {
this.args.editCard();
}
}
@action
setProductDescription(event) {
this._updatePayloadAttr('productDescription', event.target.value);
setProductTitle(content) {
this._updatePayloadAttr('productTitle', content);
}
@action
setProductDescription(content) {
this._updatePayloadAttr('productDescription', content);
}
@action
@ -76,6 +113,11 @@ export default class KoenigCardProductComponent extends Component {
this._updatePayloadAttr('productUrl', event.target.value);
}
@action
setProductButton(event) {
this._updatePayloadAttr('productButton', event.target.value);
}
@action
leaveEditMode() {
if (this.isEmpty) {
@ -99,4 +141,97 @@ export default class KoenigCardProductComponent extends Component {
// update the mobiledoc and stay in edit mode
this.args.saveCard?.(payload, false);
}
_afterRender() {
this._placeCursorAtEnd();
this._focusInput();
}
_placeCursorAtEnd() {
if (!this._textReplacementEditor) {
return;
}
let tailPosition = this._textReplacementEditor.post.tailPosition();
let rangeToSelect = tailPosition.toRange();
this._textReplacementEditor.selectRange(rangeToSelect);
}
_focusInput() {
let headingInput = this.element.querySelector('.kg-product-card-title .koenig-basic-html-input__editor');
if (headingInput) {
headingInput.focus();
}
}
@action
setPreviewSrc(files) {
let file = files[0];
if (file) {
let url = URL.createObjectURL(file);
this.previewSrc = url;
let imageElem = new Image();
imageElem.onload = () => {
// store width/height for use later to avoid saving an image card with no `src`
this._imageWidth = imageElem.naturalWidth;
this._imageHeight = imageElem.naturalHeight;
};
imageElem.src = url;
}
}
@action
resetSrcs() {
// this.set('previewSrc', null);
this.previewSrc = null;
// this._imageWidth = null;
// this._imageHeight = null;
// create undo snapshot when clearing
this.args.editor.run(() => {
this._updatePayloadAttr('productImageSrc', null);
// this._updatePayloadAttr('width', null);
// this._updatePayloadAttr('height', null);
});
}
@action
updateSrc(images) {
let [image] = images;
// create undo snapshot when image finishes uploading
this.args.editor.run(() => {
this._updatePayloadAttr('productImageSrc', image.url);
if (this._imageWidth && this._imageHeight) {
// this._updatePayloadAttr('width', this._imageWidth);
// this._updatePayloadAttr('height', this._imageHeight);
}
this._imageWidth = null;
this._imageHeight = null;
});
}
/**
* Opens a file selection dialog - Triggered by "Upload Image" buttons,
* searches for the hidden file input within the .gh-setting element
* containing the clicked button then simulates a click
* @param {MouseEvent} event - MouseEvent fired by the button click
*/
@action
triggerFileDialog(event) {
this._triggerFileDialog(event);
}
_triggerFileDialog(event) {
let target = event && event.target || this.element;
// simulate click to open file dialog
// using jQuery because IE11 doesn't support MouseEvent
$(target)
.closest('.__mobiledoc-card')
.find('input[type="file"]')
.click();
}
}