Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
✨ Added bookmark card and integrated it as fallback for unknown embeds (
Browse files Browse the repository at this point in the history
#1293)

requires TryGhost/Ghost#11024

With the bookmark card you can present links in a much richer format, similar to Twitter cards. If the URL points to a page with right meta information it can show the page title, excerpt, author, publisher and even a preview image.

Bookmark cards can be created in two ways:

1. pasting a link as the first thing in blank paragraph - we'll check to see if we can create an embed, if we can't then we'll create a bookmark card instead
2. manually selecting the bookmark card from the (+) menu or by typing "/bookmark<kbd>Enter</kbd>" or "/bookmark {url}<kbd>Enter</kbd>" for short (you might want to do this if you want the bookmark version instead of a full embed)

Pressing <kbd>Ctrl/Cmd+Z</kbd> after pasting will convert the bookmark card back to a link if that's preferred, alternatively a URL can be pasted with <kbd>Ctrl/Cmd+Shift+V</kbd> to avoid any automatic transformation to an embed/bookmark.

---

- adds "bookmark" card that functions similarly to the embed card
- if the oembed API request returns `type: "bookmark"` then the metadata is used to create a bookmark card
  • Loading branch information
rishabhgrg authored and kevinansfield committed Aug 27, 2019
1 parent 166c8ff commit 9bfd340
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 10 deletions.
14 changes: 14 additions & 0 deletions app/styles/layouts/editor.css
Expand Up @@ -697,3 +697,17 @@
.CodeMirror .CodeMirror-selected {
background: color-mod(var(--blue) lightness(+30%));
}

figure {
margin: 0;
padding: 0;
}

.koenig-card-click-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 400;
}
88 changes: 83 additions & 5 deletions app/styles/spirit/_koenig.css
Expand Up @@ -806,13 +806,91 @@

/* Cards
/* --------------------------------------------------------------- */
.koenig-card-click-overlay {
.kg-bookmark-card {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--whitegrey);
background: var(--white);
}

.koenig-editor__editor .kg-bookmark-container {
display: flex;
color: var(--darkgrey);
text-decoration: none;
box-shadow: none;
min-height: 120px; /* Just to make sure it's not a super-tiny box */
}

.kg-bookmark-content {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-basis: 100%;
align-items: flex-start;
justify-content: flex-start;
padding: 20px;
}

.kg-bookmark-title {
font-size: 1.5rem;
line-height: 1.5em;
font-weight: 600;
}

.kg-bookmark-container:hover .kg-bookmark-title {
color: var(--blue);
}

.kg-bookmark-description {
display: -webkit-box;
font-size: 1.4rem;
line-height: 1.5em;
margin-top: 10px;
color: var(--middarkgrey);
font-weight: 400;
max-height: 44px;
overflow-y: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}

.kg-bookmark-thumbnail {
position: relative;
flex-grow: 1;
min-width: 33%;
}

.kg-bookmark-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 400;
left: 0;
}

.kg-bookmark-metadata {
color: var(--darkgrey);
font-size: 1.4rem;
font-weight: 500;
display: flex;
align-items: center;
margin-top: 14px;
}

.kg-bookmark-icon {
width: 20px;
height: 20px;
margin-right: 6px;
}

.kg-bookmark-author:after {
content: "•";
margin: 0 6px;
}

.kg-bookmark-publisher {
color: var(--blue);
}


Expand Down
189 changes: 189 additions & 0 deletions lib/koenig-editor/addon/components/koenig-card-bookmark.js
@@ -0,0 +1,189 @@
import Component from '@ember/component';
import layout from '../templates/components/koenig-card-bookmark';
import {NO_CURSOR_MOVEMENT} from './koenig-editor';
import {computed} from '@ember/object';
import {utils as ghostHelperUtils} from '@tryghost/helpers';
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';

const {countWords} = ghostHelperUtils;

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() {},
registerComponent() {},

counts: computed('payload.{metadata,caption}', function () {
let imgCount = 0;
let wordCount = 0;
let metadata = this.payload.metadata;
let caption = this.payload.caption;
imgCount = (metadata && metadata.icon) ? (imgCount + 1) : imgCount;
imgCount = (metadata && metadata.thumbnail) ? (imgCount + 1) : imgCount;
let metadataWordCount = metadata ? (countWords(this.payload.metadata.title) + countWords(this.payload.metadata.description)) : 0;
wordCount = countWords(caption) + metadataWordCount;
return {
imageCount: imgCount,
wordCount: wordCount
};
}),

init() {
this._super(...arguments);
if (this.payload.url && !this.payload.metadata) {
this.convertUrl.perform(this.payload.url);
}

this.registerComponent(this);
},

didInsertElement() {
this._super(...arguments);
this._focusInput();
},

willDestroyElement() {
this._super(...arguments);

run.cancel(this._resizeDebounce);

if (this._iframeMutationObserver) {
this._iframeMutationObserver.disconnect();
}

window.removeEventListener('resize', this._windowResizeHandler);
},

actions: {
onDeselect() {
if (this.payload.url && !this.payload.metadata && !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') {
event.target.blur();
this.deleteCard();
}
},

updateCaption(caption) {
set(this.payload, 'caption', caption);
this.saveCard(this.payload, false);
},

retry() {
this.set('hasError', false);
},

insertAsLink(options = {linkOnError: false}) {
let {range} = this.editor;

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]);

// if a user is typing further on in the doc (possible if embed
// was created automatically via paste of URL) then return the
// cursor so the card->link change doesn't cause a cursor jump
if (range.headSection !== cardSection) {
postEditor.setRange(range);
}

// avoid adding an extra undo step when automatically creating
// link after an error so that an Undo after pasting a URL
// doesn't get stuck in a loop going through link->embed->link
if (options.linkOnError) {
postEditor.cancelSnapshot();
}
});
}
},

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,
type: 'bookmark'
}
});

if (!response.metadata) {
throw 'No metadata returned';
}

set(this.payload, 'linkOnError', undefined);
set(this.payload, 'metadata', response.metadata);
set(this.payload, 'type', response.type);
this.saveCard(this.payload, false);
} catch (err) {
if (this.payload.linkOnError) {
this.send('insertAsLink', {linkOnError: true});
return;
}
this.set('hasError', true);
}
}),

_focusInput() {
let urlInput = this.element.querySelector('[name="url"]');

if (urlInput) {
urlInput.focus();
}
},

_deleteIfEmpty() {
if (isBlank(this.payload.metadata) && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);
}
}
});
34 changes: 30 additions & 4 deletions lib/koenig-editor/addon/components/koenig-card-embed.js
Expand Up @@ -132,6 +132,24 @@ export default Component.extend({
postEditor.cancelSnapshot();
}
});
},

insertAsBookmark(payload) {
let {range} = this.editor;

this.editor.run((postEditor) => {
let cardSection = this.env.postModel;
let bookmarkCard = postEditor.builder.createCardSection('bookmark', payload);

postEditor.replaceSection(cardSection, bookmarkCard);

// if a user is typing further on in the doc (possible if embed
// was created automatically via paste of URL) then return the
// cursor so the card->link change doesn't cause a cursor jump
if (range.headSection !== cardSection) {
postEditor.setRange(range);
}
});
}
},

Expand All @@ -143,17 +161,25 @@ export default Component.extend({

try {
let oembedEndpoint = this.ghostPaths.url.api('oembed');
let requestData = {
url
};
if (!this.payload.isDirectUrl) {
requestData.type = 'embed';
}
let response = yield this.ajax.request(oembedEndpoint, {
data: {
url
}
data: requestData
});

if (response.type === 'bookmark') {
this.send('insertAsBookmark', response);
return;
}
if (!response.html) {
throw 'No HTML returned';
}

set(this.payload, 'linkOnError', undefined);
set(this.payload, 'isDirectUrl', undefined);
set(this.payload, 'html', response.html);
set(this.payload, 'type', response.type);
this.saveCard(this.payload, false);
Expand Down
2 changes: 1 addition & 1 deletion lib/koenig-editor/addon/components/koenig-editor.js
Expand Up @@ -886,7 +886,7 @@ export default Component.extend({
if (range && range.isCollapsed && range.headSection.isBlank && !range.headSection.isListItem) {
if (!this._modifierKeys.shift) {
editor.run((postEditor) => {
let payload = {url: text, linkOnError: true};
let payload = {url: text, linkOnError: true, isDirectUrl: true};
let card = postEditor.builder.createCardSection('embed', payload);
let nextSection = range.headSection.next;

Expand Down

0 comments on commit 9bfd340

Please sign in to comment.