Ghost/ghost/admin/app/components/gh-koenig-editor.js
Simon Backx 7c3d75ade7
🐛 Fixed pasting newlines in post titles (#15794)
fixes https://github.com/TryGhost/Team/issues/2193

- When pasting a title with a newline, we now trim the string and clear newslines before pasting.
- When sending the slug to the backend to generate a unique slug, we now sluggify it in the frontend before adding it to the URL to prevent issues with unsupported characters (causing possible routing problems in Pro).
2022-11-10 14:43:15 +01:00

169 lines
5.1 KiB
JavaScript

import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class GhKoenigEditorComponent extends Component {
containerElement = null;
titleElement = null;
koenigEditor = null;
mousedownY = 0;
@tracked titleIsHovered = false;
@tracked titleIsFocused = false;
get title() {
return this.args.title === '(Untitled)' ? '' : this.args.title;
}
@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
cleanPastedTitle(event) {
const pastedText = (event.clipboardData || window.clipboardData).getData('text');
if (!pastedText) {
return;
}
event.preventDefault();
const cleanValue = pastedText.replace(/(\n|\r)+/g, ' ').trim();
document.execCommand('insertText', false, cleanValue);
}
@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.editor;
// 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);
});
}
}
}