This repository has been archived by the owner on Nov 28, 2022. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refs TryGhost/Ghost#9623 requires TryGhost/Ghost#9666 - `{{koenig-card-embed}}` - URL input - perform oembed lookup & fetch on <kbd>Enter</kbd> - remove card if enter pressed with empty URL - show error message on server error - "retry" returns to input bar - "paste as link" removes card and outputs link - "X" removes card - force embedded <script> tags to run - wrap embed html with `.koenig-embed-{video,photo,rich}` class - add embed cards to the (+) and /-menus - "section" support in the card menus - refactor to use single card menu map and content component for both menus - update /-menu keyboard movement to handle sections - add parameter support to /-menu commands - `/embed {url}` will insert embed card and automatically fetch oembed for supplied url
- Loading branch information
1 parent
f58d462
commit 7967ee3
Showing
15 changed files
with
647 additions
and
115 deletions.
There are no files selected for viewing
243 changes: 243 additions & 0 deletions
243
lib/koenig-editor/addon/components/koenig-card-embed.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
import Component from '@ember/component'; | ||
import layout from '../templates/components/koenig-card-embed'; | ||
import {NO_CURSOR_MOVEMENT} from './koenig-editor'; | ||
import {isBlank} from '@ember/utils'; | ||
import {run} from '@ember/runloop'; | ||
import {inject as service} from '@ember/service'; | ||
import {set} from '@ember/object'; | ||
import {task} from 'ember-concurrency'; | ||
|
||
export default Component.extend({ | ||
ajax: service(), | ||
ghostPaths: service(), | ||
|
||
layout, | ||
|
||
// attrs | ||
payload: null, | ||
isSelected: false, | ||
isEditing: false, | ||
|
||
// internal properties | ||
hasError: false, | ||
|
||
// closure actions | ||
selectCard() {}, | ||
deselectCard() {}, | ||
editCard() {}, | ||
saveCard() {}, | ||
deleteCard() { }, | ||
moveCursorToNextSection() { }, | ||
moveCursorToPrevSection() { }, | ||
addParagraphAfterCard() { }, | ||
|
||
init() { | ||
this._super(...arguments); | ||
if (this.payload.url && !this.payload.html) { | ||
this.convertUrl.perform(this.payload.url); | ||
} | ||
}, | ||
|
||
didInsertElement() { | ||
this._super(...arguments); | ||
this._loadPayloadScript(); | ||
}, | ||
|
||
willDestroyElement() { | ||
this._super(...arguments); | ||
this._detachHandlers(); | ||
}, | ||
|
||
actions: { | ||
onSelect() { | ||
this._attachHandlers(); | ||
}, | ||
|
||
onDeselect() { | ||
this._detachHandlers(); | ||
|
||
if (this.payload.url && !this.payload.html && !this.hasError) { | ||
this.convertUrl.perform(this.payload.url); | ||
} else { | ||
this._deleteIfEmpty(); | ||
} | ||
}, | ||
|
||
updateUrl(event) { | ||
let url = event.target.value; | ||
set(this.payload, 'url', url); | ||
}, | ||
|
||
urlKeydown(event) { | ||
if (event.key === 'Enter') { | ||
event.preventDefault(); | ||
this.convertUrl.perform(this.payload.url); | ||
} | ||
|
||
if (event.key === 'Escape') { | ||
this.deleteCard(); | ||
} | ||
}, | ||
|
||
updateCaption(caption) { | ||
set(this.payload, 'caption', caption); | ||
this.saveCard(this.payload, false); | ||
}, | ||
|
||
retry() { | ||
this.set('hasError', false); | ||
}, | ||
|
||
insertAsLink() { | ||
this.editor.run((postEditor) => { | ||
let {builder} = postEditor; | ||
let cardSection = this.env.postModel; | ||
let p = builder.createMarkupSection('p'); | ||
let link = builder.createMarkup('a', {href: this.payload.url}); | ||
|
||
postEditor.replaceSection(cardSection, p); | ||
postEditor.insertTextWithMarkup(p.toRange().head, this.payload.url, [link]); | ||
}); | ||
} | ||
}, | ||
|
||
_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; | ||
} | ||
} | ||
}, | ||
|
||
convertUrl: task(function* (url) { | ||
if (isBlank(url)) { | ||
this.deleteCard(); | ||
return; | ||
} | ||
|
||
try { | ||
let oembedEndpoint = this.ghostPaths.url.api('oembed'); | ||
let response = yield this.ajax.request(oembedEndpoint, { | ||
data: { | ||
url | ||
} | ||
}); | ||
|
||
if (!response.html) { | ||
throw 'No HTML returned'; | ||
} | ||
|
||
set(this.payload, 'html', response.html); | ||
set(this.payload, 'type', response.type); | ||
this.saveCard(this.payload, false); | ||
|
||
run.schedule('afterRender', this, this._loadPayloadScript); | ||
} catch (err) { | ||
this.set('hasError', true); | ||
} | ||
}), | ||
|
||
// some oembeds will have a script tag but it won't automatically run | ||
// due to the way Ember renders the card components. Grab the script | ||
// element and push a new one to force the browser to download+run it | ||
_loadPayloadScript() { | ||
let oldScript = this.element.querySelector('script'); | ||
if (oldScript) { | ||
let parent = oldScript.parentElement; | ||
let newScript = document.createElement('script'); | ||
newScript.type = 'text/javascript'; | ||
|
||
if (oldScript.src) { | ||
// hide the original embed html to avoid ugly transitions as the | ||
// script runs (at least on reasonably good network and cpu) | ||
let embedElement = this.element.querySelector('[data-kg-embed]'); | ||
embedElement.style.display = 'none'; | ||
|
||
newScript.src = oldScript.src; | ||
|
||
// once the script has loaded, wait a little while for it to do it's | ||
// thing before making everything visible again | ||
newScript.onload = run.bind(this, function () { | ||
run.later(this, function () { | ||
embedElement.style.display = null; | ||
}, 500); | ||
}); | ||
|
||
newScript.onerror = run.bind(this, function () { | ||
embedElement.style.display = null; | ||
}); | ||
} else { | ||
newScript.innerHTML = oldScript.innerHTML; | ||
} | ||
|
||
oldScript.remove(); | ||
parent.appendChild(newScript); | ||
} | ||
}, | ||
|
||
_deleteIfEmpty() { | ||
if (isBlank(this.payload.html) && !this.convertUrl.isRunning && !this.hasError) { | ||
this.deleteCard(NO_CURSOR_MOVEMENT); | ||
} | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import Component from '@ember/component'; | ||
import layout from '../templates/components/koenig-menu-content'; | ||
|
||
export default Component.extend({ | ||
layout, | ||
|
||
tagName: '', | ||
|
||
itemSections: null, | ||
|
||
itemClicked() {} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.