mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added basic image editing alpha feature (#16669)
refs https://github.com/TryGhost/Team/issues/3034 - adds new alpha feature flag for image editing in Admin - allows new config for Pintura files that enable the image editing in Admin - adds new ember component for triggering image editing for post feature images --------- Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
This commit is contained in:
parent
bd04eb3d21
commit
48030c3050
16 changed files with 317 additions and 10 deletions
|
@ -567,3 +567,9 @@ add|ember-template-lint|no-invalid-interactive|5|53|5|53|9647ef6afba919b2af04fe5
|
|||
remove|ember-template-lint|no-invalid-interactive|1|103|1|103|f5a46b2538fbf79a40f2683ff1151ca60e0fa0ca|1680652800000|1691020800000|1696204800000|app/components/gh-context-menu.hbs
|
||||
add|ember-template-lint|no-invalid-interactive|1|103|1|103|534029ab0ba1b74eff4a2f31c8b4dd9f1460316a|1681430400000|1691798400000|1696982400000|app/components/gh-context-menu.hbs
|
||||
add|ember-template-lint|no-action|7|49|7|49|141d456b03124abca146e58e4ae15825fdd040bb|1681689600000|1692057600000|1697241600000|app/components/modal-post-history.hbs
|
||||
add|ember-template-lint|no-invalid-interactive|3|4|3|4|8e05ced79d8f5f5e13cd43033670b3b1e14db3ab|1681776000000|1692144000000|1697328000000|app/components/gh-editor-feature-image.hbs
|
||||
remove|ember-template-lint|no-invalid-interactive|3|4|3|4|66accf6cbc192fd9273063ef67798572309ff1bb|1675296000000|1685660400000|1690844400000|app/components/gh-editor-feature-image.hbs
|
||||
add|ember-template-lint|link-href-attributes|4|8|4|8|2078c6e43d1e5548ae745b7ac8cb736f7d2aaf68|1681776000000|1692144000000|1697328000000|app/components/gh-image-uploader-with-preview.hbs
|
||||
add|ember-template-lint|no-invalid-interactive|4|60|4|60|2078c6e43d1e5548ae745b7ac8cb736f7d2aaf68|1681776000000|1692144000000|1697328000000|app/components/gh-image-uploader-with-preview.hbs
|
||||
remove|ember-template-lint|link-href-attributes|4|8|4|8|a9119f612d27ee1fc92e9bb2c77b6fd30cab622a|1675296000000|1685660400000|1690844400000|app/components/gh-image-uploader-with-preview.hbs
|
||||
remove|ember-template-lint|no-invalid-interactive|4|47|4|47|a9119f612d27ee1fc92e9bb2c77b6fd30cab622a|1675296000000|1685660400000|1690844400000|app/components/gh-image-uploader-with-preview.hbs
|
||||
|
|
|
@ -39,7 +39,13 @@
|
|||
</span>
|
||||
<div class="gh-editor-feature-image">
|
||||
<img src={{@image}}>
|
||||
<button type="button" class="image-delete" title="Delete image" {{on "click" @clearImage}}>
|
||||
{{#if (feature 'imageEditor')}}
|
||||
<KoenigImageEditor
|
||||
@imageSrc={{@image}}
|
||||
@saveImage={{fn this.saveImage uploader.setFiles}}
|
||||
/>
|
||||
{{/if}}
|
||||
<button type="button" class="image-action image-delete" title="Delete image" {{on "click" @clearImage}}>
|
||||
{{svg-jar "trash"}}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -86,4 +86,10 @@ export default class GhEditorFeatureImageComponent extends Component {
|
|||
|
||||
setFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
@action
|
||||
saveImage(setFiles, imageFile) {
|
||||
this.canDrop = false;
|
||||
setFiles([imageFile]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{{#if @image}}
|
||||
<div class="gh-image-uploader -with-image">
|
||||
<div><img src={{@image}}></div>
|
||||
<a class="image-delete" title="Delete" {{on "click" (fn @remove "")}}>
|
||||
<a class="image-action image-delete" title="Delete" {{on "click" (fn @remove "")}}>
|
||||
{{svg-jar "trash"}}
|
||||
<span class="hidden">Delete</span>
|
||||
</a>
|
||||
|
|
5
ghost/admin/app/components/koenig-image-editor.hbs
Normal file
5
ghost/admin/app/components/koenig-image-editor.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
{{#if this.isEditorEnabled}}
|
||||
<button type="button" class="image-action image-edit" title="Edit image" {{on "click" this.handleClick}}>
|
||||
{{svg-jar "pen"}}
|
||||
</button>
|
||||
{{/if}}
|
140
ghost/admin/app/components/koenig-image-editor.js
Normal file
140
ghost/admin/app/components/koenig-image-editor.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class KoenigImageEditor extends Component {
|
||||
@service ajax;
|
||||
@service feature;
|
||||
@service ghostPaths;
|
||||
@tracked scriptLoaded = false;
|
||||
@tracked cssLoaded = false;
|
||||
|
||||
@inject config;
|
||||
|
||||
get isEditorEnabled() {
|
||||
return this.scriptLoaded && this.cssLoaded;
|
||||
}
|
||||
|
||||
getImageEditorJSUrl() {
|
||||
if (!this.config.pintura?.js) {
|
||||
return null;
|
||||
}
|
||||
let importUrl = this.config.pintura.js;
|
||||
|
||||
// load the script from admin root if relative
|
||||
if (importUrl.startsWith('/')) {
|
||||
importUrl = window.location.origin + this.ghostPaths.adminRoot.replace(/\/$/, '') + importUrl;
|
||||
}
|
||||
return importUrl;
|
||||
}
|
||||
|
||||
getImageEditorCSSUrl() {
|
||||
if (!this.config.pintura?.css) {
|
||||
return null;
|
||||
}
|
||||
let cssImportUrl = this.config.pintura.css;
|
||||
|
||||
// load the css from admin root if relative
|
||||
if (cssImportUrl.startsWith('/')) {
|
||||
cssImportUrl = window.location.origin + this.ghostPaths.adminRoot.replace(/\/$/, '') + cssImportUrl;
|
||||
}
|
||||
return cssImportUrl;
|
||||
}
|
||||
|
||||
loadImageEditorJavascript() {
|
||||
const jsUrl = this.getImageEditorJSUrl();
|
||||
|
||||
if (!jsUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.pintura) {
|
||||
this.scriptLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(jsUrl);
|
||||
|
||||
let importScriptPromise;
|
||||
|
||||
if (url.protocol === 'http:') {
|
||||
importScriptPromise = import(`http://${url.host}${url.pathname}`);
|
||||
} else {
|
||||
importScriptPromise = import(`https://${url.host}${url.pathname}`);
|
||||
}
|
||||
|
||||
importScriptPromise.then(() => {
|
||||
this.scriptLoaded = true;
|
||||
}).catch(() => {
|
||||
// log script loading failure
|
||||
});
|
||||
} catch (e) {
|
||||
// Log script loading error
|
||||
}
|
||||
}
|
||||
|
||||
loadImageEditorCSS() {
|
||||
let cssUrl = this.getImageEditorCSSUrl();
|
||||
if (!cssUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the CSS file is already present in the document's head
|
||||
let cssLink = document.querySelector(`link[href="${cssUrl}"]`);
|
||||
if (cssLink) {
|
||||
this.cssLoaded = true;
|
||||
} else {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = cssUrl;
|
||||
link.onload = () => {
|
||||
this.cssLoaded = true;
|
||||
};
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
} catch (e) {
|
||||
// Log css loading error
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
// Load the image editor script and css if not already loaded
|
||||
this.loadImageEditorJavascript();
|
||||
this.loadImageEditorCSS();
|
||||
}
|
||||
|
||||
@action
|
||||
async handleClick() {
|
||||
if (window.pintura) {
|
||||
const editor = window.pintura.openDefaultEditor({
|
||||
src: this.args.imageSrc,
|
||||
util: 'crop',
|
||||
utils: [
|
||||
'crop',
|
||||
'filter',
|
||||
'finetune',
|
||||
'redact'
|
||||
],
|
||||
locale: {
|
||||
labelButtonExport: 'Save and close'
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('loaderror', () => {
|
||||
// TODO: log error message
|
||||
});
|
||||
|
||||
editor.on('process', (result) => {
|
||||
// save edited image
|
||||
this.args.saveImage(result.dest);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
{{#if this.url}}
|
||||
<div class="gh-image-uploader -with-image">
|
||||
<div><img src={{this.url}} alt="" role="presentation"></div>
|
||||
<button type="button" class="image-delete" title="Delete" {{on "click" this.removeImage}}>
|
||||
<button type="button" class="image-action image-delete" title="Delete" {{on "click" this.removeImage}}>
|
||||
{{svg-jar "trash"}}
|
||||
<span class="hidden">Delete</span>
|
||||
</button>
|
||||
|
|
|
@ -75,6 +75,7 @@ export default class FeatureService extends Service {
|
|||
@feature('i18n') i18n;
|
||||
@feature('postHistory') postHistory;
|
||||
@feature('announcementBar') announcementBar;
|
||||
@feature('imageEditor') imageEditor;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
@import "components/stacks.css";
|
||||
@import "components/browser-preview.css";
|
||||
@import "components/filter-builder.css";
|
||||
@import "components/pintura.css";
|
||||
|
||||
|
||||
/* Layouts
|
||||
|
|
118
ghost/admin/app/styles/components/pintura.css
Normal file
118
ghost/admin/app/styles/components/pintura.css
Normal file
|
@ -0,0 +1,118 @@
|
|||
.pintura-editor {
|
||||
--font-family: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Droid Sans, Helvetica Neue, sans-serif !important;
|
||||
}
|
||||
|
||||
.PinturaModal {
|
||||
padding: 32px !important;
|
||||
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
.PinturaRoot {
|
||||
border-radius: 9px !important;
|
||||
}
|
||||
|
||||
.PinturaRoot[data-env~=landscape] > .PinturaNavTools {
|
||||
padding: 24px 24px 0 !important;
|
||||
}
|
||||
|
||||
.PinturaRoot > .PinturaNav .PinturaButton:only-of-type {
|
||||
padding: 0 14px !important;
|
||||
height: 34px !important;
|
||||
font-size: 2rem !important;
|
||||
font-weight: 500 !important;
|
||||
color: var(--white) !important;
|
||||
background-color: var(--black) !important;
|
||||
border-radius: 3px !important;
|
||||
-webkit-font-smoothing: subpixel-antialiased !important;
|
||||
}
|
||||
|
||||
.PinturaRoot > .PinturaNav .PinturaButtonExport .PinturaButtonInner {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.PinturaRoot .PinturaButtonExport:hover {
|
||||
color: var(--black) !important;
|
||||
background-color: var(--white) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Main nav */
|
||||
|
||||
.PinturaRoot[data-env~=landscape] > .PinturaNavMain {
|
||||
left: 24px !important;
|
||||
}
|
||||
|
||||
.PinturaRoot > .PinturaNavMain .PinturaTabList [aria-selected=true] {
|
||||
color: var(--green-d1) !important;
|
||||
}
|
||||
|
||||
.PinturaRoot[data-env~=landscape] > .PinturaNavMain button {
|
||||
flex-direction: row !important;
|
||||
gap: 8px !important;
|
||||
width: 120px !important;
|
||||
height: 32px !important;
|
||||
box-shadow: none !important;
|
||||
background: none !important;
|
||||
backdrop-filter: none !important;
|
||||
}
|
||||
|
||||
.PinturaRoot[data-env~=landscape] > .PinturaNavMain button svg {
|
||||
width: 24px !important;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.PinturaRoot[data-env~=landscape] > .PinturaNavMain button span {
|
||||
margin-top: 0 !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* Cropping */
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle] {
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
background: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator:not([data-shape=edge])::after {
|
||||
inset: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
content: "" !important;
|
||||
border-style: solid !important;
|
||||
border-width: 4px 4px 0 0 !important;
|
||||
display: inline-block !important;
|
||||
height: 16px !important;
|
||||
position: relative !important;
|
||||
transform: rotate(-90deg) !important;
|
||||
width: 16px !important;
|
||||
border-color: rgba(0,0,0,1) !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=tr] {
|
||||
left: -16px !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=tr]::after {
|
||||
transform: rotate(0) !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=bl] {
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=bl]::after {
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=br] {
|
||||
top: -16px !important;
|
||||
left: -16px !important;
|
||||
}
|
||||
|
||||
.PinturaRectManipulator[data-shape~=circle][data-direction=br]::after {
|
||||
transform: rotate(90deg) !important;
|
||||
}
|
|
@ -59,7 +59,7 @@
|
|||
line-height: 0;
|
||||
}
|
||||
|
||||
.image-delete {
|
||||
.image-action {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
|
@ -77,12 +77,20 @@
|
|||
box-shadow: rgba(255, 255, 255, 0.2) 0 0 0 1px;
|
||||
}
|
||||
|
||||
.image-delete svg {
|
||||
.image-edit {
|
||||
margin-right: 34px;
|
||||
}
|
||||
|
||||
.image-action svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.image-edit:hover {
|
||||
background: var(--middarkgrey);
|
||||
}
|
||||
|
||||
.image-delete:hover {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -460,11 +460,11 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
|||
height: 4px;
|
||||
}
|
||||
|
||||
.gh-editor-feature-image .image-delete {
|
||||
.gh-editor-feature-image .image-action {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gh-editor-feature-image:hover .image-delete {
|
||||
.gh-editor-feature-image:hover .image-action {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -332,6 +332,19 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gh-expandable-block">
|
||||
<div class="gh-expandable-header">
|
||||
<div>
|
||||
<h4 class="gh-expandable-title">Image Editor</h4>
|
||||
<p class="gh-expandable-description">
|
||||
Allows publishers to edit images in the lexical editor
|
||||
</p>
|
||||
</div>
|
||||
<div class="for-switch">
|
||||
<GhFeatureFlag @flag="imageEditor" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -19,7 +19,8 @@ module.exports = {
|
|||
'emailAnalytics',
|
||||
'hostSettings',
|
||||
'tenor',
|
||||
'editor'
|
||||
'editor',
|
||||
'pintura'
|
||||
];
|
||||
|
||||
frame.response = {
|
||||
|
|
|
@ -19,7 +19,8 @@ module.exports = function getConfigProperties() {
|
|||
emailAnalytics: config.get('emailAnalytics'),
|
||||
hostSettings: config.get('hostSettings'),
|
||||
tenor: config.get('tenor'),
|
||||
editor: config.get('editor')
|
||||
editor: config.get('editor'),
|
||||
pintura: config.get('pintura')
|
||||
};
|
||||
|
||||
const billingUrl = config.get('hostSettings:billing:enabled') ? config.get('hostSettings:billing:url') : '';
|
||||
|
|
|
@ -41,7 +41,8 @@ const ALPHA_FEATURES = [
|
|||
'stripeAutomaticTax',
|
||||
'makingItRain',
|
||||
'postHistory',
|
||||
'announcementBar'
|
||||
'announcementBar',
|
||||
'imageEditor'
|
||||
];
|
||||
|
||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||
|
|
Loading…
Add table
Reference in a new issue