✨ Unsplash integration
closes https://github.com/TryGhost/Ghost/issues/8859, requires https://github.com/TryGhost/Ghost/pull/8895 - adds Unsplash app to app settings - enable/disable toggle - validation and testing of Unsplash App ID - Unsplash App ID field hidden if provided via Ghost config - adds `fetchPrivate` method to `config` service to pull config that requires authentication and updates authentication routines to fetch private config - adds Unsplash buttons to editor toolbar and `{{gh-image-uploader}}` - only present when Unsplash app is enabled - opens Unsplash image selector when clicked - `{{gh-image-uploader}}` has a new `allowUnsplash` attribute to control display of the unsplash button on a per-uploader basis - adds Unsplash image selector (`{{gh-unsplash}}`) - uses new `unsplash` service to handle API requests and maintain state - search - infinite scroll - zoom image - insert image - download image - adds `{{gh-scroll-trigger}}` that will fire an event when the component is rendered into or enters the visible screen area via scrolling - updates `ui` service - adds `isFullscreen` property and updates `gh-editor` so that it gets set/unset when toggling editor fullscreen mode - adds `hasSideNav` and `isSideNavHidden` properties - updates `media-queries` service so that it fires an event each time a breakpoint is entered/exited - removes the need for observers in certain circumstances
This commit is contained in:
parent
55b9054448
commit
1e3191b811
|
@ -39,21 +39,26 @@
|
||||||
"display",
|
"display",
|
||||||
"flex-flow",
|
"flex-flow",
|
||||||
"flex-direction",
|
"flex-direction",
|
||||||
"flex-wrap",
|
|
||||||
"justify-content",
|
"justify-content",
|
||||||
"align-items",
|
"align-items",
|
||||||
"align-content",
|
"align-content",
|
||||||
|
"flex-wrap",
|
||||||
"flex-order",
|
"flex-order",
|
||||||
"flex-pack",
|
"flex-pack",
|
||||||
"flex-align",
|
"flex-align",
|
||||||
"float",
|
"float",
|
||||||
"clear",
|
"clear",
|
||||||
|
"box-sizing",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"min-width",
|
||||||
|
"min-height",
|
||||||
|
"max-width",
|
||||||
|
"max-height",
|
||||||
"overflow",
|
"overflow",
|
||||||
"overflow-x",
|
"overflow-x",
|
||||||
"overflow-y",
|
"overflow-y",
|
||||||
"-webkit-overflow-scrolling",
|
|
||||||
"clip",
|
"clip",
|
||||||
"box-sizing",
|
|
||||||
"margin",
|
"margin",
|
||||||
"margin-top",
|
"margin-top",
|
||||||
"margin-right",
|
"margin-right",
|
||||||
|
@ -64,12 +69,6 @@
|
||||||
"padding-right",
|
"padding-right",
|
||||||
"padding-bottom",
|
"padding-bottom",
|
||||||
"padding-left",
|
"padding-left",
|
||||||
"min-width",
|
|
||||||
"min-height",
|
|
||||||
"max-width",
|
|
||||||
"max-height",
|
|
||||||
"width",
|
|
||||||
"height",
|
|
||||||
"outline",
|
"outline",
|
||||||
"outline-width",
|
"outline-width",
|
||||||
"outline-style",
|
"outline-style",
|
||||||
|
@ -112,26 +111,6 @@
|
||||||
"border-top-right-image",
|
"border-top-right-image",
|
||||||
"border-bottom-right-image",
|
"border-bottom-right-image",
|
||||||
"border-bottom-left-image",
|
"border-bottom-left-image",
|
||||||
"background",
|
|
||||||
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
|
|
||||||
"background-color",
|
|
||||||
"background-image",
|
|
||||||
"background-attachment",
|
|
||||||
"background-position",
|
|
||||||
"background-position-x",
|
|
||||||
"background-position-y",
|
|
||||||
"background-clip",
|
|
||||||
"background-origin",
|
|
||||||
"background-size",
|
|
||||||
"background-repeat",
|
|
||||||
"border-radius",
|
|
||||||
"border-top-left-radius",
|
|
||||||
"border-top-right-radius",
|
|
||||||
"border-bottom-right-radius",
|
|
||||||
"border-bottom-left-radius",
|
|
||||||
"box-decoration-break",
|
|
||||||
"box-shadow",
|
|
||||||
"color",
|
|
||||||
"table-layout",
|
"table-layout",
|
||||||
"caption-side",
|
"caption-side",
|
||||||
"empty-cells",
|
"empty-cells",
|
||||||
|
@ -143,6 +122,24 @@
|
||||||
"counter-increment",
|
"counter-increment",
|
||||||
"counter-reset",
|
"counter-reset",
|
||||||
"vertical-align",
|
"vertical-align",
|
||||||
|
"stroke",
|
||||||
|
"fill",
|
||||||
|
"stroke-width",
|
||||||
|
"stroke-opacity",
|
||||||
|
"color",
|
||||||
|
"font",
|
||||||
|
"font-family",
|
||||||
|
"font-size",
|
||||||
|
"line-height",
|
||||||
|
"font-weight",
|
||||||
|
"font-style",
|
||||||
|
"font-variant",
|
||||||
|
"font-size-adjust",
|
||||||
|
"font-stretch",
|
||||||
|
"text-rendering",
|
||||||
|
"font-feature-settings",
|
||||||
|
"letter-spacing",
|
||||||
|
"hyphens",
|
||||||
"text-align",
|
"text-align",
|
||||||
"text-align-last",
|
"text-align-last",
|
||||||
"text-decoration",
|
"text-decoration",
|
||||||
|
@ -164,24 +161,8 @@
|
||||||
"word-wrap",
|
"word-wrap",
|
||||||
"word-break",
|
"word-break",
|
||||||
"tab-size",
|
"tab-size",
|
||||||
"hyphens",
|
|
||||||
"letter-spacing",
|
|
||||||
"font",
|
|
||||||
"font-family",
|
|
||||||
"font-size",
|
|
||||||
"line-height",
|
|
||||||
"font-weight",
|
|
||||||
"font-style",
|
|
||||||
"font-variant",
|
|
||||||
"font-size-adjust",
|
|
||||||
"font-stretch",
|
|
||||||
"text-rendering",
|
|
||||||
"font-feature-settings",
|
|
||||||
"user-select",
|
"user-select",
|
||||||
"src",
|
"src",
|
||||||
"opacity",
|
|
||||||
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
|
|
||||||
"filter",
|
|
||||||
"resize",
|
"resize",
|
||||||
"cursor",
|
"cursor",
|
||||||
"nav-index",
|
"nav-index",
|
||||||
|
@ -189,6 +170,28 @@
|
||||||
"nav-right",
|
"nav-right",
|
||||||
"nav-down",
|
"nav-down",
|
||||||
"nav-left",
|
"nav-left",
|
||||||
|
"background",
|
||||||
|
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
|
||||||
|
"background-color",
|
||||||
|
"background-image",
|
||||||
|
"background-size",
|
||||||
|
"background-attachment",
|
||||||
|
"background-position",
|
||||||
|
"background-position-x",
|
||||||
|
"background-position-y",
|
||||||
|
"background-clip",
|
||||||
|
"background-origin",
|
||||||
|
"background-repeat",
|
||||||
|
"border-radius",
|
||||||
|
"border-top-left-radius",
|
||||||
|
"border-top-right-radius",
|
||||||
|
"border-bottom-right-radius",
|
||||||
|
"border-bottom-left-radius",
|
||||||
|
"box-decoration-break",
|
||||||
|
"box-shadow",
|
||||||
|
"opacity",
|
||||||
|
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
|
||||||
|
"filter",
|
||||||
"transition",
|
"transition",
|
||||||
"transition-delay",
|
"transition-delay",
|
||||||
"transition-timing-function",
|
"transition-timing-function",
|
||||||
|
@ -230,6 +233,8 @@
|
||||||
"max-zoom",
|
"max-zoom",
|
||||||
"min-zoom",
|
"min-zoom",
|
||||||
"user-zoom",
|
"user-zoom",
|
||||||
"orientation"
|
"orientation",
|
||||||
|
"-webkit-overflow-scrolling",
|
||||||
|
"-ms-overflow-scrolling"
|
||||||
] ]
|
] ]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import {
|
||||||
IMAGE_EXTENSIONS,
|
IMAGE_EXTENSIONS,
|
||||||
IMAGE_MIME_TYPES
|
IMAGE_MIME_TYPES
|
||||||
} from 'ghost-admin/components/gh-image-uploader';
|
} from 'ghost-admin/components/gh-image-uploader';
|
||||||
|
import {inject as injectService} from '@ember/service';
|
||||||
|
|
||||||
const {debounce} = run;
|
const {debounce} = run;
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
ui: injectService(),
|
||||||
|
|
||||||
classNameBindings: [
|
classNameBindings: [
|
||||||
'isDraggedOver:-drag-over',
|
'isDraggedOver:-drag-over',
|
||||||
|
@ -140,6 +142,7 @@ export default Component.extend({
|
||||||
actions: {
|
actions: {
|
||||||
toggleFullScreen(isFullScreen) {
|
toggleFullScreen(isFullScreen) {
|
||||||
this.set('isFullScreen', isFullScreen);
|
this.set('isFullScreen', isFullScreen);
|
||||||
|
this.get('ui').set('isFullScreen', isFullScreen);
|
||||||
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
run.scheduleOnce('afterRender', this, this._setHeaderClass);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,9 @@ import Component from 'ember-component';
|
||||||
import {invokeAction} from 'ember-invoke-action';
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
|
allowUnsplash: false,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
update() {
|
update() {
|
||||||
if (typeof this.attrs.update === 'function') {
|
if (typeof this.attrs.update === 'function') {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
isUnsupportedMediaTypeError,
|
isUnsupportedMediaTypeError,
|
||||||
isVersionMismatchError
|
isVersionMismatchError
|
||||||
} from 'ghost-admin/services/ajax';
|
} from 'ghost-admin/services/ajax';
|
||||||
|
import {assign} from '@ember/polyfills';
|
||||||
import {htmlSafe} from 'ember-string';
|
import {htmlSafe} from 'ember-string';
|
||||||
import {invokeAction} from 'ember-invoke-action';
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
import {isBlank} from 'ember-utils';
|
import {isBlank} from 'ember-utils';
|
||||||
|
@ -18,6 +19,10 @@ export const IMAGE_MIME_TYPES = 'image/gif,image/jpg,image/jpeg,image/png,image/
|
||||||
export const IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
export const IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg'];
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
ajax: injectService(),
|
||||||
|
notifications: injectService(),
|
||||||
|
settings: injectService(),
|
||||||
|
|
||||||
tagName: 'section',
|
tagName: 'section',
|
||||||
classNames: ['gh-image-uploader'],
|
classNames: ['gh-image-uploader'],
|
||||||
classNameBindings: ['dragClass'],
|
classNameBindings: ['dragClass'],
|
||||||
|
@ -30,6 +35,7 @@ export default Component.extend({
|
||||||
extensions: null,
|
extensions: null,
|
||||||
uploadUrl: null,
|
uploadUrl: null,
|
||||||
validate: null,
|
validate: null,
|
||||||
|
allowUnsplash: false,
|
||||||
|
|
||||||
dragClass: null,
|
dragClass: null,
|
||||||
failureMessage: null,
|
failureMessage: null,
|
||||||
|
@ -37,12 +43,10 @@ export default Component.extend({
|
||||||
url: null,
|
url: null,
|
||||||
uploadPercentage: 0,
|
uploadPercentage: 0,
|
||||||
|
|
||||||
ajax: injectService(),
|
|
||||||
notifications: injectService(),
|
|
||||||
|
|
||||||
_defaultAccept: IMAGE_MIME_TYPES,
|
_defaultAccept: IMAGE_MIME_TYPES,
|
||||||
_defaultExtensions: IMAGE_EXTENSIONS,
|
_defaultExtensions: IMAGE_EXTENSIONS,
|
||||||
_defaultUploadUrl: '/uploads/',
|
_defaultUploadUrl: '/uploads/',
|
||||||
|
_showUnsplash: false,
|
||||||
|
|
||||||
// TODO: this wouldn't be necessary if the server could accept direct
|
// TODO: this wouldn't be necessary if the server could accept direct
|
||||||
// file uploads
|
// file uploads
|
||||||
|
@ -74,6 +78,18 @@ export default Component.extend({
|
||||||
return htmlSafe(`width: ${width}`);
|
return htmlSafe(`width: ${width}`);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// HACK: this settings/config dance is needed because the "override" only
|
||||||
|
// happens when visiting the unsplash app settings route
|
||||||
|
// TODO: move the override logic to the server, client knows too much
|
||||||
|
// about which values should override others
|
||||||
|
unsplash: computed('config.unsplashAPI', 'settings.unsplash', function () {
|
||||||
|
let unsplashConfig = this.get('config.unsplashAPI');
|
||||||
|
let unsplashSettings = this.get('settings.unsplash');
|
||||||
|
let unsplash = assign({}, unsplashConfig, unsplashSettings);
|
||||||
|
|
||||||
|
return unsplash;
|
||||||
|
}),
|
||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
let image = this.get('image');
|
let image = this.get('image');
|
||||||
this.set('url', image);
|
this.set('url', image);
|
||||||
|
@ -244,6 +260,11 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addUnsplashPhoto(photo) {
|
||||||
|
this.set('url', photo.urls.regular);
|
||||||
|
this.send('saveUrl');
|
||||||
|
},
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.set('file', null);
|
this.set('file', null);
|
||||||
this.set('uploadPercentage', 0);
|
this.set('uploadPercentage', 0);
|
||||||
|
|
|
@ -8,7 +8,7 @@ import run from 'ember-runloop';
|
||||||
import {assign} from 'ember-platform';
|
import {assign} from 'ember-platform';
|
||||||
import {copy} from 'ember-metal/utils';
|
import {copy} from 'ember-metal/utils';
|
||||||
import {htmlSafe} from 'ember-string';
|
import {htmlSafe} from 'ember-string';
|
||||||
import {isEmpty} from 'ember-utils';
|
import {isEmpty, typeOf} from 'ember-utils';
|
||||||
|
|
||||||
const MOBILEDOC_VERSION = '0.3.1';
|
const MOBILEDOC_VERSION = '0.3.1';
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ export const BLANK_DOC = {
|
||||||
|
|
||||||
export default Component.extend(ShortcutsMixin, {
|
export default Component.extend(ShortcutsMixin, {
|
||||||
|
|
||||||
|
config: injectService(),
|
||||||
notifications: injectService(),
|
notifications: injectService(),
|
||||||
|
settings: injectService(),
|
||||||
|
|
||||||
classNames: ['gh-markdown-editor'],
|
classNames: ['gh-markdown-editor'],
|
||||||
classNameBindings: [
|
classNameBindings: [
|
||||||
|
@ -57,10 +59,12 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
|
|
||||||
// Private
|
// Private
|
||||||
_editor: null,
|
_editor: null,
|
||||||
|
_editorFocused: false,
|
||||||
_isFullScreen: false,
|
_isFullScreen: false,
|
||||||
_isSplitScreen: false,
|
_isSplitScreen: false,
|
||||||
_isHemmingwayMode: false,
|
_isHemmingwayMode: false,
|
||||||
_isUploading: false,
|
_isUploading: false,
|
||||||
|
_showUnsplash: false,
|
||||||
_statusbar: null,
|
_statusbar: null,
|
||||||
_toolbar: null,
|
_toolbar: null,
|
||||||
_uploadedImageUrls: null,
|
_uploadedImageUrls: null,
|
||||||
|
@ -145,6 +149,28 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
status: ['words']
|
status: ['words']
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if unsplash is active insert the toolbar button after the image button
|
||||||
|
// HACK: this settings/config dance is needed because the "override" only
|
||||||
|
// happens when visiting the unsplash app settings route
|
||||||
|
// TODO: move the override logic to the server, client knows too much
|
||||||
|
// about which values should override others
|
||||||
|
let unsplashConfig = this.get('config.unsplashAPI');
|
||||||
|
let unsplashSettings = this.get('settings.unsplash');
|
||||||
|
let unsplash = assign({}, unsplashConfig, unsplashSettings);
|
||||||
|
if (unsplash.isActive) {
|
||||||
|
let image = defaultOptions.toolbar.findBy('name', 'image');
|
||||||
|
let index = defaultOptions.toolbar.indexOf(image) + 1;
|
||||||
|
|
||||||
|
defaultOptions.toolbar.splice(index, 0, {
|
||||||
|
name: 'unsplash',
|
||||||
|
action: () => {
|
||||||
|
this.send('toggleUnsplash');
|
||||||
|
},
|
||||||
|
className: 'fa fa-camera',
|
||||||
|
title: 'Add Image from Unsplash'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return assign(defaultOptions, options);
|
return assign(defaultOptions, options);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -154,7 +180,7 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
let shortcuts = this.get('shortcuts');
|
let shortcuts = this.get('shortcuts');
|
||||||
|
|
||||||
shortcuts[`${ctrlOrCmd}+shift+i`] = {action: 'insertImage'};
|
shortcuts[`${ctrlOrCmd}+shift+i`] = {action: 'openImageFileDialog'};
|
||||||
shortcuts['ctrl+alt+h'] = {action: 'toggleHemmingway'};
|
shortcuts['ctrl+alt+h'] = {action: 'toggleHemmingway'};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -201,6 +227,8 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
|
|
||||||
// loop through urls and generate image markdown
|
// loop through urls and generate image markdown
|
||||||
let images = urls.map((url) => {
|
let images = urls.map((url) => {
|
||||||
|
// plain url string, so extract filename from path
|
||||||
|
if (typeOf(url) === 'string') {
|
||||||
let filename = url.split('/').pop();
|
let filename = url.split('/').pop();
|
||||||
let alt = filename;
|
let alt = filename;
|
||||||
|
|
||||||
|
@ -210,8 +238,19 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
}
|
}
|
||||||
|
|
||||||
return `![${alt}](${url})`;
|
return `![${alt}](${url})`;
|
||||||
|
|
||||||
|
// full url object, use attrs we're given
|
||||||
|
} else {
|
||||||
|
let image = `![${url.alt}](${url.url})`;
|
||||||
|
|
||||||
|
if (url.credit) {
|
||||||
|
image += `\n${url.credit}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
let text = images.join('\n');
|
let text = images.join('\n\n');
|
||||||
|
|
||||||
// clicking the image toolbar button will lose the selection so we use
|
// clicking the image toolbar button will lose the selection so we use
|
||||||
// the captured selection to re-select here
|
// the captured selection to re-select here
|
||||||
|
@ -231,7 +270,7 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
// focus editor and place cursor at end if not already focused
|
// focus editor and place cursor at end if not already focused
|
||||||
if (!cm.hasFocus()) {
|
if (!cm.hasFocus()) {
|
||||||
this.send('focusEditor');
|
this.send('focusEditor');
|
||||||
text = `\n\n${text}`;
|
text = `\n\n${text}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert at cursor or replace selection then position cursor at end
|
// insert at cursor or replace selection then position cursor at end
|
||||||
|
@ -452,11 +491,59 @@ export default Component.extend(ShortcutsMixin, {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
insertImage() {
|
// HACK FIXME (PLEASE):
|
||||||
|
// - clicking toolbar buttons will cause the editor to lose focus
|
||||||
|
// - this is painful because we often want to know if the editor has focus
|
||||||
|
// so that we can insert images and so on in the correct place
|
||||||
|
// - the blur event will always fire before the button action is triggered 😞
|
||||||
|
// - to work around this we track focus state manually and set it to false
|
||||||
|
// after an arbitrary period that's long enough to allow the button action
|
||||||
|
// to trigger first
|
||||||
|
// - this _may_ well have unknown issues due to browser differences,
|
||||||
|
// variations in performance, moon cycles, sun spots, or cosmic rays
|
||||||
|
// - here be 🐲
|
||||||
|
// - (please let it work 🙏)
|
||||||
|
updateFocusState(focused) {
|
||||||
|
if (focused) {
|
||||||
|
this._editorFocused = true;
|
||||||
|
} else {
|
||||||
|
run.later(this, function () {
|
||||||
|
this._editorFocused = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openImageFileDialog() {
|
||||||
let captureSelection = this._editor.codemirror.hasFocus();
|
let captureSelection = this._editor.codemirror.hasFocus();
|
||||||
this._openImageFileDialog({captureSelection});
|
this._openImageFileDialog({captureSelection});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleUnsplash() {
|
||||||
|
if (this.get('_showUnsplash')) {
|
||||||
|
return this.toggleProperty('_showUnsplash');
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture current selection before it's lost by clicking toolbar btn
|
||||||
|
if (this._editorFocused) {
|
||||||
|
this._imageInsertSelection = {
|
||||||
|
anchor: this._editor.codemirror.getCursor('anchor'),
|
||||||
|
head: this._editor.codemirror.getCursor('head')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleProperty('_showUnsplash');
|
||||||
|
},
|
||||||
|
|
||||||
|
insertUnsplashPhoto(photo) {
|
||||||
|
let image = {
|
||||||
|
alt: photo.description || '',
|
||||||
|
url: photo.urls.regular,
|
||||||
|
credit: `<small>Photo by [${photo.user.name}](${photo.user.links.html}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit) / [Unsplash](https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit)</small>`
|
||||||
|
};
|
||||||
|
|
||||||
|
this._insertImages([image]);
|
||||||
|
},
|
||||||
|
|
||||||
toggleFullScreen() {
|
toggleFullScreen() {
|
||||||
let isFullScreen = !this.get('_isFullScreen');
|
let isFullScreen = !this.get('_isFullScreen');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Component from 'ember-component';
|
||||||
|
import InViewportMixin from 'ember-in-viewport';
|
||||||
|
|
||||||
|
export default Component.extend(InViewportMixin, {
|
||||||
|
|
||||||
|
onEnterViewport() {},
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
let offset = this.get('triggerOffset');
|
||||||
|
|
||||||
|
this.set('viewportSpy', true);
|
||||||
|
this.set('viewportTolerance', {
|
||||||
|
top: offset,
|
||||||
|
bottom: offset,
|
||||||
|
left: offset,
|
||||||
|
right: offset
|
||||||
|
});
|
||||||
|
|
||||||
|
this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
didEnterViewport() {
|
||||||
|
return this.onEnterViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -66,6 +66,14 @@ export default TextArea.extend({
|
||||||
this.onChange(this._editor.value());
|
this.onChange(this._editor.value());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._editor.codemirror.on('focus', () => {
|
||||||
|
this.onFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
this._editor.codemirror.on('blur', () => {
|
||||||
|
this.onBlur();
|
||||||
|
});
|
||||||
|
|
||||||
if (this.get('autofocus')) {
|
if (this.get('autofocus')) {
|
||||||
this._editor.codemirror.execCommand('goDocEnd');
|
this._editor.codemirror.execCommand('goDocEnd');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import Component from 'ember-component';
|
||||||
|
import {computed} from '@ember/object';
|
||||||
|
import {htmlSafe} from '@ember/string';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
|
||||||
|
height: 0,
|
||||||
|
photo: null,
|
||||||
|
tagName: '',
|
||||||
|
width: 1200,
|
||||||
|
zoomed: false,
|
||||||
|
|
||||||
|
// closure actions
|
||||||
|
insert() {},
|
||||||
|
zoom() {},
|
||||||
|
|
||||||
|
// avoid "binding style attributes" warnings
|
||||||
|
style: computed('photo.color', 'zoomed', function() {
|
||||||
|
let styles = [];
|
||||||
|
let ratio = this.get('photo.ratio');
|
||||||
|
let zoomed = this.get('zoomed');
|
||||||
|
|
||||||
|
styles.push(`background-color: ${this.get('photo.color')}`);
|
||||||
|
|
||||||
|
if (!zoomed) {
|
||||||
|
styles.push(`padding-bottom: ${ratio * 100}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return htmlSafe(styles.join('; '));
|
||||||
|
}),
|
||||||
|
|
||||||
|
imageUrl: computed('photo.urls.regular', function () {
|
||||||
|
let url = this.get('photo.urls.regular');
|
||||||
|
|
||||||
|
url = url.replace(/&w=1080/, '&w=1200');
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}),
|
||||||
|
|
||||||
|
didReceiveAttrs() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
let height = this.get('width') * this.get('photo.ratio');
|
||||||
|
|
||||||
|
this.set('height', height);
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
insert(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.insert(this.get('photo'));
|
||||||
|
},
|
||||||
|
|
||||||
|
zoom(event) {
|
||||||
|
let $target = $(event.target);
|
||||||
|
|
||||||
|
// only zoom when it wasn't one of the child links clicked
|
||||||
|
if (!$target.is('a') && $target.closest('a').hasClass('gh-unsplash-photo')) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.zoom(this.get('photo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't propagate otherwise we can trigger the closeZoom action on the overlay
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,86 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import ShortcutsMixin from 'ghost-admin/mixins/shortcuts';
|
||||||
|
import {bind} from '@ember/runloop';
|
||||||
|
import {inject as injectService} from '@ember/service';
|
||||||
|
import {or} from '@ember/object/computed';
|
||||||
|
|
||||||
|
const ONE_COLUMN_WIDTH = 540;
|
||||||
|
const TWO_COLUMN_WIDTH = 940;
|
||||||
|
|
||||||
|
export default Component.extend(ShortcutsMixin, {
|
||||||
|
resizeDetector: injectService(),
|
||||||
|
unsplash: injectService(),
|
||||||
|
ui: injectService(),
|
||||||
|
|
||||||
|
tagName: '',
|
||||||
|
zoomedPhoto: null,
|
||||||
|
|
||||||
|
shortcuts: {
|
||||||
|
escape: 'handleEscape'
|
||||||
|
},
|
||||||
|
|
||||||
|
// closure actions
|
||||||
|
close() {},
|
||||||
|
insert() {},
|
||||||
|
|
||||||
|
sideNavHidden: or('ui.{autoNav,isFullScreen,showMobileMenu}'),
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this._resizeCallback = bind(this, this._handleResize);
|
||||||
|
this.get('resizeDetector').setup('.gh-unsplash', this._resizeCallback);
|
||||||
|
this.registerShortcuts();
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.get('resizeDetector').teardown('.gh-unsplash', this._resizeCallback);
|
||||||
|
this.removeShortcuts();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
loadNextPage() {
|
||||||
|
this.get('unsplash').loadNextPage();
|
||||||
|
},
|
||||||
|
|
||||||
|
zoomPhoto(photo) {
|
||||||
|
this.set('zoomedPhoto', photo);
|
||||||
|
},
|
||||||
|
|
||||||
|
closeZoom() {
|
||||||
|
this.set('zoomedPhoto', null);
|
||||||
|
},
|
||||||
|
|
||||||
|
insert(photo) {
|
||||||
|
this.insert(photo);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
retry() {
|
||||||
|
this.get('unsplash').retryLastRequest();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEscape() {
|
||||||
|
if (!this.get('zoomedPhoto')) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleResize(element) {
|
||||||
|
let width = element.clientWidth;
|
||||||
|
let columns = 3;
|
||||||
|
|
||||||
|
if (width <= ONE_COLUMN_WIDTH) {
|
||||||
|
columns = 1;
|
||||||
|
} else if (width <= TWO_COLUMN_WIDTH) {
|
||||||
|
columns = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get('unsplash').changeColumnCount(columns);
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Controller from 'ember-controller';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
import {alias, empty} from 'ember-computed';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
|
||||||
|
export default Controller.extend({
|
||||||
|
notifications: injectService(),
|
||||||
|
settings: injectService(),
|
||||||
|
config: injectService(),
|
||||||
|
unsplash: injectService(),
|
||||||
|
|
||||||
|
model: alias('settings.unsplash'),
|
||||||
|
testRequestDisabled: empty('model.applicationId'),
|
||||||
|
|
||||||
|
_triggerValidations() {
|
||||||
|
let isActive = this.get('model.isActive');
|
||||||
|
let applicationId = this.get('model.applicationId');
|
||||||
|
|
||||||
|
this.get('model.hasValidated').clear();
|
||||||
|
|
||||||
|
// api key field is hidden if set via config so don't validate in that case
|
||||||
|
if (!this.get('config.unsplashAPI.applicationId')) {
|
||||||
|
// CASE: application id is empty but unsplash is enabled
|
||||||
|
if (isActive && !applicationId) {
|
||||||
|
this.get('model.errors').add(
|
||||||
|
'isActive',
|
||||||
|
'You need to enter an Application ID before enabling it'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.get('model.hasValidated').pushObject('isActive');
|
||||||
|
} else {
|
||||||
|
// run the validation for application id
|
||||||
|
this.get('model').validate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get('model.hasValidated').pushObject('isActive');
|
||||||
|
},
|
||||||
|
|
||||||
|
save: task(function* () {
|
||||||
|
let unsplash = this.get('model');
|
||||||
|
let settings = this.get('settings');
|
||||||
|
|
||||||
|
// Don't save when we have errors and properties are not validated
|
||||||
|
if ((this.get('model.errors.isActive') || this.get('model.errors.applicationId'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
settings.set('unsplash', unsplash);
|
||||||
|
return yield settings.save();
|
||||||
|
} catch (error) {
|
||||||
|
if (error) {
|
||||||
|
this.get('notifications').showAPIError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
|
sendTestRequest: task(function* () {
|
||||||
|
let notifications = this.get('notifications');
|
||||||
|
let applicationId = this.get('model.applicationId');
|
||||||
|
|
||||||
|
try {
|
||||||
|
yield this.get('unsplash').sendTestRequest(applicationId);
|
||||||
|
} catch (error) {
|
||||||
|
notifications.showAPIError(error, {key: 'unsplash-test:send'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the application id when it's valid
|
||||||
|
yield this.get('save').perform();
|
||||||
|
return true;
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this.get('save').perform();
|
||||||
|
},
|
||||||
|
|
||||||
|
update(value) {
|
||||||
|
if (this.get('model.errors.isActive')) {
|
||||||
|
this.get('model.errors.isActive').clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('model.isActive', value);
|
||||||
|
this._triggerValidations();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateId(value) {
|
||||||
|
value = value ? value.toString().trim() : '';
|
||||||
|
|
||||||
|
if (this.get('model.errors.applicationId')) {
|
||||||
|
this.get('model.errors.applicationId').clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('model.applicationId', value);
|
||||||
|
this._triggerValidations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -216,21 +216,26 @@ export default Controller.extend(ValidationEngine, {
|
||||||
},
|
},
|
||||||
|
|
||||||
_afterAuthentication(result) {
|
_afterAuthentication(result) {
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
promises.pushObject(this.get('settings').fetch());
|
||||||
|
promises.pushObject(this.get('config').fetchPrivate());
|
||||||
|
|
||||||
if (this.get('profileImage')) {
|
if (this.get('profileImage')) {
|
||||||
return this._sendImage(result.users[0])
|
return this._sendImage(result.users[0])
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// fetch settings and private config for synchronous access before transitioning
|
||||||
// fetch settings for synchronous access before transitioning
|
return RSVP.all(promises)
|
||||||
return this.get('settings').fetch().then(() => {
|
.then(() => {
|
||||||
return this.transitionToRoute('setup.three');
|
return this.transitionToRoute('setup.three');
|
||||||
});
|
});
|
||||||
}).catch((resp) => {
|
}).catch((resp) => {
|
||||||
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// fetch settings and private config for synchronous access before transitioning
|
||||||
// fetch settings for synchronous access before transitioning
|
return RSVP.all(promises)
|
||||||
return this.get('settings').fetch().then(() => {
|
.then(() => {
|
||||||
return this.transitionToRoute('setup.three');
|
return this.transitionToRoute('setup.three');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Controller from 'ember-controller';
|
import Controller from 'ember-controller';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||||
import injectController from 'ember-controller/inject';
|
import injectController from 'ember-controller/inject';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
|
@ -30,9 +31,13 @@ export default Controller.extend(ValidationEngine, {
|
||||||
try {
|
try {
|
||||||
let authResult = yield this.get('session')
|
let authResult = yield this.get('session')
|
||||||
.authenticate(authStrategy, ...authentication);
|
.authenticate(authStrategy, ...authentication);
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
// fetch settings for synchronous access
|
promises.pushObject(this.get('settings').fetch());
|
||||||
yield this.get('settings').fetch();
|
promises.pushObject(this.get('config').fetchPrivate());
|
||||||
|
|
||||||
|
// fetch settings and private config for synchronous access
|
||||||
|
yield RSVP.all(promises);
|
||||||
|
|
||||||
return authResult;
|
return authResult;
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,13 @@ export default Controller.extend(ValidationEngine, {
|
||||||
try {
|
try {
|
||||||
let authResult = yield this.get('session')
|
let authResult = yield this.get('session')
|
||||||
.authenticate(authStrategy, ...authentication);
|
.authenticate(authStrategy, ...authentication);
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
// fetch settings for synchronous access
|
promises.pushObject(this.get('settings').fetch());
|
||||||
yield this.get('settings').fetch();
|
promises.pushObject(this.get('config').fetchPrivate());
|
||||||
|
|
||||||
|
// fetch settings and private config for synchronous access
|
||||||
|
yield RSVP.all(promises);
|
||||||
|
|
||||||
return authResult;
|
return authResult;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import SignupValidator from 'ghost-admin/validators/signup';
|
||||||
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
import SlackIntegrationValidator from 'ghost-admin/validators/slack-integration';
|
||||||
import SubscriberValidator from 'ghost-admin/validators/subscriber';
|
import SubscriberValidator from 'ghost-admin/validators/subscriber';
|
||||||
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
import TagSettingsValidator from 'ghost-admin/validators/tag-settings';
|
||||||
|
import UnsplashIntegrationValidator from 'ghost-admin/validators/unsplash-integration';
|
||||||
import UserValidator from 'ghost-admin/validators/user';
|
import UserValidator from 'ghost-admin/validators/user';
|
||||||
import ValidatorExtensions from 'ghost-admin/utils/validator-extensions';
|
import ValidatorExtensions from 'ghost-admin/utils/validator-extensions';
|
||||||
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
import {A as emberA, isEmberArray} from 'ember-array/utils';
|
||||||
|
@ -46,7 +47,8 @@ export default Mixin.create({
|
||||||
slackIntegration: SlackIntegrationValidator,
|
slackIntegration: SlackIntegrationValidator,
|
||||||
subscriber: SubscriberValidator,
|
subscriber: SubscriberValidator,
|
||||||
tag: TagSettingsValidator,
|
tag: TagSettingsValidator,
|
||||||
user: UserValidator
|
user: UserValidator,
|
||||||
|
unsplashIntegration: UnsplashIntegrationValidator
|
||||||
},
|
},
|
||||||
|
|
||||||
// This adds the Errors object to the validation engine, and shouldn't affect
|
// This adds the Errors object to the validation engine, and shouldn't affect
|
||||||
|
|
|
@ -24,5 +24,6 @@ export default Model.extend(ValidationEngine, {
|
||||||
isPrivate: attr('boolean'),
|
isPrivate: attr('boolean'),
|
||||||
password: attr('string'),
|
password: attr('string'),
|
||||||
slack: attr('slack-settings'),
|
slack: attr('slack-settings'),
|
||||||
amp: attr('boolean')
|
amp: attr('boolean'),
|
||||||
|
unsplash: attr('unsplash-settings')
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import EmberObject from 'ember-object';
|
||||||
|
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||||
|
|
||||||
|
export default EmberObject.extend(ValidationEngine, {
|
||||||
|
// values entered here will act as defaults
|
||||||
|
applicationId: '',
|
||||||
|
|
||||||
|
validationType: 'unsplashIntegration',
|
||||||
|
|
||||||
|
isActive: false
|
||||||
|
});
|
|
@ -55,6 +55,7 @@ GhostRouter.map(function () {
|
||||||
this.route('settings.apps', {path: '/settings/apps'}, function () {
|
this.route('settings.apps', {path: '/settings/apps'}, function () {
|
||||||
this.route('slack', {path: 'slack'});
|
this.route('slack', {path: 'slack'});
|
||||||
this.route('amp', {path: 'amp'});
|
this.route('amp', {path: 'amp'});
|
||||||
|
this.route('unsplash', {path: 'unsplash'});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('subscribers', function () {
|
this.route('subscribers', function () {
|
||||||
|
|
|
@ -73,6 +73,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||||
});
|
});
|
||||||
|
|
||||||
let settingsPromise = this.get('settings').fetch();
|
let settingsPromise = this.get('settings').fetch();
|
||||||
|
let privateConfigPromise = this.get('config').fetchPrivate();
|
||||||
let tourPromise = this.get('tour').fetchViewed();
|
let tourPromise = this.get('tour').fetchViewed();
|
||||||
|
|
||||||
// return the feature/settings load promises so that we block until
|
// return the feature/settings load promises so that we block until
|
||||||
|
@ -80,6 +81,7 @@ export default Route.extend(ApplicationRouteMixin, ShortcutsRoute, {
|
||||||
return RSVP.all([
|
return RSVP.all([
|
||||||
featurePromise,
|
featurePromise,
|
||||||
settingsPromise,
|
settingsPromise,
|
||||||
|
privateConfigPromise,
|
||||||
tourPromise
|
tourPromise
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
|
||||||
|
import UnsplashObject from 'ghost-admin/models/unsplash-integration';
|
||||||
|
import styleBody from 'ghost-admin/mixins/style-body';
|
||||||
|
import {inject as injectService} from '@ember/service';
|
||||||
|
|
||||||
|
export default AuthenticatedRoute.extend(styleBody, {
|
||||||
|
config: injectService(),
|
||||||
|
settings: injectService(),
|
||||||
|
|
||||||
|
titleToken: 'Settings - Apps - Unsplash',
|
||||||
|
classNames: ['settings-view-apps-unsplash'],
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
let settings = this.get('settings');
|
||||||
|
|
||||||
|
if (settings.get('unsplash')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// server doesn't have any unsplash settings by default but it can provide
|
||||||
|
// overrides via config:
|
||||||
|
// - isActive: use as default but allow settings override
|
||||||
|
// - applicationId: total override, no field is shown if present
|
||||||
|
let unsplash = UnsplashObject.create({
|
||||||
|
isActive: this.get('config.unsplashAPI.isActive') || false,
|
||||||
|
applicationId: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.set('unsplash', unsplash);
|
||||||
|
|
||||||
|
return unsplash;
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
save() {
|
||||||
|
this.get('controller').send('save');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import Ember from 'ember';
|
||||||
import Service from 'ember-service';
|
import Service from 'ember-service';
|
||||||
import computed from 'ember-computed';
|
import computed from 'ember-computed';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
|
import {assign} from 'ember-platform';
|
||||||
import {isBlank} from 'ember-utils';
|
import {isBlank} from 'ember-utils';
|
||||||
|
|
||||||
// ember-cli-shims doesn't export _ProxyMixin
|
// ember-cli-shims doesn't export _ProxyMixin
|
||||||
|
@ -16,12 +17,20 @@ export default Service.extend(_ProxyMixin, {
|
||||||
fetch() {
|
fetch() {
|
||||||
let configUrl = this.get('ghostPaths.url').api('configuration');
|
let configUrl = this.get('ghostPaths.url').api('configuration');
|
||||||
|
|
||||||
return this.get('ajax').request(configUrl).then((config) => {
|
return this.get('ajax').request(configUrl).then((publicConfig) => {
|
||||||
// normalize blogUrl to non-trailing-slash
|
// normalize blogUrl to non-trailing-slash
|
||||||
let [{blogUrl}] = config.configuration;
|
let [{blogUrl}] = publicConfig.configuration;
|
||||||
config.configuration[0].blogUrl = blogUrl.replace(/\/$/, '');
|
publicConfig.configuration[0].blogUrl = blogUrl.replace(/\/$/, '');
|
||||||
|
|
||||||
this.set('content', config.configuration[0]);
|
this.set('content', publicConfig.configuration[0]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPrivate() {
|
||||||
|
let privateConfigUrl = this.get('ghostPaths.url').api('configuration', 'private');
|
||||||
|
|
||||||
|
return this.get('ajax').request(privateConfigUrl).then((privateConfig) => {
|
||||||
|
assign(this.get('content'), privateConfig.configuration[0]);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Service from 'ember-service';
|
import Evented from '@ember/object/evented';
|
||||||
import run from 'ember-runloop';
|
import Service from '@ember/service';
|
||||||
|
import {run} from '@ember/runloop';
|
||||||
|
|
||||||
const MEDIA_QUERIES = {
|
const MEDIA_QUERIES = {
|
||||||
maxWidth600: '(max-width: 600px)',
|
maxWidth600: '(max-width: 600px)',
|
||||||
|
@ -8,7 +9,7 @@ const MEDIA_QUERIES = {
|
||||||
maxWidth1000: '(max-width: 1000px)'
|
maxWidth1000: '(max-width: 1000px)'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend(Evented, {
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this._handlers = [];
|
this._handlers = [];
|
||||||
|
@ -30,7 +31,8 @@ export default Service.extend({
|
||||||
let lastValue = this.get(key);
|
let lastValue = this.get(key);
|
||||||
let newValue = query.matches;
|
let newValue = query.matches;
|
||||||
if (lastValue !== newValue) {
|
if (lastValue !== newValue) {
|
||||||
this.set(key, query.matches);
|
this.set(key, newValue);
|
||||||
|
this.trigger('change', key, newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
query.addListener(handler);
|
query.addListener(handler);
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import injectService from 'ember-service/inject';
|
import injectService from 'ember-service/inject';
|
||||||
import {computed} from '@ember/object';
|
import {computed} from '@ember/object';
|
||||||
|
import {not, or, reads} from '@ember/object/computed';
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
dropdown: injectService(),
|
dropdown: injectService(),
|
||||||
|
mediaQueries: injectService(),
|
||||||
|
|
||||||
autoNav: false,
|
autoNav: false,
|
||||||
|
isFullScreen: false,
|
||||||
showMobileMenu: false,
|
showMobileMenu: false,
|
||||||
showSettingsMenu: false,
|
showSettingsMenu: false,
|
||||||
|
|
||||||
|
hasSideNav: not('isSideNavHidden'),
|
||||||
|
isMobile: reads('mediaQueries.isMobile'),
|
||||||
|
isSideNavHidden: or('autoNav', 'isFullScreen', 'isMobile'),
|
||||||
|
|
||||||
autoNavOpen: computed('autoNav', {
|
autoNavOpen: computed('autoNav', {
|
||||||
get() {
|
get() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
import Service from '@ember/service';
|
||||||
|
import fetch from 'fetch';
|
||||||
|
import injectService from 'ember-service/inject';
|
||||||
|
import {isEmpty} from '@ember/utils';
|
||||||
|
import {or} from '@ember/object/computed';
|
||||||
|
import {reject, resolve} from 'rsvp';
|
||||||
|
import {task, taskGroup, timeout} from 'ember-concurrency';
|
||||||
|
|
||||||
|
const API_URL = 'https://api.unsplash.com';
|
||||||
|
const API_VERSION = 'v1';
|
||||||
|
const DEBOUNCE_MS = 600;
|
||||||
|
|
||||||
|
export default Service.extend({
|
||||||
|
config: injectService(),
|
||||||
|
settings: injectService(),
|
||||||
|
|
||||||
|
columnCount: 3,
|
||||||
|
columns: null,
|
||||||
|
error: '',
|
||||||
|
photos: null,
|
||||||
|
searchTerm: '',
|
||||||
|
|
||||||
|
_columnHeights: null,
|
||||||
|
_pagination: null,
|
||||||
|
|
||||||
|
applicationId: or('config.unsplashAPI.applicationId', 'settings.unsplash.applicationId'),
|
||||||
|
isLoading: or('_search.isRunning', '_loadingTasks.isRunning'),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this._reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadNew() {
|
||||||
|
this._reset();
|
||||||
|
return this.get('_loadNew').perform();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadNextPage() {
|
||||||
|
// protect against scroll trigger firing when the photos are reset
|
||||||
|
if (this.get('_search.isRunning')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEmpty(this.get('photos'))) {
|
||||||
|
return this.get('_loadNew').perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._pagination.next) {
|
||||||
|
return this.get('_loadNextPage').perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: return error?
|
||||||
|
return reject();
|
||||||
|
},
|
||||||
|
|
||||||
|
changeColumnCount(newColumnCount) {
|
||||||
|
if (newColumnCount !== this.get('columnCount')) {
|
||||||
|
this.set('columnCount', newColumnCount);
|
||||||
|
this._resetColumns();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendTestRequest(testApplicationId) {
|
||||||
|
let url = `${API_URL}/photos/random`;
|
||||||
|
let headers = {};
|
||||||
|
|
||||||
|
headers.Authorization = `Client-ID ${testApplicationId}`;
|
||||||
|
headers['Accept-Version'] = API_VERSION;
|
||||||
|
|
||||||
|
return fetch(url, {headers})
|
||||||
|
.then((response) => this._checkStatus(response));
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
updateSearch(term) {
|
||||||
|
if (term === this.get('searchTerm')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('searchTerm', term);
|
||||||
|
this._reset();
|
||||||
|
|
||||||
|
if (term) {
|
||||||
|
return this.get('_search').perform(term);
|
||||||
|
} else {
|
||||||
|
return this.get('_loadNew').perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_loadingTasks: taskGroup().drop(),
|
||||||
|
|
||||||
|
_loadNew: task(function* () {
|
||||||
|
let url = `${API_URL}/photos?per_page=30`;
|
||||||
|
yield this._makeRequest(url);
|
||||||
|
}).group('_loadingTasks'),
|
||||||
|
|
||||||
|
_loadNextPage: task(function* () {
|
||||||
|
yield this._makeRequest(this._pagination.next);
|
||||||
|
}).group('_loadingTasks'),
|
||||||
|
|
||||||
|
_retryLastRequest: task(function* () {
|
||||||
|
yield this._makeRequest(this._lastRequestUrl);
|
||||||
|
}).group('_loadingTasks'),
|
||||||
|
|
||||||
|
_search: task(function* (term) {
|
||||||
|
yield timeout(DEBOUNCE_MS);
|
||||||
|
|
||||||
|
let url = `${API_URL}/search/photos?query=${term}&per_page=30`;
|
||||||
|
yield this._makeRequest(url);
|
||||||
|
}).restartable(),
|
||||||
|
|
||||||
|
_addPhotosFromResponse(response) {
|
||||||
|
let photos = response.results || response;
|
||||||
|
|
||||||
|
photos.forEach((photo) => this._addPhoto(photo));
|
||||||
|
},
|
||||||
|
|
||||||
|
_addPhoto(photo) {
|
||||||
|
// pre-calculate ratio for later use
|
||||||
|
photo.ratio = photo.height / photo.width;
|
||||||
|
|
||||||
|
// add to general photo list
|
||||||
|
this.get('photos').pushObject(photo);
|
||||||
|
|
||||||
|
// add to least populated column
|
||||||
|
this._addPhotoToColumns(photo);
|
||||||
|
},
|
||||||
|
|
||||||
|
_addPhotoToColumns(photo) {
|
||||||
|
let min = Math.min(...this._columnHeights);
|
||||||
|
let columnIndex = this._columnHeights.indexOf(min);
|
||||||
|
|
||||||
|
// use a fixed width when calculating height to compensate for different
|
||||||
|
// overall image sizes
|
||||||
|
this._columnHeights[columnIndex] += 300 * photo.ratio;
|
||||||
|
this.get('columns')[columnIndex].pushObject(photo);
|
||||||
|
},
|
||||||
|
|
||||||
|
_reset() {
|
||||||
|
this.set('photos', []);
|
||||||
|
this._pagination = {};
|
||||||
|
this._resetColumns();
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetColumns() {
|
||||||
|
let columns = [];
|
||||||
|
let columnHeights = [];
|
||||||
|
|
||||||
|
// pre-fill column arrays based on columnCount
|
||||||
|
for (let i = 0; i < this.get('columnCount'); i++) {
|
||||||
|
columns[i] = [];
|
||||||
|
columnHeights[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('columns', columns);
|
||||||
|
this._columnHeights = columnHeights;
|
||||||
|
|
||||||
|
if (!isEmpty(this.get('photos'))) {
|
||||||
|
this.get('photos').forEach((photo) => {
|
||||||
|
this._addPhotoToColumns(photo);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_makeRequest(url) {
|
||||||
|
let headers = {};
|
||||||
|
|
||||||
|
// clear any previous error
|
||||||
|
this.set('error', '');
|
||||||
|
|
||||||
|
// store the url so it can be retried if needed
|
||||||
|
this._lastRequestUrl = url;
|
||||||
|
|
||||||
|
headers.Authorization = `Client-ID ${this.get('applicationId')}`;
|
||||||
|
headers['Accept-Version'] = API_VERSION;
|
||||||
|
|
||||||
|
return fetch(url, {headers})
|
||||||
|
.then((response) => this._checkStatus(response))
|
||||||
|
.then((response) => this._extractPagination(response))
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((response) => this._addPhotosFromResponse(response))
|
||||||
|
.catch(() => {
|
||||||
|
// if the error text isn't already set then we've get a connection error from `fetch`
|
||||||
|
if (!this.get('error')) {
|
||||||
|
this.set('error', 'Uh-oh! Trouble reaching the Unsplash API, please check your connection');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_checkStatus(response) {
|
||||||
|
// successful request
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return resolve(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorText = '';
|
||||||
|
let responseTextPromise = resolve();
|
||||||
|
|
||||||
|
if (response.headers.map['content-type'] === 'application/json') {
|
||||||
|
responseTextPromise = response.json().then((json) => {
|
||||||
|
return json.errors[0];
|
||||||
|
});
|
||||||
|
} else if (response.headers.map['content-type'] === 'text/xml') {
|
||||||
|
responseTextPromise = response.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseTextPromise.then((responseText) => {
|
||||||
|
if (response.status === 403 && response.headers.map['x-ratelimit-remaining'] === '0') {
|
||||||
|
// we've hit the ratelimit on the API
|
||||||
|
errorText = 'Unsplash API rate limit reached, please try again later.';
|
||||||
|
}
|
||||||
|
|
||||||
|
errorText = errorText || responseText || `Error ${response.status}: Uh-oh! Trouble reaching the Unsplash API`;
|
||||||
|
|
||||||
|
// set error text for display in UI
|
||||||
|
this.set('error', errorText);
|
||||||
|
|
||||||
|
// throw error to prevent further processing
|
||||||
|
let error = new Error(errorText);
|
||||||
|
error.response = response;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_extractPagination(response) {
|
||||||
|
let pagination = {};
|
||||||
|
let linkRegex = new RegExp('<(.*)>; rel="(.*)"');
|
||||||
|
let {link} = response.headers.map;
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
link.split(',').forEach((link) => {
|
||||||
|
let [, url, rel] = linkRegex.exec(link);
|
||||||
|
|
||||||
|
pagination[rel] = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pagination = pagination;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
});
|
|
@ -34,6 +34,7 @@
|
||||||
@import "components/publishmenu.css";
|
@import "components/publishmenu.css";
|
||||||
@import "components/popovers.css";
|
@import "components/popovers.css";
|
||||||
@import "components/tour.css";
|
@import "components/tour.css";
|
||||||
|
@import "components/unsplash.css";
|
||||||
|
|
||||||
|
|
||||||
/* Layouts: Groups of Components
|
/* Layouts: Groups of Components
|
||||||
|
|
|
@ -0,0 +1,324 @@
|
||||||
|
/* Unsplash Integration
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/* The parent container + layout
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9000;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash--with-sidenav {
|
||||||
|
margin-left: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-window {
|
||||||
|
padding: 25px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 100px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 23px;
|
||||||
|
left: 25px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-logo svg {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-close:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-close svg {
|
||||||
|
fill: color(var(--midgrey) l(+15%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-header-desc {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-header .gh-input-icon svg {
|
||||||
|
left: 15px;
|
||||||
|
fill: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-header .gh-input-icon {
|
||||||
|
display: block;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 50px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-search {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 30px 1px 50px;
|
||||||
|
outline: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: #111;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-search:hover {
|
||||||
|
border-color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-search:focus {
|
||||||
|
border-color: #d1d1d1;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Loading styles
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash-loading {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Error styles
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash-error {
|
||||||
|
max-width: 1200px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-error-404 {
|
||||||
|
display: block;
|
||||||
|
min-height: 225px;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Photo grid and global styles
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash .gh-loading-spinner {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: stretch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-grid-column {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-content: stretch;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-grid-column:last-of-type {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo-container > img {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover overlay */
|
||||||
|
.gh-unsplash-photo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px;
|
||||||
|
color: #fff;
|
||||||
|
background-image: linear-gradient(180deg,rgba(0,0,0,0.2) 0,transparent 40%,transparent 60%,rgba(0,0,0,0.3));
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo:hover .gh-unsplash-photo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons used within photo cards */
|
||||||
|
.gh-unsplash-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.1em;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Photo overlay content
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash-photo-header {
|
||||||
|
flex-grow: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo-author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo-author-img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo-author-name {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-button-likes svg {
|
||||||
|
height: 15px;
|
||||||
|
margin-right: 5px;
|
||||||
|
fill: #ff3f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-photo-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-button-download svg {
|
||||||
|
height: 13px;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
stroke: #777;
|
||||||
|
stroke-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Photo Zoom Preview
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-unsplash-zoom {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 9500;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 25px;
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-zoom .gh-unsplash-photo {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
min-height: 400px;
|
||||||
|
max-width: 1200px;
|
||||||
|
max-height: calc(100vh - 50px);
|
||||||
|
margin: 0;
|
||||||
|
color: #fff;
|
||||||
|
cursor: zoom-out;
|
||||||
|
background: var(--darkgrey);
|
||||||
|
box-shadow: rgba(39,44,49,0.1) 8px 14px 38px, rgba(39, 44, 49, 0.08) 1px 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-zoom .gh-unsplash-photo-container > img {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-height: calc(100vh - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-unsplash-zoom .gh-unsplash-photo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
|
@ -6,14 +6,14 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 130px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 1.6em 0;
|
margin: 1.6em 0;
|
||||||
min-height: 130px;
|
|
||||||
width: 100%;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: color(var(--midgrey) l(-18%));
|
color: color(var(--midgrey) l(-18%));
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
|
|
||||||
.gh-image-uploader img {
|
.gh-image-uploader img {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,28 +40,29 @@
|
||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 300;
|
z-index: 300;
|
||||||
display: block;
|
display: block;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 10px;
|
||||||
|
text-decoration: none;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: rgba(255, 255, 255, 0.2) 0 0 0 1px;
|
box-shadow: rgba(255, 255, 255, 0.2) 0 0 0 1px;
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .image-cancel svg {
|
.gh-image-uploader .image-cancel svg {
|
||||||
fill: #fff;
|
|
||||||
height: 13px;
|
|
||||||
width: 13px;
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
fill: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .upload-form {
|
.gh-image-uploader .upload-form {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .x-file-input {
|
.gh-image-uploader .x-file-input {
|
||||||
|
@ -72,21 +73,21 @@
|
||||||
.gh-image-uploader .x-file-input label {
|
.gh-image-uploader .x-file-input label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .description {
|
.gh-image-uploader .description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .image-cancel:hover {
|
.gh-image-uploader .image-cancel:hover {
|
||||||
background: var(--red);
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-image-uploader .failed {
|
.gh-image-uploader .failed {
|
||||||
|
@ -106,9 +107,9 @@
|
||||||
|
|
||||||
.gh-image-uploader .progress,
|
.gh-image-uploader .progress,
|
||||||
.gh-progress-container-progress {
|
.gh-progress-container-progress {
|
||||||
|
width: 60%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 60%;
|
|
||||||
background: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
|
background: linear-gradient(to bottom, #f5f5f5, #f9f9f9);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
box-shadow: rgba(0, 0, 0, 0.1) 0 1px 2px inset;
|
||||||
|
@ -131,3 +132,28 @@
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Unsplash Button
|
||||||
|
/* ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
.gh-image-uploader-unsplash {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 10px;
|
||||||
|
opacity: 0.17;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader-unsplash:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-image-uploader-unsplash svg {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
|
@ -62,6 +62,10 @@ input {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group p {
|
.form-group p {
|
||||||
margin: 4px 0 0 0;
|
margin: 4px 0 0 0;
|
||||||
color: var(--midgrey);
|
color: var(--midgrey);
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
{{gh-image-uploader
|
{{gh-image-uploader
|
||||||
text=text
|
text=text
|
||||||
altText=altText
|
altText=altText
|
||||||
|
allowUnsplash=allowUnsplash
|
||||||
update=(action 'update')
|
update=(action 'update')
|
||||||
uploadStarted=(action 'uploadStarted')
|
uploadStarted=(action 'uploadStarted')
|
||||||
uploadFinished=(action 'uploadFinished')
|
uploadFinished=(action 'uploadFinished')
|
||||||
|
|
|
@ -17,5 +17,18 @@
|
||||||
{{#gh-file-input multiple=false alt=description action=(action "fileSelected") accept=accept}}
|
{{#gh-file-input multiple=false alt=description action=(action "fileSelected") accept=accept}}
|
||||||
<div class="gh-btn gh-btn-outline" data-test-file-input-description><span>{{description}}</span></div>
|
<div class="gh-btn gh-btn-outline" data-test-file-input-description><span>{{description}}</span></div>
|
||||||
{{/gh-file-input}}
|
{{/gh-file-input}}
|
||||||
|
|
||||||
|
{{#if (and allowUnsplash unsplash.isActive)}}
|
||||||
|
<div class="gh-image-uploader-unsplash" {{action (toggle "_showUnsplash" this)}}>
|
||||||
|
{{inline-svg "unsplash"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if _showUnsplash}}
|
||||||
|
{{gh-unsplash
|
||||||
|
insert=(action "addUnsplashPhoto")
|
||||||
|
close=(action (toggle "_showUnsplash" this))
|
||||||
|
}}
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
placeholder=placeholder
|
placeholder=placeholder
|
||||||
autofocus=autofocus
|
autofocus=autofocus
|
||||||
onChange=(action "updateMarkdown")
|
onChange=(action "updateMarkdown")
|
||||||
|
onFocus=(action "updateFocusState" true)
|
||||||
|
onBlur=(action "updateFocusState" false)
|
||||||
onEditorInit=(action "setEditor")
|
onEditorInit=(action "setEditor")
|
||||||
onEditorDestroy=(action "destroyEditor")
|
onEditorDestroy=(action "destroyEditor")
|
||||||
options=simpleMDEOptions)
|
options=simpleMDEOptions)
|
||||||
|
@ -15,3 +17,9 @@
|
||||||
<div style="display:none">
|
<div style="display:none">
|
||||||
{{gh-file-input multiple=true action=(action onImageFilesSelected) accept=imageMimeTypes}}
|
{{gh-file-input multiple=true action=(action onImageFilesSelected) accept=imageMimeTypes}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if _showUnsplash}}
|
||||||
|
{{gh-unsplash
|
||||||
|
insert=(action "insertUnsplashPhoto")
|
||||||
|
close=(action "toggleUnsplash")}}
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
<div class="settings-menu-content">
|
<div class="settings-menu-content">
|
||||||
{{gh-image-uploader-with-preview
|
{{gh-image-uploader-with-preview
|
||||||
image=model.featureImage
|
image=model.featureImage
|
||||||
text="Add post image"
|
text="Upload post image"
|
||||||
|
allowUnsplash=true
|
||||||
update=(action "setCoverImage")
|
update=(action "setCoverImage")
|
||||||
remove=(action "clearCoverImage")
|
remove=(action "clearCoverImage")
|
||||||
}}
|
}}
|
||||||
|
@ -231,6 +232,7 @@
|
||||||
{{gh-image-uploader-with-preview
|
{{gh-image-uploader-with-preview
|
||||||
image=model.twitterImage
|
image=model.twitterImage
|
||||||
text="Add Twitter image"
|
text="Add Twitter image"
|
||||||
|
allowUnsplash=true
|
||||||
update=(action "setTwitterImage")
|
update=(action "setTwitterImage")
|
||||||
remove=(action "clearTwitterImage")
|
remove=(action "clearTwitterImage")
|
||||||
}}
|
}}
|
||||||
|
@ -298,6 +300,7 @@
|
||||||
{{gh-image-uploader-with-preview
|
{{gh-image-uploader-with-preview
|
||||||
image=model.ogImage
|
image=model.ogImage
|
||||||
text="Add Facebook image"
|
text="Add Facebook image"
|
||||||
|
allowUnsplash=true
|
||||||
update=(action "setOgImage")
|
update=(action "setOgImage")
|
||||||
remove=(action "clearOgImage")
|
remove=(action "clearOgImage")
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{yield}}
|
|
@ -11,7 +11,8 @@
|
||||||
<div class="settings-menu-content">
|
<div class="settings-menu-content">
|
||||||
{{gh-image-uploader-with-preview
|
{{gh-image-uploader-with-preview
|
||||||
image=tag.featureImage
|
image=tag.featureImage
|
||||||
text="Add tag image"
|
text="Upload tag image"
|
||||||
|
allowUnsplash=true
|
||||||
update=(action "setCoverImage")
|
update=(action "setCoverImage")
|
||||||
remove=(action "clearCoverImage")}}
|
remove=(action "clearCoverImage")}}
|
||||||
<form>
|
<form>
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
<a class="gh-unsplash-photo" href="#" onclick={{action "zoom"}} data-test-unsplash-photo={{photo.id}}>
|
||||||
|
<div class="gh-unsplash-photo-container" style={{style}} data-test-unsplash-photo-container>
|
||||||
|
<img src={{imageUrl}} alt={{photo.description}} width={{width}} height={{height}} data-test-unsplash-photo-image />
|
||||||
|
<div class="gh-unsplash-photo-overlay">
|
||||||
|
<div class="gh-unsplash-photo-header">
|
||||||
|
<a class="gh-unsplash-button-likes gh-unsplash-button" href="{{photo.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank">{{inline-svg "unsplash-heart"}}{{photo.likes}}</a>
|
||||||
|
<a class="gh-unsplash-button-download gh-unsplash-button" href="{{photo.links.download}}/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit&force=true">{{inline-svg "download"}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="gh-unsplash-photo-footer">
|
||||||
|
<div class="gh-unsplash-photo-author">
|
||||||
|
<a class="gh-unsplash-photo-author-img" href="{{photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank">
|
||||||
|
<img src="{{photo.user.profile_image.medium}}" />
|
||||||
|
</a>
|
||||||
|
<a class="gh-unsplash-photo-author-name" href="{{photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank">
|
||||||
|
{{photo.user.name}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a class="gh-unsplash-button" href="#" onclick={{action "insert"}}>Insert image</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
|
@ -0,0 +1,73 @@
|
||||||
|
{{#liquid-wormhole class="unsplash"}}
|
||||||
|
<div class="gh-unsplash {{if ui.hasSideNav "gh-unsplash--with-sidenav"}}">
|
||||||
|
<div class="gh-unsplash-window">
|
||||||
|
<div class="gh-unsplash-container">
|
||||||
|
<a class="gh-unsplash-logo" href="https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank">{{inline-svg "unsplash"}}</a>
|
||||||
|
<div class="gh-unsplash-close" aria-role="button" {{action "close"}}>{{inline-svg "close"}}</div>
|
||||||
|
<header class="gh-unsplash-header">
|
||||||
|
<h1 class="gh-unsplash-header-title">Unsplash</h1>
|
||||||
|
<p class="gh-unsplash-header-desc">Beautiful, free photos.<br>
|
||||||
|
Gifted by the world’s most generous community of photographers. 🎁</p>
|
||||||
|
<span class="gh-input-icon">
|
||||||
|
{{inline-svg "search"}}
|
||||||
|
{{gh-input unsplash.searchTerm
|
||||||
|
class="gh-unsplash-search"
|
||||||
|
type="text"
|
||||||
|
name="searchKeyword"
|
||||||
|
placeholder="Search free high-resolution photos"
|
||||||
|
tabindex="1"
|
||||||
|
autocorrect="off"
|
||||||
|
update=(action "updateSearch" target=unsplash)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{{#if unsplash.photos}}
|
||||||
|
<section class="gh-unsplash-grid">
|
||||||
|
{{#each unsplash.columns as |photos|}}
|
||||||
|
<div class="gh-unsplash-grid-column">
|
||||||
|
{{#each photos as |photo|}}
|
||||||
|
{{gh-unsplash-photo photo=photo zoom=(action "zoomPhoto") insert=(action "insert")}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
</section>
|
||||||
|
{{else if (and unsplash.searchTerm (not unsplash.error unsplash.isLoading))}}
|
||||||
|
<section class="gh-unsplash-error">
|
||||||
|
<img class="gh-unsplash-error-404" src="assets/img/unsplash-404.png" alt="No photos found" />
|
||||||
|
<h4>No photos found for '{{unsplash.searchTerm}}'</h4>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if unsplash.error}}
|
||||||
|
{{!-- TODO: add better error styles? --}}
|
||||||
|
<section class="gh-unsplash-error">
|
||||||
|
<img class="gh-unsplash-error-404" src="assets/img/unsplash-404.png" alt="Network error" />
|
||||||
|
<h4>{{unsplash.error}} (<a href="#" {{action "retry"}}>retry</a>)</h4>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if unsplash.isLoading}}
|
||||||
|
<div class="gh-unsplash-loading">
|
||||||
|
<div class="gh-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{gh-scroll-trigger
|
||||||
|
onEnterViewport=(action "loadNextPage")
|
||||||
|
triggerOffset=1000}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if zoomedPhoto}}
|
||||||
|
<div class="gh-unsplash-zoom" {{action "closeZoom"}}>
|
||||||
|
{{gh-unsplash-photo
|
||||||
|
photo=zoomedPhoto
|
||||||
|
zoomed=true
|
||||||
|
zoom=(action "closeZoom")
|
||||||
|
insert=(action "insert")}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{/liquid-wormhole}}
|
|
@ -53,6 +53,30 @@
|
||||||
</article>
|
</article>
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="apps-grid-cell">
|
||||||
|
{{#link-to "settings.apps.unsplash" id="unsplash-link"}}
|
||||||
|
<article class="apps-card-app">
|
||||||
|
<div class="apps-card-left">
|
||||||
|
<figure class="apps-card-app-icon" style="background-image:url(assets/img/unsplashicon.png);background-size:45px;"></figure>
|
||||||
|
<div class="apps-card-meta">
|
||||||
|
<h3 class="apps-card-app-title">Unsplash</h3>
|
||||||
|
<p class="apps-card-app-desc">Beautiful, free photos</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gh-card-right">
|
||||||
|
<div class="apps-configured">
|
||||||
|
{{#if unsplash.isActive}}
|
||||||
|
<span class="green">Active</span>
|
||||||
|
{{else}}
|
||||||
|
<span>Configure</span>
|
||||||
|
{{/if}}
|
||||||
|
{{inline-svg "arrow-right"}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{{/link-to}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="apps-grid-note">(More coming soon!)</p>
|
<p class="apps-grid-note">(More coming soon!)</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
<section class="gh-canvas">
|
||||||
|
<header class="gh-canvas-header">
|
||||||
|
<h2 class="gh-canvas-title" data-test-screen-title>
|
||||||
|
{{#link-to "settings.apps.index"}}Apps{{/link-to}}
|
||||||
|
<span>{{inline-svg "arrow-right"}}</span>
|
||||||
|
Unsplash
|
||||||
|
</h2>
|
||||||
|
<section class="view-actions">
|
||||||
|
{{gh-task-button task=save class="gh-btn gh-btn-blue gh-btn-icon" data-test-save-button=true}}
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="view-container">
|
||||||
|
<br>
|
||||||
|
<section class="app-grid">
|
||||||
|
<div class="app-cell">
|
||||||
|
<img class="app-icon" src="assets/img/unsplashicon.png" />
|
||||||
|
</div>
|
||||||
|
<div class="app-cell">
|
||||||
|
<h3>Unsplash</h3>
|
||||||
|
<p>Beautiful, free photos</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="gh-setting-header">Unsplash configuration</div>
|
||||||
|
<div class="gh-setting" id="unsplash-toggle">
|
||||||
|
<div class="gh-setting-content">
|
||||||
|
<div class="gh-setting-title">Enable Unsplash</div>
|
||||||
|
<div class="gh-setting-desc">Enable <a href="https://unsplash.com" target="_blank">Unsplash</a> image integration for your posts</div>
|
||||||
|
</div>
|
||||||
|
<div class="gh-setting-action">
|
||||||
|
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="isActive" class="right"}}
|
||||||
|
<div class="for-checkbox">
|
||||||
|
<label for="isActive" class="checkbox">
|
||||||
|
{{one-way-checkbox model.isActive id="isActive" name="isActive" type="checkbox" update=(action "update") data-test-checkbox="unsplash"}}
|
||||||
|
<span class="input-toggle-component"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{gh-error-message errors=model.errors property="isActive"}}
|
||||||
|
{{/gh-form-group}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#unless config.unsplashAPI.applicationId}}
|
||||||
|
<form class="app-config-form" id="unsplash-settings" novalidate="novalidate" {{action "save" on="submit"}}>
|
||||||
|
<div class="gh-setting">
|
||||||
|
<div class="gh-setting-content">
|
||||||
|
|
||||||
|
<div class="gh-setting-title">Unsplash integration</div>
|
||||||
|
<div class="gh-setting-desc">Access Unsplash images from within the editor.</div>
|
||||||
|
<div class="gh-setting-content-extended">
|
||||||
|
{{#gh-form-group errors=model.errors hasValidated=model.hasValidated property="applicationId"}}
|
||||||
|
{{gh-input model.applicationId name="unsplash" update=(action "updateId") onenter=(action "save") focusOut=(action "validate" "applicationId" target=model) placeholder="0f387e82271755665bd49683e914856b1e34..." data-test-input="unsplash"}}
|
||||||
|
{{#unless model.errors.applicationId}}
|
||||||
|
<p>Set up a new Unsplash application <a href="https://unsplash.com/documentation#registering-your-application" target="_blank">here</a>, and grab the Application ID.</p>
|
||||||
|
{{else}}
|
||||||
|
{{gh-error-message errors=model.errors property="applicationId"}}
|
||||||
|
{{/unless}}
|
||||||
|
{{/gh-form-group}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{gh-task-button "Test Application ID" task=sendTestRequest runningText="Validating" successText="Valid Application ID" failureText="Invalid Application Id" class="gh-btn gh-btn-green gh-btn-icon" disabled=testRequestDisabled data-test-button="send-request"}}
|
||||||
|
{{/unless}}
|
||||||
|
</section>
|
||||||
|
</section>
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
import Transform from 'ember-data/transform';
|
||||||
|
import UnsplashObject from 'ghost-admin/models/unsplash-integration';
|
||||||
|
|
||||||
|
export default Transform.extend({
|
||||||
|
deserialize(serialized) {
|
||||||
|
if (serialized) {
|
||||||
|
let settingsObject;
|
||||||
|
try {
|
||||||
|
settingsObject = JSON.parse(serialized) || {};
|
||||||
|
} catch (e) {
|
||||||
|
settingsObject = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnsplashObject.create(settingsObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
serialize(deserialized) {
|
||||||
|
return deserialized ? JSON.stringify(deserialized) : {};
|
||||||
|
}
|
||||||
|
});
|
|
@ -24,4 +24,12 @@ export default function () {
|
||||||
this.use('fade', {duration: 300}),
|
this.use('fade', {duration: 300}),
|
||||||
this.reverse('fade', {duration: 300})
|
this.reverse('fade', {duration: 300})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Maybe animate with explode. gh-unsplash-window should ideally slide in from bottom to top of screen
|
||||||
|
// this.transition(
|
||||||
|
// this.hasClass('gh-unsplash-window'),
|
||||||
|
// this.toValue(true),
|
||||||
|
// this.use('toUp', {duration: 500}),
|
||||||
|
// this.reverse('toDown', {duration: 500})
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import BaseValidator from './base';
|
||||||
|
|
||||||
|
export default BaseValidator.create({
|
||||||
|
properties: ['applicationId'],
|
||||||
|
|
||||||
|
applicationId(model) {
|
||||||
|
let applicationId = model.get('applicationId');
|
||||||
|
let hasValidated = model.get('hasValidated');
|
||||||
|
|
||||||
|
let whiteSpaceRegex = new RegExp(/^\S*$/gi);
|
||||||
|
|
||||||
|
if (!applicationId.match(whiteSpaceRegex)) {
|
||||||
|
model.get('errors').add(
|
||||||
|
'applicationId',
|
||||||
|
'Please enter a valid Application Id for Unsplash'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasValidated.addObject('applicationId');
|
||||||
|
}
|
||||||
|
});
|
|
@ -37,7 +37,7 @@ export function testConfig() {
|
||||||
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
// this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server
|
||||||
this.namespace = '/ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced
|
this.namespace = '/ghost/api/v0.1'; // make this `api`, for example, if your API is namespaced
|
||||||
// this.timing = 400; // delay for each request, automatically set to 0 during testing
|
// this.timing = 400; // delay for each request, automatically set to 0 during testing
|
||||||
this.logging = true;
|
// this.logging = true;
|
||||||
|
|
||||||
mockAuthentication(this);
|
mockAuthentication(this);
|
||||||
mockConfiguration(this);
|
mockConfiguration(this);
|
||||||
|
|
|
@ -22,4 +22,14 @@ export default function mockConfiguration(server) {
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get('/configuration/private/', function ({db}) {
|
||||||
|
if (isEmpty(db.private)) {
|
||||||
|
server.loadFixtures('private');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
configuration: [db.private]
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default [{
|
||||||
|
unsplashAPI: ''
|
||||||
|
}];
|
|
@ -192,5 +192,15 @@ export default [
|
||||||
created_by: 1,
|
created_by: 1,
|
||||||
updated_at: '2015-10-27T17:39:58.276Z',
|
updated_at: '2015-10-27T17:39:58.276Z',
|
||||||
updated_by: 1
|
updated_by: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
created_at: '2017-08-11T06:38:10.000Z',
|
||||||
|
created_by: 1,
|
||||||
|
key: 'unsplash',
|
||||||
|
type: 'blog',
|
||||||
|
updated_at: '2017-08-11T08:00:14.000Z',
|
||||||
|
updated_by: 1,
|
||||||
|
value: '{"applicationId":"","isActive":false}'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -71,6 +71,8 @@
|
||||||
"ember-data-filter": "1.13.0",
|
"ember-data-filter": "1.13.0",
|
||||||
"ember-element-resize-detector": "0.1.5",
|
"ember-element-resize-detector": "0.1.5",
|
||||||
"ember-export-application-global": "2.0.0",
|
"ember-export-application-global": "2.0.0",
|
||||||
|
"ember-fetch": "3.2.9",
|
||||||
|
"ember-in-viewport": "2.1.1",
|
||||||
"ember-infinity": "0.2.8",
|
"ember-infinity": "0.2.8",
|
||||||
"ember-inline-svg": "0.1.11",
|
"ember-inline-svg": "0.1.11",
|
||||||
"ember-invoke-action": "1.4.0",
|
"ember-invoke-action": "1.4.0",
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M20 5.5l-8 8-8-8m-3.5 13h23" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 217 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg version="1.1" viewBox="0 0 32 32"><path d="M17.4 29c-.8.8-2 .8-2.8 0l-12.3-12.8c-3.1-3.1-3.1-8.2 0-11.4 3.1-3.1 8.2-3.1 11.3 0l2.4 2.8 2.3-2.8c3.1-3.1 8.2-3.1 11.3 0 3.1 3.1 3.1 8.2 0 11.4l-12.2 12.8z"></path></svg>
|
After Width: | Height: | Size: 221 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 32 32"><path d="M20.8 18.1c0 2.7-2.2 4.8-4.8 4.8s-4.8-2.1-4.8-4.8c0-2.7 2.2-4.8 4.8-4.8 2.7.1 4.8 2.2 4.8 4.8zm11.2-7.4v14.9c0 2.3-1.9 4.3-4.3 4.3h-23.4c-2.4 0-4.3-1.9-4.3-4.3v-15c0-2.3 1.9-4.3 4.3-4.3h3.7l.8-2.3c.4-1.1 1.7-2 2.9-2h8.6c1.2 0 2.5.9 2.9 2l.8 2.4h3.7c2.4 0 4.3 1.9 4.3 4.3zm-8.6 7.5c0-4.1-3.3-7.5-7.5-7.5-4.1 0-7.5 3.4-7.5 7.5s3.3 7.5 7.5 7.5c4.2-.1 7.5-3.4 7.5-7.5z"></path></svg>
|
After Width: | Height: | Size: 414 B |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -78,5 +78,16 @@ describe('Acceptance: Settings - Apps', function () {
|
||||||
// has correct url
|
// has correct url
|
||||||
expect(currentURL(), 'currentURL').to.equal('/settings/apps/amp');
|
expect(currentURL(), 'currentURL').to.equal('/settings/apps/amp');
|
||||||
});
|
});
|
||||||
|
it('it redirects to Unsplash when clicking on the grid', async function () {
|
||||||
|
await visit('/settings/apps');
|
||||||
|
|
||||||
|
// has correct url
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/settings/apps');
|
||||||
|
|
||||||
|
await click('#unsplash-link');
|
||||||
|
|
||||||
|
// has correct url
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/settings/apps/unsplash');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
/* jshint expr:true */
|
||||||
|
import Mirage from 'ember-cli-mirage';
|
||||||
|
import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd';
|
||||||
|
import destroyApp from '../../helpers/destroy-app';
|
||||||
|
import startApp from '../../helpers/start-app';
|
||||||
|
import {afterEach, beforeEach, describe, it} from 'mocha';
|
||||||
|
import {authenticateSession, invalidateSession} from 'ghost-admin/tests/helpers/ember-simple-auth';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
|
||||||
|
describe('Acceptance: Settings - Apps - Unsplash', function () {
|
||||||
|
let application;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
application = startApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
destroyApp(application);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to signin when not authenticated', async function () {
|
||||||
|
invalidateSession(application);
|
||||||
|
await visit('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/signin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to team page when authenticated as author', async function () {
|
||||||
|
let role = server.create('role', {name: 'Author'});
|
||||||
|
server.create('user', {roles: [role], slug: 'test-user'});
|
||||||
|
|
||||||
|
authenticateSession(application);
|
||||||
|
await visit('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/team/test-user');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to team page when authenticated as editor', async function () {
|
||||||
|
let role = server.create('role', {name: 'Editor'});
|
||||||
|
server.create('user', {roles: [role], slug: 'test-user'});
|
||||||
|
|
||||||
|
authenticateSession(application);
|
||||||
|
await visit('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/team');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when logged in', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
let role = server.create('role', {name: 'Administrator'});
|
||||||
|
server.create('user', {roles: [role]});
|
||||||
|
|
||||||
|
return authenticateSession(application);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it validates and saves an application id properly', async function () {
|
||||||
|
server.get('/configuration/private', function () {
|
||||||
|
return new Mirage.Response(200, {}, {
|
||||||
|
configuration: {
|
||||||
|
unsplashAPI: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
// has correct url
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
// without application id provided via config
|
||||||
|
expect(find('[data-test-input="unsplash"]').text().trim(), 'default application id value')
|
||||||
|
.to.equal('');
|
||||||
|
|
||||||
|
expect(find('[data-test-checkbox="unsplash"]').prop('checked'), 'isActive checkbox').to.be.false;
|
||||||
|
|
||||||
|
await click(find('[data-test-checkbox="unsplash"]'), 'enable without application id');
|
||||||
|
|
||||||
|
expect(find('[data-test-checkbox="unsplash"]').prop('checked'), 'isActive checkbox').to.be.true;
|
||||||
|
|
||||||
|
expect(find('#unsplash-toggle .error .response').text().trim(), 'inline validation checkbox')
|
||||||
|
.to.equal('You need to enter an Application ID before enabling it');
|
||||||
|
|
||||||
|
await fillIn('[data-test-input="unsplash"]', '345 456 567 ');
|
||||||
|
|
||||||
|
expect(find('#unsplash-toggle .error .response').text().trim(), 'inline validation checkbox')
|
||||||
|
.to.equal('');
|
||||||
|
|
||||||
|
await click('[data-test-save-button]');
|
||||||
|
|
||||||
|
// application id validation
|
||||||
|
expect(find('#unsplash-settings .error .response').text().trim(), 'inline validation response')
|
||||||
|
.to.equal('Please enter a valid Application Id for Unsplash');
|
||||||
|
|
||||||
|
// doesn't save when errors
|
||||||
|
expect(find('[data-test-save-button]').text().trim(), 'task button saved response')
|
||||||
|
.to.equal('Retry');
|
||||||
|
|
||||||
|
// CMD-S shortcut works
|
||||||
|
await fillIn('[data-test-input="unsplash"]', '123456789012345678901234567890');
|
||||||
|
await triggerEvent('.gh-app', 'keydown', {
|
||||||
|
keyCode: 83, // s
|
||||||
|
metaKey: ctrlOrCmd === 'command',
|
||||||
|
ctrlKey: ctrlOrCmd === 'ctrl'
|
||||||
|
});
|
||||||
|
|
||||||
|
let [firstRequest] = server.pretender.handledRequests.slice(-1);
|
||||||
|
let params = JSON.parse(firstRequest.requestBody);
|
||||||
|
let result = JSON.parse(params.settings.findBy('key', 'unsplash').value);
|
||||||
|
|
||||||
|
expect(result.applicationId).to.equal('123456789012345678901234567890');
|
||||||
|
expect(find('#unsplash-settings .error .response').text().trim(), 'inline validation response')
|
||||||
|
.to.equal('');
|
||||||
|
|
||||||
|
server.get('https://api.unsplash.com/photos/random', function () {
|
||||||
|
return new Mirage.Response(401, {}, {
|
||||||
|
errors: ['OAuth error: The access token is invalid']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test invalid application id
|
||||||
|
await fillIn('[data-test-input="unsplash"]', '098765432109876543210987654321');
|
||||||
|
await click('[data-test-button="send-request"]');
|
||||||
|
|
||||||
|
expect(find('[data-test-button="send-request"]').text().trim(), 'test request button validation response')
|
||||||
|
.to.equal('Invalid Application Id');
|
||||||
|
|
||||||
|
expect(find('.gh-alert-red .gh-alert-content').text().trim(), 'server response')
|
||||||
|
.to.equal('OAuth error: The access token is invalid');
|
||||||
|
|
||||||
|
let [secondRequest] = server.pretender.handledRequests.slice(-1);
|
||||||
|
|
||||||
|
// Result shouldn't be saved
|
||||||
|
expect(secondRequest.requestBody).to.equal(null);
|
||||||
|
expect(secondRequest.url).to.equal('https://api.unsplash.com/photos/random');
|
||||||
|
|
||||||
|
server.get('https://api.unsplash.com/photos/random', function () {
|
||||||
|
return new Mirage.Response(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test valid application id
|
||||||
|
await fillIn('[data-test-input="unsplash"]', '098765432109876543210987654321');
|
||||||
|
await click('[data-test-button="send-request"]');
|
||||||
|
|
||||||
|
expect(find('[data-test-button="send-request"]').text().trim(), 'test request button validation response')
|
||||||
|
.to.equal('Valid Application ID');
|
||||||
|
|
||||||
|
// saves settings when valid application id
|
||||||
|
expect(find('[data-test-save-button]').text().trim(), 'task button saved response')
|
||||||
|
.to.equal('Saved');
|
||||||
|
|
||||||
|
let [thirdRequest] = server.pretender.handledRequests.slice(-1);
|
||||||
|
params = JSON.parse(thirdRequest.requestBody);
|
||||||
|
result = JSON.parse(params.settings.findBy('key', 'unsplash').value);
|
||||||
|
|
||||||
|
// Result should be saved
|
||||||
|
expect(result.applicationId).to.equal('098765432109876543210987654321');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render application id input when config provides it', async function () {
|
||||||
|
server.get('/configuration/private', function () {
|
||||||
|
return new Mirage.Response(200, {}, {
|
||||||
|
configuration: [{
|
||||||
|
unsplashAPI: {
|
||||||
|
applicationId: '12345678923456789'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
// has correct url
|
||||||
|
expect(currentURL(), 'currentURL').to.equal('/settings/apps/unsplash');
|
||||||
|
|
||||||
|
// without application id provided via config
|
||||||
|
expect(find('[data-test-input="unsplash"]').length, 'no application id input')
|
||||||
|
.to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -453,4 +453,11 @@ describe('Integration: Component: gh-image-uploader', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('unsplash', function () {
|
||||||
|
it('has unsplash icon only when unsplash is active & allowed');
|
||||||
|
it('opens unsplash modal when icon clicked');
|
||||||
|
it('inserts unsplash image when selected');
|
||||||
|
it('closes unsplash modal when close is triggered');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,4 +21,13 @@ describe('Integration: Component: gh-markdown-editor', function() {
|
||||||
this.render(hbs`{{gh-markdown-editor}}`);
|
this.render(hbs`{{gh-markdown-editor}}`);
|
||||||
expect(this.$()).to.have.length(1);
|
expect(this.$()).to.have.length(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('unsplash', function () {
|
||||||
|
it('has unsplash icon in toolbar if unsplash is active');
|
||||||
|
it('opens unsplash modal when clicked');
|
||||||
|
it('closes unsplash modal when close triggered');
|
||||||
|
it('inserts unsplash image & credit when selected');
|
||||||
|
it('inserts at cursor when editor has focus');
|
||||||
|
it('inserts at end when editor is blurred');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {find} from 'ember-native-dom-helpers';
|
||||||
|
import {setupComponentTest} from 'ember-mocha';
|
||||||
|
|
||||||
|
describe('Integration: Component: gh-unsplash-photo', function() {
|
||||||
|
setupComponentTest('gh-unsplash-photo', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
// NOTE: images.unsplash.com replaced with example.com to ensure we aren't
|
||||||
|
// loading lots of images during tests and we get an immediate 404
|
||||||
|
this.set('photo', {
|
||||||
|
'id': 'OYFHT4X5isg',
|
||||||
|
'created_at': '2017-08-09T00:20:42-04:00',
|
||||||
|
'updated_at': '2017-08-11T08:27:42-04:00',
|
||||||
|
'width': 5184,
|
||||||
|
'height': 3456,
|
||||||
|
'color': '#A8A99B',
|
||||||
|
'likes': 58,
|
||||||
|
'liked_by_user': false,
|
||||||
|
'description': null,
|
||||||
|
'user': {
|
||||||
|
'id': 'cEpP9pR9Q7E',
|
||||||
|
'updated_at': '2017-08-11T08:27:42-04:00',
|
||||||
|
'username': 'danotis',
|
||||||
|
'name': 'Dan Otis',
|
||||||
|
'first_name': 'Dan',
|
||||||
|
'last_name': 'Otis',
|
||||||
|
'twitter_username': 'danotis',
|
||||||
|
'portfolio_url': 'http://dan.exposure.co',
|
||||||
|
'bio': 'Senior Visual Designer at Huge ',
|
||||||
|
'location': 'San Jose, CA',
|
||||||
|
'total_likes': 0,
|
||||||
|
'total_photos': 8,
|
||||||
|
'total_collections': 0,
|
||||||
|
'profile_image': {
|
||||||
|
'small': 'https://example.com/profile-fb-1502251227-8fe7a0522137.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=37f67120fc464d7d920ff23c84963b38',
|
||||||
|
'medium': 'https://example.com/profile-fb-1502251227-8fe7a0522137.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=0a4f8a583caec826ac6b1ca80161fa43',
|
||||||
|
'large': 'https://example.com/profile-fb-1502251227-8fe7a0522137.jpg?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=b3aa4206e5d87f3eaa7bbe9180ebcd2b'
|
||||||
|
},
|
||||||
|
'links': {
|
||||||
|
'self': 'https://api.unsplash.com/users/danotis',
|
||||||
|
'html': 'https://unsplash.com/@danotis',
|
||||||
|
'photos': 'https://api.unsplash.com/users/danotis/photos',
|
||||||
|
'likes': 'https://api.unsplash.com/users/danotis/likes',
|
||||||
|
'portfolio': 'https://api.unsplash.com/users/danotis/portfolio',
|
||||||
|
'following': 'https://api.unsplash.com/users/danotis/following',
|
||||||
|
'followers': 'https://api.unsplash.com/users/danotis/followers'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'current_user_collections': [],
|
||||||
|
'urls': {
|
||||||
|
'raw': 'https://example.com/photo-1502252430442-aac78f397426',
|
||||||
|
'full': 'https://example.com/photo-1502252430442-aac78f397426?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&s=20f86c2f7bbb019122498a45d8260ee9',
|
||||||
|
'regular': 'https://example.com/photo-1502252430442-aac78f397426?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&s=181760db8b7a61fa60a35277d7eb434e',
|
||||||
|
'small': 'https://example.com/photo-1502252430442-aac78f397426?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&s=1e2265597b59e874a1a002b4c3fd961c',
|
||||||
|
'thumb': 'https://example.com/photo-1502252430442-aac78f397426?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=57c86b0692bea92a282b9ab0dbfdacf4'
|
||||||
|
},
|
||||||
|
'categories': [],
|
||||||
|
'links': {
|
||||||
|
'self': 'https://api.unsplash.com/photos/OYFHT4X5isg',
|
||||||
|
'html': 'https://unsplash.com/photos/OYFHT4X5isg',
|
||||||
|
'download': 'https://unsplash.com/photos/OYFHT4X5isg/download',
|
||||||
|
'download_location': 'https://api.unsplash.com/photos/OYFHT4X5isg/download'
|
||||||
|
},
|
||||||
|
'ratio': 0.6666666666666666
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets background-color style', function () {
|
||||||
|
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-unsplash-photo-container]').attributes.style.value
|
||||||
|
).to.have.string('background-color: #A8A99B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets padding-bottom style', function () {
|
||||||
|
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
|
||||||
|
|
||||||
|
// don't check full padding-bottom value as it will likely vary across
|
||||||
|
// browsers
|
||||||
|
expect(
|
||||||
|
find('[data-test-unsplash-photo-container]').attributes.style.value
|
||||||
|
).to.have.string('padding-bottom: 66.66');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses correct image size url', function () {
|
||||||
|
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-unsplash-photo-image]').attributes.src.value
|
||||||
|
).to.have.string('&w=1200');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calculates image width/height', function () {
|
||||||
|
this.render(hbs`{{gh-unsplash-photo photo=photo}}`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-unsplash-photo-image]').attributes.width.value
|
||||||
|
).to.equal('1200');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find('[data-test-unsplash-photo-image]').attributes.height.value
|
||||||
|
).to.equal('800');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers insert action');
|
||||||
|
it('triggers zoom action');
|
||||||
|
|
||||||
|
describe('zoomed', function () {
|
||||||
|
it('omits padding-bottom style');
|
||||||
|
it('triggers insert action');
|
||||||
|
it('triggers zoom action');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {setupComponentTest} from 'ember-mocha';
|
||||||
|
|
||||||
|
describe('Integration: Component: gh-unsplash', function() {
|
||||||
|
setupComponentTest('gh-unsplash', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', function() {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
// Template block usage:
|
||||||
|
// this.render(hbs`
|
||||||
|
// {{#gh-unsplash}}
|
||||||
|
// template content
|
||||||
|
// {{/gh-unsplash}}
|
||||||
|
// `);
|
||||||
|
|
||||||
|
this.render(hbs`{{gh-unsplash}}`);
|
||||||
|
expect(this.$()).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads new photos by default');
|
||||||
|
it('has responsive columns');
|
||||||
|
it('can zoom');
|
||||||
|
it('can close zoom by clicking on image');
|
||||||
|
it('can close zoom by clicking outside image');
|
||||||
|
it('triggers insert action');
|
||||||
|
it('handles errors');
|
||||||
|
|
||||||
|
describe('searching', function () {
|
||||||
|
it('works');
|
||||||
|
it('handles no results');
|
||||||
|
it('handles error');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('closing', function () {
|
||||||
|
it('triggers close action');
|
||||||
|
it('can be triggerd by escape key');
|
||||||
|
it('cannot be triggered by escape key when zoomed');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,23 +0,0 @@
|
||||||
/* jshint expr:true */
|
|
||||||
import {describe, it} from 'mocha';
|
|
||||||
import {expect} from 'chai';
|
|
||||||
import {setupComponentTest} from 'ember-mocha';
|
|
||||||
|
|
||||||
describe('Unit: Component: gh-infinite-scroll', function () {
|
|
||||||
setupComponentTest('gh-infinite-scroll', {
|
|
||||||
unit: true
|
|
||||||
// specify the other units that are required for this test
|
|
||||||
// needs: ['component:foo', 'helper:bar']
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders', function () {
|
|
||||||
// creates the component instance
|
|
||||||
let component = this.subject();
|
|
||||||
|
|
||||||
expect(component._state).to.equal('preRender');
|
|
||||||
|
|
||||||
// renders the component on the page
|
|
||||||
this.render();
|
|
||||||
expect(component._state).to.equal('inDOM');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -11,7 +11,8 @@ describe('Unit:Serializer: setting', function() {
|
||||||
'transform:facebook-url-user',
|
'transform:facebook-url-user',
|
||||||
'transform:twitter-url-user',
|
'transform:twitter-url-user',
|
||||||
'transform:navigation-settings',
|
'transform:navigation-settings',
|
||||||
'transform:slack-settings'
|
'transform:slack-settings',
|
||||||
|
'transform:unsplash-settings'
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,10 @@ import {setupTest} from 'ember-mocha';
|
||||||
|
|
||||||
describe('Unit: Service: ui', function() {
|
describe('Unit: Service: ui', function() {
|
||||||
setupTest('service:ui', {
|
setupTest('service:ui', {
|
||||||
needs: ['service:dropdown']
|
needs: [
|
||||||
|
'service:dropdown',
|
||||||
|
'service:mediaQueries'
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace this with your real tests.
|
// Replace this with your real tests.
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
import Pretender from 'pretender';
|
||||||
|
import wait from 'ember-test-helpers/wait';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {errorOverride, errorReset} from '../../helpers/adapter-error';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {run} from '@ember/runloop';
|
||||||
|
import {setupTest} from 'ember-mocha';
|
||||||
|
|
||||||
|
describe('Unit: Service: unsplash', function() {
|
||||||
|
setupTest('service:unsplash', {
|
||||||
|
needs: [
|
||||||
|
'service:ajax',
|
||||||
|
'service:config',
|
||||||
|
'service:ghostPaths',
|
||||||
|
'service:settings'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let server;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
server = new Pretender();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
server.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace this with your real tests.
|
||||||
|
it('exists', function() {
|
||||||
|
let service = this.subject();
|
||||||
|
expect(service).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can send a test request');
|
||||||
|
it('can load new');
|
||||||
|
it('can load next page');
|
||||||
|
|
||||||
|
describe('search', function () {
|
||||||
|
it('sends search request');
|
||||||
|
it('debounces query updates');
|
||||||
|
it('can load next page of search results');
|
||||||
|
it('clears photos when starting new search');
|
||||||
|
it('loads new when query is cleared');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('columns', function () {
|
||||||
|
it('sorts photos into columns based on column height');
|
||||||
|
it('can change column count');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', function () {
|
||||||
|
it('handles rate limit exceeded', async function () {
|
||||||
|
server.get('https://api.unsplash.com/photos', function () {
|
||||||
|
return [403, {'x-ratelimit-remaining': '0'}, 'Rate Limit Exceeded'];
|
||||||
|
});
|
||||||
|
|
||||||
|
let service = this.subject();
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
service.loadNextPage();
|
||||||
|
});
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
errorOverride();
|
||||||
|
expect(service.get('error')).to.have.string('Unsplash API rate limit reached');
|
||||||
|
errorReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles json errors', async function () {
|
||||||
|
server.get('https://api.unsplash.com/photos', function () {
|
||||||
|
return [500, {'Content-Type': 'application/json'}, JSON.stringify({
|
||||||
|
errors: ['Unsplash API Error']
|
||||||
|
})];
|
||||||
|
});
|
||||||
|
|
||||||
|
let service = this.subject();
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
service.loadNextPage();
|
||||||
|
});
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
errorOverride();
|
||||||
|
expect(service.get('error')).to.equal('Unsplash API Error');
|
||||||
|
errorReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles text errors', async function () {
|
||||||
|
server.get('https://api.unsplash.com/photos', function () {
|
||||||
|
return [500, {'Content-Type': 'text/xml'}, 'Unsplash text error'];
|
||||||
|
});
|
||||||
|
|
||||||
|
let service = this.subject();
|
||||||
|
|
||||||
|
run(() => {
|
||||||
|
service.loadNextPage();
|
||||||
|
});
|
||||||
|
await wait();
|
||||||
|
|
||||||
|
errorOverride();
|
||||||
|
expect(service.get('error')).to.equal('Unsplash text error');
|
||||||
|
errorReset();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isLoading', function () {
|
||||||
|
it('is false by default');
|
||||||
|
it('is true when loading new');
|
||||||
|
it('is true when loading next page');
|
||||||
|
it('is true when searching');
|
||||||
|
it('returns to false when finished');
|
||||||
|
});
|
||||||
|
});
|
11
yarn.lock
11
yarn.lock
|
@ -3355,6 +3355,17 @@ ember-factory-for-polyfill@^1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-version-checker "^1.2.0"
|
ember-cli-version-checker "^1.2.0"
|
||||||
|
|
||||||
|
ember-fetch@3.2.9:
|
||||||
|
version "3.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-3.2.9.tgz#91670b320acb5993128555ea70ce4d168cb1db26"
|
||||||
|
dependencies:
|
||||||
|
broccoli-funnel "^1.2.0"
|
||||||
|
broccoli-stew "^1.4.2"
|
||||||
|
broccoli-templater "^1.0.0"
|
||||||
|
ember-cli-babel "^6.0.0"
|
||||||
|
node-fetch "^2.0.0-alpha.3"
|
||||||
|
whatwg-fetch "^2.0.3"
|
||||||
|
|
||||||
ember-fetch@^1.4.2:
|
ember-fetch@^1.4.2:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-1.6.0.tgz#cf93d3f7049c593f14d11b6f0924b746303b7812"
|
resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-1.6.0.tgz#cf93d3f7049c593f14d11b6f0924b746303b7812"
|
||||||
|
|
Loading…
Reference in New Issue