2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Deleted react-editor experiment

no issue

- cleanup now that the experiments have finished
This commit is contained in:
Kevin Ansfield 2022-09-08 17:35:18 +01:00
parent a440076a12
commit 3ec8b4e9b3
14 changed files with 0 additions and 1775 deletions

View file

@ -1,67 +0,0 @@
<div class="gh-koenig-editor relative w-100 vh-100 overflow-x-hidden overflow-y-auto z-0" {{did-insert this.registerElement}} ...attributes>
{{!-- full height content pane --}}
{{!-- template-lint-disable no-down-event-binding no-invalid-interactive no-passed-in-event-handlers --}}
<div
class="gh-koenig-editor-pane flex flex-column mih-100"
{{on "mousedown" this.trackMousedown}}
{{on "mouseup" this.focusEditor}}
>
<GhEditorFeatureImage
@image={{@featureImage}}
@updateImage={{@setFeatureImage}}
@clearImage={{@clearFeatureImage}}
@alt={{@featureImageAlt}}
@updateAlt={{@setFeatureImageAlt}}
@caption={{@featureImageCaption}}
@updateCaption={{@setFeatureImageCaption}}
@forceButtonDisplay={{or (not @title) (eq @title "(Untitled)") this.titleIsHovered this.titleIsFocused}}
/>
<GhTextarea
@class="gh-editor-title"
@placeholder={{@titlePlaceholder}}
@shouldFocus={{or @titleAutofocus false}}
@tabindex="1"
@autoExpand=".gh-koenig-editor"
@value={{readonly this.title}}
@input={{this.updateTitle}}
@focus-out={{optional @onTitleBlur}}
@keyDown={{this.onTitleKeydown}}
@didCreateTextarea={{this.registerTitleElement}}
{{on "focus" (fn (mut this.titleIsFocused) true)}}
{{on "blur" (fn (mut this.titleIsFocused) false)}}
{{on "mouseover" (fn (mut this.titleIsHovered) true)}}
{{on "mouseleave" (fn (mut this.titleIsHovered) false)}}
data-test-editor-title-input={{true}}
/>
<KoenigReactEditor
@mobiledoc={{@body}}
@didCreateEditor={{this.onEditorCreated}}
@onChange={{@onBodyChange}}
@uploadUrl={{this.uploadUrl}}
@accentColor={{this.accentColor}}
/>
{{!-- <KoenigEditor
@mobiledoc={{@body}}
@placeholder={{@bodyPlaceholder}}
@spellcheck={{true}}
@onChange={{@onBodyChange}}
@didCreateEditor={{this.onEditorCreated}}
@cursorDidExitAtTop={{this.focusTitle}}
@headerOffset={{@headerOffset}}
@dropTargetSelector=".gh-koenig-editor-pane"
@scrollContainerSelector={{@scrollContainerSelector}}
@scrollOffsetTopSelector={{@scrollOffsetTopSelector}}
@scrollOffsetBottomSelector={{@scrollOffsetBottomSelector}}
@wordCountDidChange={{@onWordCountChange}}
@snippets={{@snippets}}
@saveSnippet={{@saveSnippet}}
@updateSnippet={{@updateSnippet}}
@deleteSnippet={{@deleteSnippet}}
@cardOptions={{@cardOptions}}
@postType={{@postType}}
/> --}}
</div>
</div>

View file

@ -1,167 +0,0 @@
import Component from '@glimmer/component';
import ghostPaths from 'ghost-admin/utils/ghost-paths';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class GhKoenigEditorReactComponent extends Component {
@service settings;
containerElement = null;
titleElement = null;
koenigEditor = null;
mousedownY = 0;
uploadUrl = `${ghostPaths().apiRoot}/images/upload/`;
@tracked titleIsHovered = false;
@tracked titleIsFocused = false;
get title() {
return this.args.title === '(Untitled)' ? '' : this.args.title;
}
get accentColor() {
const color = this.settings.get('accentColor');
if (color && color[0] === '#') {
return color.slice(1);
}
return color;
}
@action
registerElement(element) {
this.containerElement = element;
}
@action
trackMousedown(event) {
// triggered when a mousedown is registered on .gh-koenig-editor-pane
this.mousedownY = event.clientY;
}
// Title actions -----------------------------------------------------------
@action
registerTitleElement(element) {
this.titleElement = element;
// this is needed because focus event handler won't be fired if input has focus when rendering
if (this.titleElement === document.activeElement) {
this.titleIsFocused = true;
}
}
@action
updateTitle(event) {
this.args.onTitleChange?.(event.target.value);
}
@action
focusTitle() {
this.titleElement.focus();
}
@action
onTitleKeydown(event) {
let value = event.target.value;
let selectionStart = event.target.selectionStart;
// enter will always focus the editor
// down arrow will only focus the editor when the cursor is at the
// end of the input to preserve the default OS behaviour
if (
event.key === 'Enter' ||
event.key === 'Tab' ||
((event.key === 'ArrowDown' || event.key === 'ArrowRight') && (!value || selectionStart === value.length))
) {
event.preventDefault();
// on Enter we also want to create a blank para if necessary
if (event.key === 'Enter') {
this._addParaAtTop();
}
this.koenigEditor.focus();
}
}
// Body actions ------------------------------------------------------------
@action
onEditorCreated(koenig) {
this._setupEditor(koenig);
this.args.onEditorCreated?.(koenig);
}
@action
focusEditor(event) {
if (event.target.classList.contains('gh-koenig-editor-pane')) {
let editorCanvas = this.koenigEditor.element;
let {bottom} = editorCanvas.getBoundingClientRect();
// if a mousedown and subsequent mouseup occurs below the editor
// canvas, focus the editor and put the cursor at the end of the
// document
if (this.mousedownY > bottom && event.clientY > bottom) {
let {post} = this.koenigEditor;
let range = post.toRange();
let {tailSection} = range;
event.preventDefault();
this.koenigEditor.focus();
// we should always have a visible cursor when focusing
// at the bottom so create an empty paragraph if last
// section is a card
if (tailSection.isCardSection) {
this.koenigEditor.run((postEditor) => {
let newSection = postEditor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
tailSection = newSection;
});
}
this.koenigEditor.selectRange(tailSection.tailPosition());
// ensure we're scrolled to the bottom
this.containerElement.scrollTop = this.containerElement.scrollHeight;
}
}
}
_setupEditor(koenig) {
let component = this;
this.koenigEditor = koenig;
// focus the title when pressing SHIFT+TAB
this.koenigEditor.registerKeyCommand({
str: 'SHIFT+TAB',
run() {
component.focusTitle();
return true;
}
});
}
_addParaAtTop() {
if (!this.koenigEditor) {
return;
}
let editor = this.koenigEditor;
let section = editor.post.toRange().head.section;
// create a blank paragraph at the top of the editor unless it's already
// a blank paragraph
if (section.isListItem || !section.isBlank || section.text !== '') {
editor.run((postEditor) => {
let {builder} = postEditor;
let newPara = builder.createMarkupSection('p');
let sections = section.isListItem ? section.parent.parent.sections : section.parent.sections;
postEditor.insertSectionBefore(sections, newPara, section);
});
}
}
}

View file

@ -1 +0,0 @@
<div {{react-render this.ReactComponent}}></div>

View file

@ -1,98 +0,0 @@
import Component from '@glimmer/component';
import React, {Suspense} from 'react';
class ErrorHandler extends React.Component {
state = {
hasError: false
};
static getDerivedStateFromError() {
return {hasError: true};
}
render() {
if (this.state.hasError) {
return (
<p className="koenig-react-editor-error">Loading has failed. Try refreshing the browser!</p>
);
}
return this.props.children;
}
}
const fetchKoenig = function () {
let status = 'pending';
let response;
const fetchPackage = async () => {
if (window.koenigEditor) {
return window.koenigEditor.default;
}
// the manual specification of the protocol in the import template string is
// required to work around ember-auto-import complaining about an unknown dynamic import
// during the build step
const GhostAdmin = window.Ember.Namespace.NAMESPACES.find(ns => ns.name === 'ghost-admin');
const url = new URL(GhostAdmin.__container__.lookup('service:config').get('editor.url'));
if (url.protocol === 'http:') {
await import(`http://${url.host}${url.pathname}`);
} else {
await import(`https://${url.host}${url.pathname}`);
}
return window.koenigEditor.default;
};
const suspender = fetchPackage().then(
(res) => {
status = 'success';
response = res;
},
(err) => {
status = 'error';
response = err;
}
);
const read = () => {
switch (status) {
case 'pending':
throw suspender;
case 'error':
throw response;
default:
return response;
}
};
return {read};
};
const editorResource = fetchKoenig();
const Koenig = (props) => {
const _Koenig = editorResource.read();
return <_Koenig {...props} />;
};
export default class KoenigReactEditor extends Component {
ReactComponent = () => {
return (
<div className={['koenig-react-editor', this.args.className].filter(Boolean).join(' ')}>
<ErrorHandler>
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
<Koenig
mobiledoc={this.args.mobiledoc}
didCreateEditor={this.args.didCreateEditor}
onChange={this.args.onChange}
uploadUrl={this.args.uploadUrl}
accentColor={this.args.accentColor}
/>
</Suspense>
</ErrorHandler>
</div>
);
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +0,0 @@
import Controller from '@ember/controller';
import {inject as service} from '@ember/service';
export default class ReactEditLoadingController extends Controller {
@service ui;
}

View file

@ -36,11 +36,6 @@ Router.map(function () {
this.route('edit', {path: ':type/:post_id'});
});
this.route('react-editor', function () {
this.route('new', {path: ':type'});
this.route('edit', {path: ':type/:post_id'});
});
this.route('lexical-editor', function () {
this.route('new', {path: ':type'});
this.route('edit', {path: ':type/:post_id'});

View file

@ -1,74 +0,0 @@
import $ from 'jquery';
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
export default AuthenticatedRoute.extend({
config: service(),
feature: service(),
notifications: service(),
router: service(),
ui: service(),
classNames: ['editor'],
beforeModel() {
if (!this.config.get('editor.url')) {
return this.router.transitionTo('posts');
}
},
activate() {
this._super(...arguments);
this.ui.set('isFullScreen', true);
},
deactivate() {
this._super(...arguments);
this.ui.set('isFullScreen', false);
},
actions: {
save() {
this._blurAndScheduleAction(function () {
this.controller.send('save');
});
},
authorizationFailed() {
this.controller.send('toggleReAuthenticateModal');
},
willTransition(transition) {
// exit early if an upgrade is required because our extended route
// class will abort the transition and show an error
if (this.get('upgradeStatus.isRequired')) {
return this._super(...arguments);
}
this.controller.willTransition(transition);
}
},
buildRouteInfoMetadata() {
return {
titleToken: () => {
return this.get('controller.post.title') || 'Editor';
},
bodyClasses: ['gh-body-fullscreen'],
mainClasses: ['gh-main-white']
};
},
_blurAndScheduleAction(func) {
let selectedElement = $(document.activeElement);
// TODO: we should trigger a blur for textareas as well as text inputs
if (selectedElement.is('input[type="text"]')) {
selectedElement.trigger('focusout');
}
// wait for actions triggered by the focusout to finish before saving
run.scheduleOnce('actions', this, func);
}
});

View file

@ -1,67 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import {pluralize} from 'ember-inflector';
export default class EditRoute extends AuthenticatedRoute {
beforeModel(transition) {
super.beforeModel(...arguments);
// if the transition is not new->edit, reset the post on the controller
// so that the editor view is cleared before showing the loading state
if (transition.urlMethod !== 'replace') {
let editor = this.controllerFor('react-editor');
editor.set('post', null);
editor.reset();
}
}
model(params, transition) {
// eslint-disable-next-line camelcase
let {type: modelName, post_id} = params;
if (!['post', 'page'].includes(modelName)) {
let path = transition.intent.url.replace(/^\//, '');
return this.replaceWith('error404', {path, status: 404});
}
let query = {
// eslint-disable-next-line camelcase
id: post_id
};
return this.store.query(modelName, query)
.then(records => records.get('firstObject'));
}
// the API will return a post even if the logged in user doesn't have
// permission to edit it (all posts are public) so we need to do our
// own permissions check and redirect if necessary
afterModel(post) {
super.afterModel(...arguments);
const user = this.session.user;
const returnRoute = pluralize(post.constructor.modelName);
if (user.isAuthorOrContributor && !post.isAuthoredByUser(user)) {
return this.replaceWith(returnRoute);
}
// If the post is not a draft and user is contributor, redirect to index
if (user.isContributor && !post.isDraft) {
return this.replaceWith(returnRoute);
}
}
serialize(model) {
return {
type: model.constructor.modelName,
post_id: model.id
};
}
// there's no specific controller for this route, instead all editor
// handling is done on the editor route/controller
setupController(controller, post) {
let editor = this.controllerFor('react-editor');
editor.setPost(post);
}
}

View file

@ -1,8 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default class IndexRoute extends AuthenticatedRoute {
beforeModel() {
super.beforeModel(...arguments);
this.replaceWith('react-editor.new', 'post');
}
}

View file

@ -1,27 +0,0 @@
import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
export default class NewRoute extends AuthenticatedRoute {
model(params, transition) {
let {type: modelName} = params;
if (!['post','page'].includes(modelName)) {
let path = transition.intent.url.replace(/^\//, '');
return this.replaceWith('error404', {path, status: 404});
}
return this.store.createRecord(modelName, {authors: [this.session.user]});
}
// there's no specific controller for this route, instead all editor
// handling is done on the editor route/controler
setupController(controller, newPost) {
let editor = this.controllerFor('react-editor');
editor.setPost(newPost);
}
buildRouteInfoMetadata() {
return {
mainClasses: ['editor-new']
};
}
}

View file

@ -957,11 +957,6 @@
max-width: 740px;
}
.koenig-react-editor {
max-width: 740px;
margin: 0 auto;
}
/* menu
/* --------------------------------------------------------------- */
.koenig-plus-menu-button {

View file

@ -1,151 +0,0 @@
{{#if this.post}}
<div class="flex flex-row">
<GhEditor
@tagName="section"
@class="gh-editor gh-view relative"
as |editor|
>
<header class="gh-editor-header br2 pe-none">
<Editor::PublishManagement
@post={{this.post}}
@hasUnsavedChanges={{this.hasDirtyAttributes}}
@beforePublish={{perform this.beforeSaveTask}}
@afterPublish={{this.afterSave}}
@saveTask={{this.saveTask}}
as |publishManagement|
>
<div class="flex items-center pe-auto h-100">
{{#if this.ui.isFullScreen}}
<LinkTo @route={{pluralize this.post.displayName }} class="gh-btn-editor gh-editor-back-button" data-test-link={{pluralize this.post.displayName}}>
<span>
{{svg-jar "arrow-left"}}
{{capitalize (pluralize this.post.displayName)}}
</span>
</LinkTo>
{{/if}}
<div class="gh-editor-post-status">
<span>
<GhEditorPostStatus
@post={{this.post}}
@hasDirtyAttributes={{this.hasDirtyAttributes}}
@isSaving={{or this.autosaveTask.isRunning this.saveTasks.isRunning}}
@openUpdateFlow={{publishManagement.openUpdateFlow}}
/>
</span>
</div>
</div>
<section class="flex items-center pe-auto h-100">
{{#unless this.post.isNew}}
<Editor::PublishButtons @publishManagement={{publishManagement}} />
{{#unless this.showSettingsMenu}}
<div class="settings-menu-toggle-spacer"></div>
{{/unless}}
{{/unless}}
</section>
</Editor::PublishManagement>
</header>
{{!--
gh-koenig-editor acts as a wrapper around the title input and
koenig editor canvas to support Ghost-specific editor behaviour
--}}
<GhKoenigEditorReact
@title={{readonly this.post.titleScratch}}
@titleAutofocus={{this.shouldFocusTitle}}
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
@onTitleChange={{this.updateTitleScratch}}
@onTitleBlur={{perform this.saveTitleTask}}
@body={{readonly this.post.scratch}}
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}
@onBodyChange={{this.updateScratch}}
@headerOffset={{editor.headerHeight}}
@scrollContainerSelector=".gh-koenig-editor"
@scrollOffsetBottomSelector=".gh-mobile-nav-bar"
@onEditorCreated={{this.setKoenigEditor}}
@onWordCountChange={{this.updateWordCount}}
@snippets={{this.snippets}}
@saveSnippet={{if this.canManageSnippets this.saveSnippet}}
@updateSnippet={{if this.canManageSnippets this.toggleUpdateSnippetModal}}
@deleteSnippet={{if this.canManageSnippets this.toggleDeleteSnippetModal}}
@featureImage={{this.post.featureImage}}
@featureImageAlt={{this.post.featureImageAlt}}
@featureImageCaption={{this.post.featureImageCaption}}
@setFeatureImage={{this.setFeatureImage}}
@setFeatureImageAlt={{this.setFeatureImageAlt}}
@setFeatureImageCaption={{this.setFeatureImageCaption}}
@clearFeatureImage={{this.clearFeatureImage}}
@cardOptions={{hash
post=this.post
}}
@postType={{this.post.displayName}}
/>
<div class="gh-editor-wordcount-container">
<div class="gh-editor-wordcount">
{{gh-pluralize this.wordCount.wordCount "word"}}
</div>
<a href="https://ghost.org/help/using-the-editor/" class="flex" target="_blank" rel="noopener noreferrer">{{svg-jar "help"}}</a>
</div>
</GhEditor>
{{#if this.showSettingsMenu}}
<GhPostSettingsMenu
@post={{this.post}}
@deletePost={{this.openDeletePostModal}}
@updateSlugTask={{this.updateSlugTask}}
@savePostTask={{this.savePostTask}}
/>
{{/if}}
</div>
<button type="button" class="settings-menu-toggle gh-btn gh-btn-editor gh-btn-icon icon-only gh-btn-action-icon" title="Settings" {{on "click" this.toggleSettingsMenu}} data-test-psm-trigger>
{{#if this.showSettingsMenu}}
<span class="settings-menu-open">{{svg-jar "sidemenu-open"}}</span>
{{else}}
<span>{{svg-jar "sidemenu"}}</span>
{{/if}}
</button>
{{#if this.showReAuthenticateModal}}
<GhFullscreenModal @modal="re-authenticate"
@close={{this.toggleReAuthenticateModal}}
@modifier="action wide" />
{{/if}}
{{#if this.showUpgradeModal}}
<GhFullscreenModal
@modal="upgrade-host-limit"
@model={{hash
message=this.hostLimitError.context
details=this.hostLimitError.details
}}
@close={{this.closeUpgradeModal}}
@modifier="action wide"
/>
{{/if}}
{{#if this.snippetToUpdate}}
<GhFullscreenModal
@modal="update-snippet"
@model={{this.snippetToUpdate}}
@confirm={{this.updateSnippet}}
@close={{this.toggleUpdateSnippetModal}}
@modifier="action wide"
/>
{{/if}}
{{#if this.snippetToDelete}}
<GhFullscreenModal
@modal="delete-snippet"
@model={{this.snippetToDelete}}
@confirm={{this.deleteSnippet}}
@close={{this.toggleDeleteSnippetModal}}
@modifier="action wide"
/>
{{/if}}
{{/if}}
{{outlet}}

View file

@ -233,9 +233,6 @@ module.exports = function (defaults) {
process: 'process/browser'
})
]
},
alias: {
'react-mobiledoc-editor': 'react-mobiledoc-editor/dist/main.js'
}
}
});