mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Added first pass of product card
refs https://github.com/TryGhost/Team/issues/1233 - adds card that contains a a title, description wrapped in a link - this hasn't been designed yet so I didn't add any css
This commit is contained in:
parent
98da36ce2a
commit
0285f02fb6
4 changed files with 202 additions and 2 deletions
|
@ -0,0 +1,85 @@
|
|||
<KoenigCard
|
||||
@env={{@env}}
|
||||
@class={{concat (kg-style "container-card") " kg-button-card mih10 miw-100 relative"}}
|
||||
@headerOffset={{@headerOffset}}
|
||||
@toolbar={{this.toolbar}}
|
||||
@payload={{@payload}}
|
||||
@isSelected={{@isSelected}}
|
||||
@isEditing={{@isEditing}}
|
||||
@selectCard={{@selectCard}}
|
||||
@deselectCard={{@deselectCard}}
|
||||
@editCard={{@editCard}}
|
||||
@hasEditMode={{true}}
|
||||
@saveCard={{@saveCard}}
|
||||
@saveAsSnippet={{@saveAsSnippet}}
|
||||
@onLeaveEdit={{this.leaveEditMode}}
|
||||
@addParagraphAfterCard={{@addParagraphAfterCard}}
|
||||
@moveCursorToPrevSection={{@moveCursorToPrevSection}}
|
||||
@moveCursorToNextSection={{@moveCursorToNextSection}}
|
||||
@editor={{@editor}}
|
||||
{{did-insert this.registerElement}}
|
||||
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>
|
||||
|
||||
<KoenigSettingsPanel>
|
||||
<div class="kg-settings-panel-control">
|
||||
<label class="kg-settings-panel-control-label" for="product-title-input">Product 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")}}
|
||||
>
|
||||
</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>
|
||||
<div class="kg-settings-panel-control-input">
|
||||
<input
|
||||
type="text"
|
||||
class="gh-input"
|
||||
id="product-url-input"
|
||||
name="product-url"
|
||||
value={{@payload.productUrl}}
|
||||
placeholder="Add product url"
|
||||
{{on "input" this.setProductUrl}}
|
||||
{{on-key "Enter" (fn this.focusElement "#product-url-input")}}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</KoenigSettingsPanel>
|
||||
|
||||
{{else}}
|
||||
|
||||
<a href={{@payload.productUrl}} target="_blank" rel="noopener noreferrer">
|
||||
<div><p>{{@payload.productTitle}}</p></div>
|
||||
<div><p>{{@payload.productDescription}}</p></div>
|
||||
</a>
|
||||
|
||||
{{/if}}
|
||||
</KoenigCard>
|
|
@ -0,0 +1,102 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {run} from '@ember/runloop';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {set} from '@ember/object';
|
||||
|
||||
export default class KoenigCardProductComponent extends Component {
|
||||
@service config;
|
||||
@service feature;
|
||||
@service store;
|
||||
@service membersUtils;
|
||||
@service ui;
|
||||
|
||||
get isEmpty() {
|
||||
const {productTitle, productDescription, productUrl} = this.args.payload;
|
||||
|
||||
return isBlank(productTitle) && isBlank(productDescription) && isBlank(productUrl);
|
||||
}
|
||||
|
||||
get isIncomplete() {
|
||||
const {productTitle, productDescription, productUrl} = this.args.payload;
|
||||
|
||||
return isBlank(productTitle) || isBlank(productDescription) || isBlank(productUrl);
|
||||
}
|
||||
|
||||
get toolbar() {
|
||||
if (this.args.isEditing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
items: [{
|
||||
buttonClass: 'fw4 flex items-center white',
|
||||
icon: 'koenig/kg-edit',
|
||||
iconClass: 'fill-white',
|
||||
title: 'Edit',
|
||||
text: '',
|
||||
action: run.bind(this, this.args.editCard)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.args.registerComponent(this);
|
||||
|
||||
const payloadDefaults = {};
|
||||
|
||||
Object.entries(payloadDefaults).forEach(([key, value]) => {
|
||||
if (this.args.payload[key] === undefined) {
|
||||
this._updatePayloadAttr(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// required for snippet rects to be calculated - editor reaches in to component,
|
||||
// expecting a non-Glimmer component with a .element property
|
||||
@action
|
||||
registerElement(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
@action
|
||||
setProductTitle(event) {
|
||||
this._updatePayloadAttr('productTitle', event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
setProductDescription(event) {
|
||||
this._updatePayloadAttr('productDescription', event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
setProductUrl(event) {
|
||||
this._updatePayloadAttr('productUrl', event.target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
leaveEditMode() {
|
||||
if (this.isEmpty) {
|
||||
// afterRender is required to avoid double modification of `isSelected`
|
||||
// TODO: see if there's a way to avoid afterRender
|
||||
run.scheduleOnce('afterRender', this, this.args.deleteCard);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
focusElement(selector, event) {
|
||||
event.preventDefault();
|
||||
document.querySelector(selector)?.focus();
|
||||
}
|
||||
|
||||
_updatePayloadAttr(attr, value) {
|
||||
let payload = this.args.payload;
|
||||
|
||||
set(payload, attr, value);
|
||||
|
||||
// update the mobiledoc and stay in edit mode
|
||||
this.args.saveCard?.(payload, false);
|
||||
}
|
||||
}
|
|
@ -20,7 +20,8 @@ export const CARD_COMPONENT_MAP = {
|
|||
paywall: 'koenig-card-paywall',
|
||||
file: 'koenig-card-file',
|
||||
audio: 'koenig-card-audio',
|
||||
video: 'koenig-card-video'
|
||||
video: 'koenig-card-video',
|
||||
product: 'koenig-card-product'
|
||||
};
|
||||
|
||||
// map card names to generic icons (used for ghost elements when dragging)
|
||||
|
@ -43,7 +44,8 @@ export const CARD_ICON_MAP = {
|
|||
paywall: 'koenig/kg-card-type-divider',
|
||||
file: 'koenig/kg-card-type-divider',
|
||||
audio: 'koenig/kg-card-type-divider',
|
||||
video: 'koenig/kg-card-type-video'
|
||||
video: 'koenig/kg-card-type-video',
|
||||
product: 'koenig/kg-card-type-product'
|
||||
};
|
||||
|
||||
// TODO: move koenigOptions directly into cards now that card components register
|
||||
|
@ -67,6 +69,7 @@ export default [
|
|||
createComponentCard('file'),
|
||||
createComponentCard('audio'),
|
||||
createComponentCard('video'),
|
||||
createComponentCard('product'),
|
||||
createComponentCard('paywall', {hasEditMode: false, selectAfterInsert: false})
|
||||
];
|
||||
|
||||
|
@ -225,6 +228,15 @@ export const CARD_MENU = [
|
|||
type: 'card',
|
||||
replaceArg: 'video',
|
||||
isAvailable: 'feature.videoCard'
|
||||
},
|
||||
{
|
||||
label: 'Product',
|
||||
icon: 'koenig/kg-card-type-other',
|
||||
desc: 'Add a product recommendation',
|
||||
matches: ['product'],
|
||||
type: 'card',
|
||||
replaceArg: 'product',
|
||||
isAvailable: 'feature.productCard'
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export {default} from 'koenig-editor/components/koenig-card-product';
|
Loading…
Add table
Reference in a new issue