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

Koenig - Image card

refs https://github.com/TryGhost/Ghost/issues/9311
- add actions for cursor movement and pass through to card components
    - `moveCursorToNextSection` deselects card and places cursor at beginning of next section, useful for caption inputs where <kbd>down arrow</kbd> or <kbd>right arrow</kbd> should move the cursor out of the input & card. Also creates an empty paragraph before moving the cursor if for some reason an empty paragraph doesn't exist after the last card in the doc
    - `moveCursorToPrevSection` deselects card and places cursor at end of previous section, useful for caption inputs where <kbd>up arrow</kbd> or <kbd>left arrow</kbd> should move the cursor out of the input & card
    - `addParagraphAfterCard` deselects card, creates a new paragraph after the card and moves the cursor to it. Useful for caption inputs where <kbd>enter</kbd> should have the same behaviour as if it was pressed whilst the card is selected
- modify `{{gh-uploader}}` so that it passes the FileList to it's `onStart` closure action. Useful for displaying previews when uploading images
- modify `{{koenig-card}}` toolbar display so that it can display text as well as icon buttons
- update `{{koenig-card-image}}` so that it has a full image uploader and caption input
This commit is contained in:
Kevin Ansfield 2018-02-22 20:41:40 +00:00
parent 4f1238d002
commit 5d552202f6
7 changed files with 319 additions and 7 deletions

View file

@ -183,7 +183,7 @@ export default Component.extend({
let uploads = [];
this._reset();
this.onStart();
this.onStart(files);
// NOTE: for...of loop results in a transpilation that errors in Edge,
// once we drop IE11 support we should be able to use native for...of

View file

@ -105,6 +105,17 @@ button, .btn-base {
.kg-card-selected {
border: 1px solid var(--blue);
}
/* force a 16:10 aspect ratio */
.kg-media-placeholder:before {
content: "";
float: left;
padding-bottom: 62.5%;
}
.kg-media-placeholder:after {
clear: left;
content: " ";
display: table;
}
/* Client styles */

View file

@ -1,7 +1,184 @@
import $ from 'jquery';
import Component from '@ember/component';
import layout from '../templates/components/koenig-card-image';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {computed} from '@ember/object';
import {htmlSafe} from '@ember/string';
import {run} from '@ember/runloop';
import {set} from '@ember/object';
export default Component.extend({
tagName: '',
layout
layout,
// attrs
payload: null,
isSelected: false,
isEditing: false,
imageExtensions: IMAGE_EXTENSIONS,
imageMimeTypes: IMAGE_MIME_TYPES,
// closure actions
selectCard() {},
editCard() {},
saveCard() {},
moveCursorToNextSection() {},
moveCursorToPrevSection() {},
addParagraphAfterCard() {},
toolbar: computed('payload.src', function () {
if (this.get('payload.src')) {
return {
items: [{
title: 'Replace',
action: run.bind(this, this._triggerFileDialog)
}]
};
}
return null;
}),
willDestroyElement() {
this._super(...arguments);
this._detachHandlers();
},
actions: {
updateSrc(images) {
let [image] = images;
this._updatePayloadAttr('src', image.url);
},
updateCaption(caption) {
this._updatePayloadAttr('caption', caption);
},
onSelect() {
this._attachHandlers();
},
onDeselect() {
this._detachHandlers();
},
/**
* 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);
},
setPreviewSrc(files) {
let file = files[0];
if (file) {
let reader = new FileReader();
reader.onload = (e) => {
this.set('previewSrc', htmlSafe(e.target.result));
};
reader.readAsDataURL(file);
}
},
resetSrcs() {
this.set('previewSrc', null);
this._updatePayloadAttr('src', null);
}
},
_updatePayloadAttr(attr, value) {
let payload = this.get('payload');
let save = this.get('saveCard');
set(payload, attr, value);
// update the mobiledoc and stay in edit mode
save(payload, false);
},
_attachHandlers() {
if (!this._keypressHandler) {
this._keypressHandler = run.bind(this, this._handleKeypress);
window.addEventListener('keypress', this._keypressHandler);
}
if (!this._keydownHandler) {
this._keydownHandler = run.bind(this, this._handleKeydown);
window.addEventListener('keydown', this._keydownHandler);
}
},
_detachHandlers() {
window.removeEventListener('keypress', this._keypressHandler);
window.removeEventListener('keydown', this._keydownHandler);
this._keypressHandler = null;
this._keydownHandler = null;
},
// only fires if the card is selected, moves focus to the caption input so
// that it's possible to start typing without explicitly focusing the input
_handleKeypress(event) {
let captionInput = this.element.querySelector('[name="caption"]');
if (captionInput && captionInput !== document.activeElement) {
captionInput.value = `${captionInput.value}${event.key}`;
captionInput.focus();
}
},
// this will be fired for keydown events when the caption input is focused,
// we look for cursor movements or the enter key to defocus and trigger the
// corresponding editor behaviour
_handleKeydown(event) {
let captionInput = this.element.querySelector('[name="caption"]');
if (event.target === captionInput) {
if (event.key === 'Escape') {
captionInput.blur();
return;
}
if (event.key === 'Enter') {
captionInput.blur();
this.addParagraphAfterCard();
event.preventDefault();
return;
}
let selectionStart = captionInput.selectionStart;
let length = captionInput.value.length;
if ((event.key === 'ArrowUp' || event.key === 'ArrowLeft') && selectionStart === 0) {
captionInput.blur();
this.moveCursorToPrevSection();
event.preventDefault();
return;
}
if ((event.key === 'ArrowDown' || event.key === 'ArrowRight') && selectionStart === length) {
captionInput.blur();
this.moveCursorToNextSection();
event.preventDefault();
return;
}
}
},
_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

@ -391,6 +391,48 @@ export default Component.extend({
deselectCard(card) {
this.deselectCard(card);
},
moveCursorToPrevSection(card) {
let section = this._getSectionFromCard(card);
if (section.prev) {
this.deselectCard(card);
this._moveCaretToTailOfSection(section.prev, false);
}
},
moveCursorToNextSection(card) {
let section = this._getSectionFromCard(card);
if (section.next) {
this.deselectCard(card);
this._moveCaretToHeadOfSection(section.next, false);
} else {
this.send('addParagraphAfterCard', card);
}
},
addParagraphAfterCard(card) {
let editor = this.get('editor');
let section = this._getSectionFromCard(card);
let collection = section.parent.sections;
let nextSection = section.next;
this.deselectCard(card);
editor.run((postEditor) => {
let {builder} = postEditor;
let newPara = builder.createMarkupSection('p');
if (nextSection) {
postEditor.insertSectionBefore(collection, newPara, nextSection);
} else {
postEditor.insertSectionAtEnd(newPara);
}
postEditor.setRange(newPara.tailPosition());
});
}
},
@ -632,9 +674,18 @@ export default Component.extend({
return card.env.postModel;
},
_moveCaretToHeadOfSection(section, skipCursorChange = true) {
this._moveCaretToSection('head', section, skipCursorChange);
},
_moveCaretToTailOfSection(section, skipCursorChange = true) {
this._moveCaretToSection('tail', section, skipCursorChange);
},
_moveCaretToSection(position, section, skipCursorChange = true) {
this.editor.run((postEditor) => {
let range = section.tailPosition().toRange();
let sectionPosition = position === 'head' ? section.headPosition() : section.tailPosition();
let range = sectionPosition.toRange();
// don't trigger another cursor change selection after selecting
if (skipCursorChange && !range.isEqual(this.editor.range)) {

View file

@ -1 +1,61 @@
<img src={{payload.src}} alt={{payload.alt}} />
{{#koenig-card
tagName="figure"
class=(concat (kg-style "media-card-h") " flex flex-column")
isSelected=isSelected
isEditing=isEditing
selectCard=(action selectCard)
onSelect=(action "onSelect")
onDeselect=(action "onDeselect")
editCard=(action editCard)
toolbar=toolbar
hasEditMode=false
}}
{{#gh-uploader
accept=imageMimeTypes
extensions=imageExtensions
onStart=(action "setPreviewSrc")
onComplete=(action "updateSrc")
onFailed=(action "resetSrcs")
as |uploader|
}}
<div class="relative">
{{#if (or previewSrc payload.src)}}
<img src={{or previewSrc payload.src}} class={{kg-style "image-normal"}} alt={{payload.alt}}>
{{/if}}
{{#if (or uploader.errors uploader.isUploading (not payload.src))}}
<div class="relative miw-100 flex items-center {{if (not previewSrc 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 uploader.isUploading}}
{{uploader.progressBar}}
{{else if (not previewSrc payload.src)}}
<button class="db center btn-base ba b--whitegrey br3 sans-serif fw4 f7 middarkgrey" onclick={{action "triggerFileDialog"}}>
<span>Click to select an image</span>
</button>
{{/if}}
</div>
{{/if}}
</div>
<div style="display:none">
{{gh-file-input multiple=false action=uploader.setFiles accept=imageMimeTypes}}
</div>
{{/gh-uploader}}
{{#if (or isSelected payload.caption)}}
<figcaption class="{{kg-style "figcaption"}} w-100">
<input
value={{payload.caption}}
type="text"
class="miw-100 tc bn form-text"
name="caption"
oninput={{action "updateCaption" value="target.value"}}
placeholder="Type caption for image (optional)">
</figcaption>
{{/if}}
{{/koenig-card}}

View file

@ -5,8 +5,18 @@
{{#if toolbar}}
<div class="koenig-card-toolbar koenig-toolbar {{if showToolbar "koenig-toolbar--visible"}}" style={{toolbarStyle}}>
{{#each toolbar.items as |item|}}
<button type="button" title={{item.title}} class="koenig-toolbar-btn" onmousedown={{action item.action}}>
{{inline-svg item.icon}}
<button
type="button"
title={{item.title}}
class="koenig-toolbar-btn sans-serif f7"
style="width: auto !important"
onmousedown={{action item.action}}
>
{{#if item.icon}}
{{inline-svg item.icon}}
{{else}}
{{item.title}}
{{/if}}
</button>
{{/each}}
</div>

View file

@ -48,6 +48,9 @@
selectCard=(action "selectCard" card)
isEditing=card.isEditing
editCard=(action "editCard" card)
moveCursorToPrevSection=(action "moveCursorToPrevSection" card)
moveCursorToNextSection=(action "moveCursorToNextSection" card)
addParagraphAfterCard=(action "addParagraphAfterCard" card)
}}
{{/-in-element}}
{{/each}}