Added gallery card to the editor

no issue
- added gallery card (initial implementation)
    - supports upto 9 images in gallery
    - max 3 images per row
- fixed gh-uploaded error handling for generic errors
- ignore jsconfig.json
This commit is contained in:
Kevin Ansfield 2018-08-29 18:17:49 +01:00 committed by Rish
parent 8a164b6846
commit 08179545ab
13 changed files with 474 additions and 21 deletions

View File

@ -50,3 +50,4 @@ Session.vim
/libpeerconnection.log
testem.log
/concat-stats-for
jsconfig.json

View File

@ -257,8 +257,8 @@ export default Component.extend({
}
let result = {
fileName: file.name,
message: error.payload.errors[0].message
message,
fileName: file.name
};
// TODO: check for or expose known error types?

View File

@ -0,0 +1,293 @@
import $ from 'jquery';
import Component from '@ember/component';
import EmberObject, {computed, set} from '@ember/object';
import countWords, {stripTags} from '../utils/count-words';
import layout from '../templates/components/koenig-card-gallery';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {htmlSafe} from '@ember/string';
import {isEmpty} from '@ember/utils';
import {run} from '@ember/runloop';
const MAX_IMAGES = 9;
const MAX_PER_ROW = 3;
export default Component.extend({
layout,
// attrs
files: null,
images: null,
payload: null,
isSelected: false,
isEditing: false,
imageExtensions: IMAGE_EXTENSIONS,
imageMimeTypes: IMAGE_MIME_TYPES,
// properties
errorMessage: null,
handlesDragDrop: true,
// closure actions
selectCard() { },
deselectCard() { },
editCard() { },
saveCard() { },
deleteCard() { },
moveCursorToNextSection() { },
moveCursorToPrevSection() { },
addParagraphAfterCard() { },
registerComponent() { },
counts: computed('payload.{caption,payload.images.[]}', function () {
let wordCount = 0;
let imageCount = this.payload.images.length;
if (this.payload.src) {
imageCount += 1;
}
if (this.payload.caption) {
wordCount += countWords(stripTags(this.payload.caption));
}
return {wordCount, imageCount};
}),
toolbar: computed('images.[]', function () {
let items = [];
if (!isEmpty(this.images)) {
items.push({
title: 'Add images',
icon: 'koenig/kg-add',
iconClass: 'fill-white',
action: run.bind(this, this._triggerFileDialog)
});
}
if (items.length > 0) {
return {items};
}
}),
imageRows: computed('images.@each.{src,previewSrc,width,height,row}', function () {
let rows = [];
let noOfImages = this.images.length;
this.images.forEach((image, idx) => {
let row = image.row;
let classes = ['relative', 'hide-child'];
if (noOfImages > 1 && (noOfImages % 3 === 1) && (idx === (noOfImages - 2))) {
row = row + 1;
}
if (!rows[row]) {
rows[row] = [];
} else {
classes.push('ml4');
}
if (row > 0) {
classes.push('mt4');
}
let styledImage = Object.assign({}, image);
let aspectRatio = (image.width || 1) / (image.height || 1);
styledImage.style = htmlSafe(`flex: ${aspectRatio} 1 0%`);
styledImage.classes = classes.join(' ');
rows[row].push(styledImage);
});
return rows;
}),
init() {
this._super(...arguments);
if (!this.payload || isEmpty(this.payload.images)) {
this._updatePayloadAttr('images', []);
}
this.images = this.payload.images.map(image => EmberObject.create(image));
this.registerComponent(this);
},
actions: {
insertImageIntoPayload(uploadResult) {
let image = this.images.findBy('fileName', uploadResult.fileName);
let idx = this.images.indexOf(image);
image.set('src', uploadResult.url);
this.payload.images.replace(idx, 1, [
Object.assign({}, image, {previewSrc: undefined})
]);
this._updatePayloadAttr('images', this.payload.images);
},
setFiles(files) {
this._startUpload(files);
},
insertImagePreviews(files) {
let count = this.images.length;
let row = Math.ceil(count / MAX_PER_ROW) - 1;
Array.from(files).forEach((file) => {
count = count + 1;
row = Math.ceil(count / MAX_PER_ROW) - 1;
let image = EmberObject.create({
row,
fileName: file.name
});
this.images.pushObject(image);
this.payload.images.push(Object.assign({}, image));
let reader = new FileReader();
reader.onload = (e) => {
let imageObject = new Image();
let previewSrc = htmlSafe(e.target.result);
if (!image.src) {
image.set('previewSrc', previewSrc);
}
imageObject.onload = () => {
// update current display images
image.set('width', imageObject.width);
image.set('height', imageObject.height);
// ensure width/height makes it into the payload images
let payloadImage = this.payload.images.findBy('fileName', image.fileName);
if (payloadImage) {
payloadImage.width = imageObject.width;
payloadImage.height = imageObject.height;
this._updatePayloadAttr('images', this.payload.images);
}
};
imageObject.src = previewSrc;
};
reader.readAsDataURL(file);
});
},
deleteImage(image) {
let localImage = this.images.findBy('fileName', image.fileName);
this.images.removeObject(localImage);
this.images.forEach((img, idx) => {
img.row = Math.ceil((idx + 1) / MAX_PER_ROW) - 1;
});
let payloadImage = this.payload.images.findBy('fileName', image.fileName);
this.payload.images.removeObject(payloadImage);
this.payload.images.forEach((img, idx) => {
img.row = Math.ceil((idx + 1) / MAX_PER_ROW) - 1;
});
this._updatePayloadAttr('images', this.payload.images);
},
updateCaption(caption) {
this._updatePayloadAttr('caption', caption);
},
/**
* 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
*/
triggerFileDialog(event) {
this._triggerFileDialog(event);
},
uploadFailed(uploadResult) {
let image = this.images.findBy('fileName', uploadResult.fileName);
this.images.removeObject(image);
let payloadImage = this.payload.images.findBy('fileName', uploadResult.fileName);
this.payload.images.removeObject(payloadImage);
this._updatePayloadAttr('images', this.payload.images);
this.set('errorMessage', 'Some images failed to upload');
},
clearErrorMessage() {
this.set('errorMessage', null);
}
},
_startUpload(files = []) {
let currentCount = this.images.length;
let allowedCount = (MAX_IMAGES - currentCount);
let strippedFiles = Array.prototype.slice.call(files, 0, allowedCount);
if (strippedFiles.length < files.length) {
this.set('errorMessage', 'Can contain only upto 9 images!');
}
this.set('files', strippedFiles);
},
dragOver(event) {
if (!event.dataTransfer) {
return;
}
// this is needed to work around inconsistencies with dropping files
// from Chrome's downloads bar
if (navigator.userAgent.indexOf('Chrome') > -1) {
let eA = event.dataTransfer.effectAllowed;
event.dataTransfer.dropEffect = (eA === 'move' || eA === 'linkMove') ? 'move' : 'copy';
}
event.stopPropagation();
event.preventDefault();
this.set('isDraggedOver', true);
},
dragLeave(event) {
event.preventDefault();
this.set('isDraggedOver', false);
},
drop(event) {
event.preventDefault();
this.set('isDraggedOver', false);
if (event.dataTransfer.files) {
this._startUpload(event.dataTransfer.files);
}
},
_updatePayloadAttr(attr, value) {
let payload = this.payload;
let save = this.saveCard;
set(payload, attr, value);
// update the mobiledoc and stay in edit mode
save(payload, false);
},
_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();
}
});

View File

@ -11,7 +11,7 @@ import MobiledocRange from 'mobiledoc-kit/utils/cursor/range';
import calculateReadingTime from '../utils/reading-time';
import countWords from '../utils/count-words';
import defaultAtoms from '../options/atoms';
import defaultCards from '../options/cards';
import defaultCards, {CARD_COMPONENT_MAP} from '../options/cards';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import layout from '../templates/components/koenig-editor';
import parserPlugins from '../options/parser-plugins';
@ -52,17 +52,6 @@ export const BLANK_DOC = {
]
};
// map card names to component names
export const CARD_COMPONENT_MAP = {
hr: 'koenig-card-hr',
image: 'koenig-card-image',
markdown: 'koenig-card-markdown',
'card-markdown': 'koenig-card-markdown', // backwards-compat with markdown editor
html: 'koenig-card-html',
code: 'koenig-card-code',
embed: 'koenig-card-embed'
};
export const CURSOR_BEFORE = -1;
export const CURSOR_AFTER = 1;
export const NO_CURSOR_MOVEMENT = 0;

View File

@ -1,5 +1,17 @@
import createComponentCard from '../utils/create-component-card';
// map card names to component names
export const CARD_COMPONENT_MAP = {
hr: 'koenig-card-hr',
image: 'koenig-card-image',
markdown: 'koenig-card-markdown',
'card-markdown': 'koenig-card-markdown', // backwards-compat with markdown editor
html: 'koenig-card-html',
code: 'koenig-card-code',
embed: 'koenig-card-embed',
gallery: 'koenig-card-gallery'
};
// TODO: move koenigOptions directly into cards now that card components register
// themselves so that they are available on card.component
export default [
@ -11,7 +23,8 @@ export default [
createComponentCard('image', {hasEditMode: false, deleteIfEmpty(card) {
return card.payload.imageSelector && !card.payload.src;
}}),
createComponentCard('markdown', {deleteIfEmpty: 'payload.markdown'})
createComponentCard('markdown', {deleteIfEmpty: 'payload.markdown'}),
createComponentCard('gallery', {hasEditMode: false})
];
export const CARD_MENU = [
@ -42,6 +55,14 @@ export const CARD_MENU = [
type: 'card',
replaceArg: 'html'
},
{
label: 'Gallery',
icon: 'koenig/kg-card-type-gallery',
iconClass: 'kg-card-type-native',
matches: ['gallery'],
type: 'card',
replaceArg: 'gallery'
},
{
label: 'Divider',
icon: 'koenig/kg-card-type-divider',

View File

@ -0,0 +1,93 @@
{{#koenig-card
tagName="figure"
class=(concat (kg-style "media-card") " " (kg-style "breakout" size="wide") " flex flex-column")
isSelected=isSelected
isEditing=isEditing
selectCard=(action selectCard)
deselectCard=(action deselectCard)
editCard=(action editCard)
toolbar=toolbar
hasEditMode=false
addParagraphAfterCard=addParagraphAfterCard
moveCursorToPrevSection=moveCursorToPrevSection
moveCursorToNextSection=moveCursorToNextSection
editor=editor
as |card|
}}
{{#gh-uploader
files=files
accept=imageMimeTypes
extensions=imageExtensions
onStart=(action "insertImagePreviews")
onUploadSuccess=(action "insertImageIntoPayload")
onUploadFailure=(action "uploadFailed")
as |uploader|
}}
<div class="relative{{unless images " bg-whitegrey-l2"}}">
{{#if imageRows}}
<div class="flex flex-column">
{{#each imageRows as |row|}}
<div class="flex flex-row justify-center">
{{#each row as |image|}}
<div style={{image.style}} class={{image.classes}}>
<img src={{or image.previewSrc image.src}} width={{image.width}} height={{image.height}} class="w-100 h-100 db">
<div class="bg-image-overlay-top child">
<div class="flex flex-row-reverse">
<button class="bg-white-90 pl3 pr3 br3" {{action "deleteImage" image}}>{{svg-jar "koenig/kg-trash" class="fill-darkgrey w4 h4"}}</button>
</div>
</div>
</div>
{{/each}}
</div>
{{/each}}
</div>
{{/if}}
{{#if (or uploader.isUploading (is-empty imageRows))}}
<div class="relative miw-100 flex items-center {{if (is-empty imageRows) "kg-media-placeholder ba b--whitegrey" "absolute absolute--fill bg-white-50"}}">
{{#if isDraggedOver}}
<span class="db center sans-serif fw7 f7 middarkgrey">
Drop 'em like it's hot 🔥
</span>
{{else if uploader.isUploading}}
{{uploader.progressBar}}
{{else if (is-empty imageRows)}}
<button class="flex flex-column items-center center sans-serif fw4 f7 middarkgrey pa8 pt6 pb6 kg-image-button" onclick={{action "triggerFileDialog"}}>
{{svg-jar "gallery-placeholder" class="kg-placeholder-gallery nudge-bottom--10"}}
<span class="mt2 midgrey">Click to select up to 9 images</span>
</button>
{{/if}}
</div>
{{else if 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 add up to 9 images
</span>
</div>
{{/if}}
{{#if (and errorMessage (not isDraggedOver))}}
<div class="absolute absolute--fill flex items-center bg-black-60">
<span class="db center sans-serif fw7 f7 pl2 pr2 bg-red white">
{{errorMessage}}.
<button onclick={{action "clearErrorMessage"}} style="text-decoration: underline !important">
Dismiss
</button>
</span>
</div>
{{/if}}
</div>
<div style="display:none">
{{gh-file-input multiple=true action=(action "setFiles") accept=imageMimeTypes}}
</div>
{{/gh-uploader}}
{{#if (or isSelected (clean-basic-html payload.caption))}}
{{card.captionInput
caption=payload.caption
update=(action "updateCaption")
placeholder="Type caption for gallery (optional)"
}}
{{/if}}
{{/koenig-card}}

View File

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

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M9 7h5c.552 0 1 .448 1 1s-.448 1-1 1H9v5c0 .552-.448 1-1 1s-1-.448-1-1V9H2c-.552 0-1-.448-1-1s.448-1 1-1h5V2c0-.552.448-1 1-1s1 .448 1 1v5z" fill="#FFF" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@ -0,0 +1,8 @@
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path fill="#FFF" fill-rule="nonzero" d="M1 2h25v24H1z"/>
<path d="M27.333 24.667v-22C27.333.889 26.444 0 24.667 0h-22C1.93 0 1.302.26.78.781.261 1.301 0 1.931 0 2.667v22c0 1.777.889 2.666 2.667 2.666h22c1.777 0 2.666-.889 2.666-2.666z" fill="#C064C4"/>
<path d="M19.113 12.933c-.126-.603-.497-.911-1.112-.925-.616-.014-1 .277-1.154.873l-2.123 4.667c-.051.112-.137.176-.26.192-.121.017-.221-.023-.3-.117l-1.467-1.752c-.34-.462-.796-.67-1.368-.625-.571.044-.99.32-1.254.829L6.76 21.6c-.133.222-.136.446-.008.671.128.226.321.338.58.338H22c.236 0 .42-.097.552-.293.132-.195.154-.402.067-.621l-3.506-8.762zM10.667 8c0 1.778-.89 2.667-2.667 2.667-1.778 0-2.667-.89-2.667-2.667 0-1.778.89-2.667 2.667-2.667 1.778 0 2.667.89 2.667 2.667z" fill="#FFF"/>
<path d="M30.667 8c-.369 0-.683.13-.943.39-.26.26-.39.575-.39.943v19.334c0 .444-.223.666-.667.666H9.333c-.889 0-1.333.445-1.333 1.334S8.444 32 9.333 32h20C31.111 32 32 31.111 32 29.333v-20C32 8.444 31.556 8 30.667 8z" fill="#C064C4"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -93,14 +93,14 @@
"ember-sticky-element": "0.2.2",
"ember-svg-jar": "1.2.0",
"ember-test-selectors": "1.0.0",
"ember-truth-helpers": "2.0.0",
"ember-truth-helpers": "2.1.0",
"ember-useragent": "0.6.1",
"ember-wormhole": "0.5.4",
"emberx-file-input": "1.2.1",
"eslint": "4.19.1",
"eslint-plugin-ghost": "0.0.25",
"fs-extra": "4.0.3",
"ghost-spirit": "0.0.36",
"ghost-spirit": "0.0.41",
"glob": "7.1.2",
"google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost",
"grunt": "1.0.3",

View File

@ -0,0 +1,14 @@
<svg width="142" height="133" viewBox="0 0 142 133" xmlns="http://www.w3.org/2000/svg">
<g stroke="#9BAEB8" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M116.564 6.731c-.359-3.414-3.417-5.891-6.831-5.532L6.731 12.025c-3.414.359-5.89 3.417-5.532 6.831l11.244 106.98c.36 3.414 3.417 5.89 6.831 5.532l103.002-10.826c3.414-.359 5.891-3.417 5.532-6.831L116.564 6.73z"/>
<path d="M109.22 13.398c-.111-1.055-.636-2.024-1.461-2.692-.825-.667-1.881-.98-2.936-.869L13.33 19.453c-1.056.111-2.024.637-2.692 1.461-.668.825-.98 1.881-.87 2.937l9.19 87.44 99.453-10.453-9.191-87.44zM17.168 94.25l32.55-3.42M84.1 87.216l32.52-3.418"/>
<g stroke-width="1.5">
<path d="M59.043 98.59c-9.3 1.04-6.382-12-19.314-16.619l52.269-5.493c-11.689 7.205-6.123 19.355-15.437 20.27l-17.518 1.841zM65.864 79.225C62.738 71.163 58.508 65.407 52.7 62.95M65.864 79.225c-2.227-13.332-1.061-23.69 8.078-32.121"/>
<path d="M74.758 66.434c.627 3.041.077 5.81-1.648 8.306-1.725 2.496-4.12 3.989-7.186 4.478-.626-3.04-.076-5.809 1.649-8.305 1.724-2.496 4.12-3.989 7.185-4.479zM62.584 45.334c2.198 1 3.686 2.615 4.464 4.844.778 2.228.618 4.418-.48 6.57-2.198-1.001-3.686-2.616-4.464-4.844-.778-2.229-.618-4.419.48-6.57zM91.775 45.977c-2.312 2.604-5.205 4.01-8.68 4.217-3.477.207-6.517-.845-9.12-3.156-2.605-2.312-4.01-5.205-4.218-8.68-.207-3.476.845-6.516 3.156-9.121l6.576-7.409c.658-.741 1.244-.539 1.303.45l.424 7.103 7.1-.422c1.2-.072 1.834.492 1.906 1.69l.424 7.101 7.101-.422c.99-.059 1.26.5.602 1.24l-6.575 7.409zM49.385 49.865c2.249 1.404 3.672 3.398 4.27 5.982.597 2.583.194 5-1.21 7.249-1.405 2.25-3.4 3.673-5.983 4.27-2.583.597-5 .194-7.249-1.21l-6.4-3.997c-.64-.4-.563-.865.173-1.035l5.28-1.22-1.22-5.28c-.206-.89.136-1.439 1.027-1.645l5.278-1.221-1.22-5.279c-.17-.735.215-1.01.855-.61l6.399 3.996z"/>
</g>
<g opacity=".6">
<path d="M20.698 98.047l4.933-.519M30.555 97.01l4.933-.518M40.413 95.975l4.933-.519M89.7 90.794l4.934-.518M99.558 89.758l4.933-.518M109.416 88.722l4.933-.518M21.65 107.106l4.933-.518M31.508 106.07l4.933-.518M41.365 105.034l4.933-.518M51.223 103.998l4.933-.518M61.08 102.962l4.933-.518M70.938 101.926l4.933-.518M80.795 100.89l4.933-.519M90.653 99.854l4.933-.519M100.51 98.818l4.933-.519M110.368 97.782l4.933-.519M18.075 102.88l3.061-.322M26.06 102.04l4.934-.518M35.918 101.005l4.933-.519M45.776 99.969l4.933-.519M85.206 95.824l4.933-.518M95.063 94.788l4.933-.518M104.921 93.752l4.933-.518M114.779 92.716l2.748-.289"/>
</g>
<path d="M64.504 126.583c19.467 1.362 53.066 4.343 62.482 5 3.424.24 6.394-2.341 6.634-5.766l7.503-107.307c.24-3.424-2.342-6.394-5.766-6.634-5.398-.377-18.289-1.224-18.289-1.224" opacity=".6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,24 @@
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {setupComponentTest} from 'ember-mocha';
describe('Integration: Component: koenig-card-gallery', function () {
setupComponentTest('koenig-card-gallery', {
integration: true
});
it.skip('renders', function () {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
// Template block usage:
// this.render(hbs`
// {{#koenig-card-gallery}}
// template content
// {{/koenig-card-gallery}}
// `);
this.render(hbs`{{koenig-card-gallery}}`);
expect(this.$()).to.have.length(1);
});
});

View File

@ -4229,7 +4229,13 @@ ember-text-measurer@^0.4.0:
dependencies:
ember-cli-babel "^6.8.2"
ember-truth-helpers@2.0.0, ember-truth-helpers@^2.0.0:
ember-truth-helpers@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.1.0.tgz#d4dab4eee7945aa2388126485977baeb33ca0798"
dependencies:
ember-cli-babel "^6.6.0"
ember-truth-helpers@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-2.0.0.tgz#f3e2eef667859197f1328bb4f83b0b35b661c1ac"
dependencies:
@ -5241,9 +5247,9 @@ ghost-ignition@^2.7.0:
prettyjson "^1.1.3"
uuid "^3.0.0"
ghost-spirit@0.0.36:
version "0.0.36"
resolved "https://registry.yarnpkg.com/ghost-spirit/-/ghost-spirit-0.0.36.tgz#9b6ebcc9e9b0c782e4abeb7849b87b47b86b5d22"
ghost-spirit@0.0.41:
version "0.0.41"
resolved "https://registry.yarnpkg.com/ghost-spirit/-/ghost-spirit-0.0.41.tgz#6436f264560a9a6287bf2d6fb8f9509004b9fe6a"
dependencies:
autoprefixer "8.2.0"
bluebird "^3.4.6"