mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
✨ Added ability to update snippet contents (#2073)
refs https://github.com/TryGhost/Team/issues/1008 To update a snippet, select the content in the editor that you want as your snippet text and click the snippet icon as per creating a snippet. Once the snippet name input shows, start typing the name of an existing snippet to be able to select it for update. - replaced main snippet input component with the labs component - removed the feature flag and associated labs screen toggle - removed original/labs snippet input conditional in the editor
This commit is contained in:
parent
f2883c3636
commit
4e30da6c70
8 changed files with 74 additions and 299 deletions
|
@ -59,7 +59,6 @@ export default Service.extend({
|
||||||
oauthLogin: feature('oauthLogin', {developer: true}),
|
oauthLogin: feature('oauthLogin', {developer: true}),
|
||||||
emailOnlyPosts: feature('emailOnlyPosts', {developer: true}),
|
emailOnlyPosts: feature('emailOnlyPosts', {developer: true}),
|
||||||
dashboardTwo: feature('dashboardTwo', {developer: true}),
|
dashboardTwo: feature('dashboardTwo', {developer: true}),
|
||||||
snippetReplacements: feature('snippetReplacements', {developer: true}),
|
|
||||||
|
|
||||||
_user: null,
|
_user: null,
|
||||||
|
|
||||||
|
|
|
@ -309,19 +309,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gh-expandable-block">
|
|
||||||
<div class="gh-expandable-header">
|
|
||||||
<div>
|
|
||||||
<h4 class="gh-expandable-title">Snippet replacements</h4>
|
|
||||||
<p class="gh-expandable-description">
|
|
||||||
When creating a snippet, allow for replacing an existing snippet.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="for-switch">
|
|
||||||
<GhFeatureFlag @flag="snippetReplacements" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -37,8 +37,7 @@
|
||||||
|
|
||||||
{{!-- pop-up snippet editing toolbar --}}
|
{{!-- pop-up snippet editing toolbar --}}
|
||||||
{{#if this.snippetRange}}
|
{{#if this.snippetRange}}
|
||||||
{{#if (feature "snippetReplacements")}}
|
<KoenigSnippetInput
|
||||||
<KoenigSnippetInputLabs
|
|
||||||
@editor={{this.editor}}
|
@editor={{this.editor}}
|
||||||
@snippetRange={{this.snippetRange}}
|
@snippetRange={{this.snippetRange}}
|
||||||
@snippetRect={{this.snippetRect}}
|
@snippetRect={{this.snippetRect}}
|
||||||
|
@ -47,15 +46,6 @@
|
||||||
@update={{@updateSnippet}}
|
@update={{@updateSnippet}}
|
||||||
@cancel={{this.cancelAddSnippet}}
|
@cancel={{this.cancelAddSnippet}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
|
||||||
<KoenigSnippetInput
|
|
||||||
@editor={{this.editor}}
|
|
||||||
@snippetRange={{this.snippetRange}}
|
|
||||||
@snippetRect={{this.snippetRect}}
|
|
||||||
@save={{@saveSnippet}}
|
|
||||||
@cancel={{this.cancelAddSnippet}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{!-- (+) icon and pop-up menu --}}
|
{{!-- (+) icon and pop-up menu --}}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<div class="kg-input-bar absolute z-999" style={{this.style}} {{did-insert this.registerAndPositionElement}} ...attributes>
|
|
||||||
<GhInputWithSelect
|
|
||||||
@triggerClass="kg-link-input pa2 pr6 mih-100 ba br3 shadow-2 f8 lh-heading tracked-2 outline-0 h10 nudge-top--8 vertical"
|
|
||||||
@dropdownClass="gh-snippet-dropdown"
|
|
||||||
@placeholder="Snippet name"
|
|
||||||
@options={{@snippets}}
|
|
||||||
@showCreate={{true}}
|
|
||||||
@searchEnabled={{false}}
|
|
||||||
@searchField="name"
|
|
||||||
@onChange={{this.selectSnippet}}
|
|
||||||
@renderInPlace={{false}}
|
|
||||||
@autofocus={{true}}
|
|
||||||
@onInput={{this.nameInput}}
|
|
||||||
as |snippet|
|
|
||||||
>
|
|
||||||
{{#if snippet.__isSuggestion__}}
|
|
||||||
{{snippet}}
|
|
||||||
{{else}}
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
{{snippet.name}}
|
|
||||||
<span class="fill-darkgrey dib w4 h4">{{svg-jar "sync"}}</span>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</GhInputWithSelect>
|
|
||||||
</div>
|
|
|
@ -1,222 +0,0 @@
|
||||||
import Component from '@glimmer/component';
|
|
||||||
import getScrollParent from '../utils/get-scroll-parent';
|
|
||||||
import {TOOLBAR_MARGIN} from './koenig-toolbar';
|
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {guidFor} from '@ember/object/internals';
|
|
||||||
import {htmlSafe} from '@ember/template';
|
|
||||||
import {run} from '@ember/runloop';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
import {tracked} from '@glimmer/tracking';
|
|
||||||
|
|
||||||
// pixels that should be added to the `left` property of the tick adjustment styles
|
|
||||||
// TODO: handle via CSS?
|
|
||||||
const TICK_ADJUSTMENT = 8;
|
|
||||||
|
|
||||||
export default class KoenigSnippetInputLabsComponent extends Component {
|
|
||||||
@service koenigUi;
|
|
||||||
|
|
||||||
@tracked name = '';
|
|
||||||
@tracked style = htmlSafe('');
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
|
|
||||||
// hide any other toolbars
|
|
||||||
this.koenigUi.inputHasFocus = true;
|
|
||||||
|
|
||||||
// record the range now because the property is bound and will update
|
|
||||||
// when the selection changes
|
|
||||||
this._snippetRange = this.args.snippetRange;
|
|
||||||
|
|
||||||
// grab a window range so that we can use getBoundingClientRect. Using
|
|
||||||
// document.createRange is more efficient than doing editor.setRange
|
|
||||||
// because it doesn't trigger all of the selection changing side-effects
|
|
||||||
// TODO: extract MobiledocRange->NativeRange into a util
|
|
||||||
let editor = this.args.editor;
|
|
||||||
let cursor = editor.cursor;
|
|
||||||
let {head, tail} = this.args.snippetRange;
|
|
||||||
let {node: headNode, offset: headOffset} = cursor._findNodeForPosition(head);
|
|
||||||
let {node: tailNode, offset: tailOffset} = cursor._findNodeForPosition(tail);
|
|
||||||
let range = document.createRange();
|
|
||||||
range.setStart(headNode, headOffset);
|
|
||||||
range.setEnd(tailNode, tailOffset);
|
|
||||||
this._windowRange = range;
|
|
||||||
|
|
||||||
// watch the window for mousedown events so that we can close the menu
|
|
||||||
// when we detect a click outside
|
|
||||||
this._onMousedownHandler = run.bind(this, this._handleMousedown);
|
|
||||||
window.addEventListener('mousedown', this._onMousedownHandler);
|
|
||||||
|
|
||||||
// watch for keydown events so that we can close the menu on Escape
|
|
||||||
this._onKeydownHandler = run.bind(this, this._handleKeydown);
|
|
||||||
window.addEventListener('keydown', this._onKeydownHandler);
|
|
||||||
|
|
||||||
this.scrollParent = getScrollParent(editor.element);
|
|
||||||
this.scrollTop = this.scrollParent.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
get snippetMobiledoc() {
|
|
||||||
let {snippetRange, editor} = this.args;
|
|
||||||
return editor.serializePost(editor.post.trimTo(snippetRange), 'mobiledoc');
|
|
||||||
}
|
|
||||||
|
|
||||||
willDestroy() {
|
|
||||||
super.willDestroy?.(...arguments);
|
|
||||||
this.koenigUi.inputHasFocus = false;
|
|
||||||
window.removeEventListener('mousedown', this._onMousedownHandler);
|
|
||||||
window.removeEventListener('keydown', this._onKeydownHandler);
|
|
||||||
this._removeStyleElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectSnippet(snippetName) {
|
|
||||||
const snippetNameLC = snippetName.trim().toLowerCase();
|
|
||||||
const existingSnippet = this.args.snippets.find(snippet => snippet.name.toLowerCase() === snippetNameLC);
|
|
||||||
|
|
||||||
if (existingSnippet) {
|
|
||||||
this.replaceSnippet(existingSnippet);
|
|
||||||
} else {
|
|
||||||
this.createSnippet(snippetName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createSnippet(name) {
|
|
||||||
this.args.save({
|
|
||||||
name,
|
|
||||||
mobiledoc: this.snippetMobiledoc
|
|
||||||
}).then(() => {
|
|
||||||
this.args.cancel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
replaceSnippet(snippet) {
|
|
||||||
this.args.update(
|
|
||||||
snippet,
|
|
||||||
{mobiledoc: this.snippetMobiledoc}
|
|
||||||
);
|
|
||||||
|
|
||||||
// close the snippet input
|
|
||||||
this.args.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
nameKeydown(event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
// prevent Enter from triggering in the editor and removing text
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// convert selection into a mobiledoc document
|
|
||||||
let {snippetRange, editor} = this.args;
|
|
||||||
let mobiledoc = editor.serializePost(editor.post.trimTo(snippetRange), 'mobiledoc');
|
|
||||||
|
|
||||||
this.args.save({
|
|
||||||
name: event.target.value,
|
|
||||||
mobiledoc
|
|
||||||
}).then(() => {
|
|
||||||
this.args.cancel();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
nameInput(name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: largely shared with {{koenig-toolbar}} and {{koenig-link-input}} - extract to a shared util?
|
|
||||||
@action
|
|
||||||
registerAndPositionElement(element) {
|
|
||||||
this.scrollParent.scrollTop = this.scrollTop;
|
|
||||||
|
|
||||||
element.id = guidFor(element);
|
|
||||||
this.element = element;
|
|
||||||
|
|
||||||
let containerRect = this.element.offsetParent.getBoundingClientRect();
|
|
||||||
let rangeRect = this.args.snippetRect || this._windowRange.getBoundingClientRect();
|
|
||||||
let {width, height} = this.element.getBoundingClientRect();
|
|
||||||
let newPosition = {};
|
|
||||||
|
|
||||||
// rangeRect is relative to the viewport so we need to subtract the
|
|
||||||
// container measurements to get a position relative to the container
|
|
||||||
newPosition = {
|
|
||||||
top: rangeRect.top - containerRect.top - height - TOOLBAR_MARGIN,
|
|
||||||
left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2,
|
|
||||||
right: null
|
|
||||||
};
|
|
||||||
|
|
||||||
let tickPosition = 50;
|
|
||||||
// don't overflow left boundary
|
|
||||||
if (newPosition.left < 0) {
|
|
||||||
newPosition.left = 0;
|
|
||||||
|
|
||||||
// calculate the tick percentage position
|
|
||||||
let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2;
|
|
||||||
tickPosition = absTickPosition / width * 100;
|
|
||||||
if (tickPosition < 5) {
|
|
||||||
tickPosition = 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// same for right boundary
|
|
||||||
if (newPosition.left + width > containerRect.width) {
|
|
||||||
newPosition.left = null;
|
|
||||||
newPosition.right = 0;
|
|
||||||
|
|
||||||
// calculate the tick percentage position
|
|
||||||
let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2;
|
|
||||||
tickPosition = 100 + absTickPosition / width * 100;
|
|
||||||
if (tickPosition > 95) {
|
|
||||||
tickPosition = 95;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the tick is a pseudo-element so we the only way we can affect it's
|
|
||||||
// style is by adding a style element to the head
|
|
||||||
this._removeStyleElement(); // reset to base styles
|
|
||||||
if (tickPosition !== 50) {
|
|
||||||
this._addStyleElement(`left: calc(${tickPosition}% - ${TICK_ADJUSTMENT}px)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the toolbar position
|
|
||||||
this.style = htmlSafe(Object.keys(newPosition).map((style) => {
|
|
||||||
if (newPosition[style] !== null) {
|
|
||||||
return `${style}: ${newPosition[style]}px`;
|
|
||||||
}
|
|
||||||
}).compact().join('; '));
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleMousedown(event) {
|
|
||||||
const isOutsideElement = this.element && !event.target.closest(this.element.id);
|
|
||||||
const isOutsideDropdown = !event.target.closest('.ember-basic-dropdown-content');
|
|
||||||
|
|
||||||
if (isOutsideElement && isOutsideDropdown) {
|
|
||||||
this.args.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleKeydown(event) {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
this._cancelAndReselect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cancelAndReselect() {
|
|
||||||
this.args.cancel();
|
|
||||||
if (this._snippetRange) {
|
|
||||||
this.args.editor.selectRange(this._snippetRange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_addStyleElement(styles) {
|
|
||||||
let styleElement = document.createElement('style');
|
|
||||||
styleElement.id = `${this.element.id}-style`;
|
|
||||||
styleElement.innerHTML = `#${this.element.id}:before, #${this.element.id}:after { ${styles} }`;
|
|
||||||
document.head.appendChild(styleElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeStyleElement() {
|
|
||||||
let styleElement = document.querySelector(`#${this.element.id}-style`);
|
|
||||||
if (styleElement) {
|
|
||||||
styleElement.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,25 @@
|
||||||
<div class="kg-input-bar absolute z-999" style={{this.style}} {{did-insert this.registerAndPositionElement}} ...attributes>
|
<div class="kg-input-bar absolute z-999" style={{this.style}} {{did-insert this.registerAndPositionElement}} ...attributes>
|
||||||
<input
|
<GhInputWithSelect
|
||||||
placeholder="Snippet name"
|
@triggerClass="kg-link-input pa2 pr6 mih-100 ba br3 shadow-2 f8 lh-heading tracked-2 outline-0 h10 nudge-top--8 vertical"
|
||||||
value={{this.name}}
|
@dropdownClass="gh-snippet-dropdown"
|
||||||
class="kg-link-input pa2 pr6 mih-100 ba br3 shadow-2 f8 lh-heading tracked-2 outline-0 b--green h10 nudge-top--8"
|
@placeholder="Snippet name"
|
||||||
{{on "input" this.nameInput}}
|
@options={{@snippets}}
|
||||||
{{on "keydown" this.nameKeydown}}
|
@showCreate={{true}}
|
||||||
{{did-insert this.focusInput}}
|
@searchEnabled={{false}}
|
||||||
/>
|
@searchField="name"
|
||||||
|
@onChange={{this.selectSnippet}}
|
||||||
|
@renderInPlace={{false}}
|
||||||
|
@autofocus={{true}}
|
||||||
|
@onInput={{this.nameInput}}
|
||||||
|
as |snippet|
|
||||||
|
>
|
||||||
|
{{#if snippet.__isSuggestion__}}
|
||||||
|
{{snippet}}
|
||||||
|
{{else}}
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
{{snippet.name}}
|
||||||
|
<span class="fill-darkgrey dib w4 h4">{{svg-jar "sync"}}</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</GhInputWithSelect>
|
||||||
</div>
|
</div>
|
|
@ -50,6 +50,14 @@ export default class KoenigSnippetInputComponent extends Component {
|
||||||
// watch for keydown events so that we can close the menu on Escape
|
// watch for keydown events so that we can close the menu on Escape
|
||||||
this._onKeydownHandler = run.bind(this, this._handleKeydown);
|
this._onKeydownHandler = run.bind(this, this._handleKeydown);
|
||||||
window.addEventListener('keydown', this._onKeydownHandler);
|
window.addEventListener('keydown', this._onKeydownHandler);
|
||||||
|
|
||||||
|
this.scrollParent = getScrollParent(editor.element);
|
||||||
|
this.scrollTop = this.scrollParent.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
get snippetMobiledoc() {
|
||||||
|
let {snippetRange, editor} = this.args;
|
||||||
|
return editor.serializePost(editor.post.trimTo(snippetRange), 'mobiledoc');
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
|
@ -61,15 +69,34 @@ export default class KoenigSnippetInputComponent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
focusInput(element) {
|
selectSnippet(snippetName) {
|
||||||
let scrollParent = getScrollParent(element);
|
const snippetNameLC = snippetName.trim().toLowerCase();
|
||||||
let scrollTop = scrollParent.scrollTop;
|
const existingSnippet = this.args.snippets.find(snippet => snippet.name.toLowerCase() === snippetNameLC);
|
||||||
|
|
||||||
element.focus();
|
if (existingSnippet) {
|
||||||
|
this.replaceSnippet(existingSnippet);
|
||||||
|
} else {
|
||||||
|
this.createSnippet(snippetName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reset the scroll position to avoid jumps
|
createSnippet(name) {
|
||||||
// TODO: why does the input focus cause a scroll to the bottom of the doc?
|
this.args.save({
|
||||||
scrollParent.scrollTop = scrollTop;
|
name,
|
||||||
|
mobiledoc: this.snippetMobiledoc
|
||||||
|
}).then(() => {
|
||||||
|
this.args.cancel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceSnippet(snippet) {
|
||||||
|
this.args.update(
|
||||||
|
snippet,
|
||||||
|
{mobiledoc: this.snippetMobiledoc}
|
||||||
|
);
|
||||||
|
|
||||||
|
// close the snippet input
|
||||||
|
this.args.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -92,13 +119,15 @@ export default class KoenigSnippetInputComponent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
nameInput(event) {
|
nameInput(name) {
|
||||||
this.name = event.target.value;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: largely shared with {{koenig-toolbar}} and {{koenig-link-input}} - extract to a shared util?
|
// TODO: largely shared with {{koenig-toolbar}} and {{koenig-link-input}} - extract to a shared util?
|
||||||
@action
|
@action
|
||||||
registerAndPositionElement(element) {
|
registerAndPositionElement(element) {
|
||||||
|
this.scrollParent.scrollTop = this.scrollTop;
|
||||||
|
|
||||||
element.id = guidFor(element);
|
element.id = guidFor(element);
|
||||||
this.element = element;
|
this.element = element;
|
||||||
|
|
||||||
|
@ -156,7 +185,10 @@ export default class KoenigSnippetInputComponent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMousedown(event) {
|
_handleMousedown(event) {
|
||||||
if (this.element && !event.target.closest(this.element.id)) {
|
const isOutsideElement = this.element && !event.target.closest(this.element.id);
|
||||||
|
const isOutsideDropdown = !event.target.closest('.ember-basic-dropdown-content');
|
||||||
|
|
||||||
|
if (isOutsideElement && isOutsideDropdown) {
|
||||||
this.args.cancel();
|
this.args.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export {default} from 'koenig-editor/components/koenig-snippet-input-labs';
|
|
Loading…
Add table
Reference in a new issue