0
Fork 0
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:
Rishabh Garg 2023-04-19 16:27:26 +05:30 committed by GitHub
parent bd04eb3d21
commit 48030c3050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 317 additions and 10 deletions

View file

@ -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

View file

@ -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>

View file

@ -86,4 +86,10 @@ export default class GhEditorFeatureImageComponent extends Component {
setFiles(event.dataTransfer.files);
}
@action
saveImage(setFiles, imageFile) {
this.canDrop = false;
setFiles([imageFile]);
}
}

View file

@ -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>

View 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}}

View 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);
});
}
}
}

View file

@ -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>

View file

@ -75,6 +75,7 @@ export default class FeatureService extends Service {
@feature('i18n') i18n;
@feature('postHistory') postHistory;
@feature('announcementBar') announcementBar;
@feature('imageEditor') imageEditor;
_user = null;

View file

@ -43,6 +43,7 @@
@import "components/stacks.css";
@import "components/browser-preview.css";
@import "components/filter-builder.css";
@import "components/pintura.css";
/* Layouts

View 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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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}}

View file

@ -19,7 +19,8 @@ module.exports = {
'emailAnalytics',
'hostSettings',
'tenor',
'editor'
'editor',
'pintura'
];
frame.response = {

View file

@ -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') : '';

View file

@ -41,7 +41,8 @@ const ALPHA_FEATURES = [
'stripeAutomaticTax',
'makingItRain',
'postHistory',
'announcementBar'
'announcementBar',
'imageEditor'
];
module.exports.GA_KEYS = [...GA_FEATURES];