diff --git a/ghost/admin/.lint-todo b/ghost/admin/.lint-todo index 920a6bc85f..a360d0e4c4 100644 --- a/ghost/admin/.lint-todo +++ b/ghost/admin/.lint-todo @@ -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 diff --git a/ghost/admin/app/components/gh-editor-feature-image.hbs b/ghost/admin/app/components/gh-editor-feature-image.hbs index 9379c749d6..6330f05223 100644 --- a/ghost/admin/app/components/gh-editor-feature-image.hbs +++ b/ghost/admin/app/components/gh-editor-feature-image.hbs @@ -39,7 +39,13 @@
-
diff --git a/ghost/admin/app/components/gh-editor-feature-image.js b/ghost/admin/app/components/gh-editor-feature-image.js index 36ba9662eb..fc27049764 100644 --- a/ghost/admin/app/components/gh-editor-feature-image.js +++ b/ghost/admin/app/components/gh-editor-feature-image.js @@ -86,4 +86,10 @@ export default class GhEditorFeatureImageComponent extends Component { setFiles(event.dataTransfer.files); } + + @action + saveImage(setFiles, imageFile) { + this.canDrop = false; + setFiles([imageFile]); + } } diff --git a/ghost/admin/app/components/gh-image-uploader-with-preview.hbs b/ghost/admin/app/components/gh-image-uploader-with-preview.hbs index 2a5c198e0e..667a0cb90f 100644 --- a/ghost/admin/app/components/gh-image-uploader-with-preview.hbs +++ b/ghost/admin/app/components/gh-image-uploader-with-preview.hbs @@ -1,7 +1,7 @@ {{#if @image}}
- + {{svg-jar "trash"}} diff --git a/ghost/admin/app/components/koenig-image-editor.hbs b/ghost/admin/app/components/koenig-image-editor.hbs new file mode 100644 index 0000000000..2fdecc9eef --- /dev/null +++ b/ghost/admin/app/components/koenig-image-editor.hbs @@ -0,0 +1,5 @@ +{{#if this.isEditorEnabled}} + +{{/if}} diff --git a/ghost/admin/app/components/koenig-image-editor.js b/ghost/admin/app/components/koenig-image-editor.js new file mode 100644 index 0000000000..9daf1aba44 --- /dev/null +++ b/ghost/admin/app/components/koenig-image-editor.js @@ -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); + }); + } + } +} diff --git a/ghost/admin/app/components/settings/staff/modals/upload-image.hbs b/ghost/admin/app/components/settings/staff/modals/upload-image.hbs index fd638d619d..e035a6d766 100644 --- a/ghost/admin/app/components/settings/staff/modals/upload-image.hbs +++ b/ghost/admin/app/components/settings/staff/modals/upload-image.hbs @@ -3,7 +3,7 @@ {{#if this.url}}
- diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index ca05ee2bf1..e0816220fc 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -75,6 +75,7 @@ export default class FeatureService extends Service { @feature('i18n') i18n; @feature('postHistory') postHistory; @feature('announcementBar') announcementBar; + @feature('imageEditor') imageEditor; _user = null; diff --git a/ghost/admin/app/styles/app.css b/ghost/admin/app/styles/app.css index 9788db4582..1d8de82c86 100644 --- a/ghost/admin/app/styles/app.css +++ b/ghost/admin/app/styles/app.css @@ -43,6 +43,7 @@ @import "components/stacks.css"; @import "components/browser-preview.css"; @import "components/filter-builder.css"; +@import "components/pintura.css"; /* Layouts diff --git a/ghost/admin/app/styles/components/pintura.css b/ghost/admin/app/styles/components/pintura.css new file mode 100644 index 0000000000..05db6a76bf --- /dev/null +++ b/ghost/admin/app/styles/components/pintura.css @@ -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; +} diff --git a/ghost/admin/app/styles/components/uploader.css b/ghost/admin/app/styles/components/uploader.css index e9c730fbf4..19d8a32142 100644 --- a/ghost/admin/app/styles/components/uploader.css +++ b/ghost/admin/app/styles/components/uploader.css @@ -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; diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index 4d916fe233..c6b13d7185 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -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; } diff --git a/ghost/admin/app/templates/settings/labs.hbs b/ghost/admin/app/templates/settings/labs.hbs index 4853934115..6a7d8276fa 100644 --- a/ghost/admin/app/templates/settings/labs.hbs +++ b/ghost/admin/app/templates/settings/labs.hbs @@ -332,6 +332,19 @@
+
+
+
+

Image Editor

+

+ Allows publishers to edit images in the lexical editor +

+
+
+ +
+
+
{{/if}} diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/config.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/config.js index fc0f4485b7..05b610b7e5 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/config.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/config.js @@ -19,7 +19,8 @@ module.exports = { 'emailAnalytics', 'hostSettings', 'tenor', - 'editor' + 'editor', + 'pintura' ]; frame.response = { diff --git a/ghost/core/core/server/services/public-config/config.js b/ghost/core/core/server/services/public-config/config.js index d7dd302fe4..e344f5d990 100644 --- a/ghost/core/core/server/services/public-config/config.js +++ b/ghost/core/core/server/services/public-config/config.js @@ -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') : ''; diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 897998d58f..3853230fc4 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -41,7 +41,8 @@ const ALPHA_FEATURES = [ 'stripeAutomaticTax', 'makingItRain', 'postHistory', - 'announcementBar' + 'announcementBar', + 'imageEditor' ]; module.exports.GA_KEYS = [...GA_FEATURES];