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:
parent
4f1238d002
commit
5d552202f6
7 changed files with 319 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Add table
Reference in a new issue