From 5d552202f6ae313e2b7b6857cb9457c403ca8e73 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 22 Feb 2018 20:41:40 +0000 Subject: [PATCH] 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 down arrow or right arrow 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 up arrow or left arrow 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 enter 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 --- ghost/admin/app/components/gh-uploader.js | 2 +- .../styles/spirit-product/_custom-styles.css | 11 ++ .../addon/components/koenig-card-image.js | 181 +++++++++++++++++- .../addon/components/koenig-editor.js | 53 ++++- .../components/koenig-card-image.hbs | 62 +++++- .../templates/components/koenig-card.hbs | 14 +- .../templates/components/koenig-editor.hbs | 3 + 7 files changed, 319 insertions(+), 7 deletions(-) diff --git a/ghost/admin/app/components/gh-uploader.js b/ghost/admin/app/components/gh-uploader.js index f556edce58..cbf561f03a 100644 --- a/ghost/admin/app/components/gh-uploader.js +++ b/ghost/admin/app/components/gh-uploader.js @@ -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 diff --git a/ghost/admin/app/styles/spirit-product/_custom-styles.css b/ghost/admin/app/styles/spirit-product/_custom-styles.css index 6704c485f3..09cae0babe 100644 --- a/ghost/admin/app/styles/spirit-product/_custom-styles.css +++ b/ghost/admin/app/styles/spirit-product/_custom-styles.css @@ -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 */ diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js index ec7e85739c..4ac76b1db0 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image.js @@ -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(); + } }); diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js index 07f8655a3a..d1e66d96eb 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js @@ -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)) { diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card-image.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card-image.hbs index 3ae0f1f6ca..74e0ddcb85 100644 --- a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card-image.hbs +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card-image.hbs @@ -1 +1,61 @@ -{{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| + }} +
+ {{#if (or previewSrc payload.src)}} + {{payload.alt}} + {{/if}} + + {{#if (or uploader.errors uploader.isUploading (not payload.src))}} +
+ {{#if uploader.errors}} + + {{uploader.errors.firstObject.message}} + + {{/if}} + + {{#if uploader.isUploading}} + {{uploader.progressBar}} + {{else if (not previewSrc payload.src)}} + + {{/if}} +
+ {{/if}} +
+ +
+ {{gh-file-input multiple=false action=uploader.setFiles accept=imageMimeTypes}} +
+ {{/gh-uploader}} + + {{#if (or isSelected payload.caption)}} +
+ +
+ {{/if}} +{{/koenig-card}} diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs index 8784b8915d..f0f91b5091 100644 --- a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-card.hbs @@ -5,8 +5,18 @@ {{#if toolbar}}
{{#each toolbar.items as |item|}} - {{/each}}
diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs index 6ddf34b6fa..f919d3be32 100644 --- a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs @@ -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}}