diff --git a/ghost/admin/app/styles/components/koenig.css b/ghost/admin/app/styles/components/koenig.css index 6252a4fa3d..7567633c12 100644 --- a/ghost/admin/app/styles/components/koenig.css +++ b/ghost/admin/app/styles/components/koenig.css @@ -1941,6 +1941,12 @@ button.emoji-picker__category-button.active { margin-top: 20px; } +/* Before/After card +/* --------------------------------------------------------------- */ +.kg-before-after-card img { + max-width: none; +} + /* Upload cards: audio and file /* --------------------------------------------------------------- */ diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.hbs b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.hbs new file mode 100644 index 0000000000..3e24bb328a --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.hbs @@ -0,0 +1,191 @@ + + +
+
+ {{#if @payload.beforeImage}} + + {{/if}} +
+
+ {{#if @payload.afterImage}} + + {{/if}} +
+
+ +
+ +
+
+ + {{#if @isEditing}} + +
+
After Image
+
+
+ {{#if @payload.afterImage}} + + {{else}} + + {{/if}} +
+
+
+
+
Before Image
+
+
+ {{#if @payload.beforeImage}} + + {{else}} + + {{/if}} +
+
+
+
+
Layout
+
+
+ + +
+
+
+
+
Orientation
+
+
+ + +
+
+
+
+
Starting position
+
+ +
+
+
+ {{/if}} + {{#if (or @isSelected (clean-basic-html @payload.caption))}} + + {{/if}} +
diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.js new file mode 100644 index 0000000000..0a19c7d944 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-before-after.js @@ -0,0 +1,230 @@ +import $ from 'jquery'; +import Component from '@glimmer/component'; +import { + IMAGE_EXTENSIONS, + IMAGE_MIME_TYPES +} from 'ghost-admin/components/gh-image-uploader'; +import {action} from '@ember/object'; +import {utils as ghostHelperUtils} from '@tryghost/helpers'; +import {run} from '@ember/runloop'; +import {tracked} from '@glimmer/tracking'; + +const {countWords} = ghostHelperUtils; + +export default class KoenigCardBeforeAfterComponent extends Component { + @tracked imageWidth; + files = null; + selectingFile = false; + imageMimeTypes = IMAGE_MIME_TYPES; + imageExtensions = IMAGE_EXTENSIONS; + + get overlayStyle() { + if (this.args.payload.orientation === 'horizontal') { + return `width: ${this.args.payload.startingPosition}%`; + } + if (this.args.payload.orientation === 'vertical') { + return `height: ${this.args.payload.startingPosition}%`; + } + return null; + } + + get wordCount() { + return countWords(this.payload.caption); + } + + 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) + }] + }; + } + + updateImageDimensions() { + let beforeImage = this.args.payload.beforeImage; + let afterImage = this.args.payload.afterImage; + + let smallestImageWidth = Math.min( + beforeImage ? beforeImage.width : Infinity, + afterImage ? afterImage.width : Infinity + ); + + try { + this.imageWidth = Math.min( + smallestImageWidth, + parseInt(getComputedStyle(this.element).getPropertyValue('width')) + ); + } catch (err) { + this.imageWidth = Math.min( + smallestImageWidth, + 0 + ); + } + } + + constructor(owner, args) { + super(owner, args); + args.registerComponent(this); + if (!args.payload.orientation) { + args.payload.orientation = 'horizontal'; + } + if (!args.payload.cardWidth) { + args.payload.cardWidth = 'wide'; + } + if (!args.payload.startingPosition) { + args.payload.startingPosition = 50; + } + if (!args.payload.caption) { + args.payload.caption = null; + } + } + + _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"]') + .trigger('click'); + } + + setupListeners(element) { + let observer = new MutationObserver(() => { + // @TODO Update on specific mutations + this.updateImageDimensions(); + }); + let config = {attributes: true, childList: true, subtree: true}; + observer.observe(element, config); + this.updateImageDimensions(); + } + + // 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; + this.setupListeners(element); + } + + @action + uploadStart(file) { + // @TODO Handle in progress uploads + let existingImage = this.args.payload.afterImage || this.args.payload.beforeImage; + + return new Promise((resolve, reject) => { + let objectURL = URL.createObjectURL(file); + let image = new Image(); + image.addEventListener('load', () => { + let id = this.selectingFile; + this.selectingFile = false; + let metadata = { + aspectRatio: image.naturalWidth / image.naturalHeight, + id: id + }; + if (existingImage) { + if (metadata.aspectRatio !== existingImage.aspectRatio) { + reject(new Error('Before/After images must have the same aspect ratio')); + } + } + resolve(metadata); + }); + image.src = objectURL; + }); + } + + @action + uploadSuccess(file, metadata) { + let image = new Image(); + image.addEventListener('load', () => { + let imageData = { + src: file.url, + aspectRatio: metadata.aspectRatio, + width: image.naturalWidth, + height: image.naturalHeight + }; + let prop = `${metadata.id}Image`; + this.args.payload[prop] = imageData; + }); + image.src = file.url; + } + + @action + setLayoutWide() { + this.args.payload.cardWidth = 'wide'; + } + + @action + setLayoutFull() { + this.args.payload.cardWidth = 'full'; + } + + @action + setOrientationHorizontal() { + this.args.payload.orientation = 'horizontal'; + } + + @action + setOrientationVertical() { + this.args.payload.orientation = 'vertical'; + } + + @action + setStartingPosition(event) { + this.args.payload.startingPosition = Math.min(100, Math.max(0, parseInt(event.target.value))); + } + + @action + removeBeforeImage() { + this.args.payload.beforeImage = null; + } + + @action + selectBeforeImage() { + this.selectingFile = 'before'; + this._triggerFileDialog(); + } + + @action + selectAfterImage() { + this.selectingFile = 'after'; + this._triggerFileDialog(); + } + + @action + removeAfterImage() { + this.args.payload.afterImage = null; + } + + @action + uploadFailed() { + } + + @action + handleErrors() { + } + + @action + setCaption(caption) { + this.args.payload.caption = caption; + } + + @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); + } + } +} diff --git a/ghost/admin/lib/koenig-editor/addon/options/cards.js b/ghost/admin/lib/koenig-editor/addon/options/cards.js index 17a689b6c8..6e786ac95b 100644 --- a/ghost/admin/lib/koenig-editor/addon/options/cards.js +++ b/ghost/admin/lib/koenig-editor/addon/options/cards.js @@ -21,7 +21,8 @@ export const CARD_COMPONENT_MAP = { video: 'koenig-card-video', audio: 'koenig-card-audio', file: 'koenig-card-file', - product: 'koenig-card-product' + product: 'koenig-card-product', + 'before-after': 'koenig-card-before-after' }; // map card names to generic icons (used for ghost elements when dragging) @@ -45,7 +46,8 @@ export const CARD_ICON_MAP = { video: 'koenig/kg-card-type-video', audio: 'koenig/kg-card-type-audio', file: 'koenig/kg-card-type-file', - product: 'koenig/kg-card-type-product' + product: 'koenig/kg-card-type-product', + 'before-after': 'koenig-card-before-after' }; // TODO: move koenigOptions directly into cards now that card components register @@ -70,7 +72,8 @@ export default [ createComponentCard('audio'), createComponentCard('file'), createComponentCard('product'), - createComponentCard('paywall', {hasEditMode: false, selectAfterInsert: false}) + createComponentCard('paywall', {hasEditMode: false, selectAfterInsert: false}), + createComponentCard('before-after') ]; export const CARD_MENU = [ @@ -245,6 +248,15 @@ export const CARD_MENU = [ type: 'card', replaceArg: 'product', isAvailable: 'feature.productCard' + }, + { + label: 'Before/After', + icon: 'koenig/kg-card-type-other', + desc: 'Compare two images', + matches: ['before', 'after', 'compare'], + type: 'card', + replaceArg: 'before-after', + isAvailable: 'feature.beforeAfterCard' }] }, { diff --git a/ghost/admin/lib/koenig-editor/app/components/koenig-card-before-after.js b/ghost/admin/lib/koenig-editor/app/components/koenig-card-before-after.js new file mode 100644 index 0000000000..9c69405077 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/app/components/koenig-card-before-after.js @@ -0,0 +1 @@ +export {default} from 'koenig-editor/components/koenig-card-before-after';