Removed duplication of "delete if empty" card logic

no issue

The logic for "delete if empty" was duplicated in two places:

1. when `createComponentCard` is used to register a card, this option method was used when cleaning up a post when first rendering (cards in empty state can be saved before the editor auto-removes them but we don't want to show them again)
2. inside of card's own delete-if-empty handling on certain actions such as deselection or leaving edit mode

- added an `ifEmpty` property to each card component
  - used by the editor's first-render cleanup routing if the property is present
  - can be re-used internally for the card's own deselect/exit-edit-mode behaviour
- updated the cleanup routine in `<KoenigEditor>`
  - added a `allComponentCardsRegistered` property that will return `true` when the `.component` property is set on every card (the property is set during card component initialisation so we're at the mercy of Ember's render process so not all card components will be immediately registered)
  - swapped `_cleanup` for `_cleanupTask` that will wait for `allComponentCardsRegistered` to be `true` before performing cleanup, ensuring that we always have access to the card component's `isEmpty` property even when Ember renders cards across multiple render batches
  - checks for `isEmpty` being a boolean and will delete the card if it's value is `true`
- updated all cards that had delete-if-empty behaviour
  - added `isEmpty` properties
  - removed duplicated logic in the `createComponentCard` calls
This commit is contained in:
Kevin Ansfield 2021-11-10 14:45:54 +00:00
parent d34a1df707
commit 7fcb373bef
12 changed files with 93 additions and 79 deletions

View File

@ -32,6 +32,10 @@ export default Component.extend({
addParagraphAfterCard() {},
registerComponent() {},
isEmpty: computed('payload.metadata', function () {
return isBlank(this.payload.metadata);
}),
counts: computed('payload.{metadata,caption}', function () {
let imgCount = 0;
let wordCount = 0;
@ -66,7 +70,9 @@ export default Component.extend({
if (this.payload.url && !this.payload.metadata && !this.hasError) {
this.convertUrl.perform(this.payload.url);
} else {
this._deleteIfEmpty();
if (this.isEmpty && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);
}
}
},
@ -171,11 +177,5 @@ export default Component.extend({
if (urlInput) {
urlInput.focus();
}
},
_deleteIfEmpty() {
if (isBlank(this.payload.metadata) && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);
}
}
});

View File

@ -18,6 +18,12 @@ export default class KoenigCardButtonComponent extends Component {
@tracked contentFocused = false;
@tracked offers = null;
get isEmpty() {
const {buttonText, buttonUrl} = this.args.payload;
return isBlank(buttonText) && isBlank(buttonUrl);
}
get toolbar() {
if (this.args.isEditing) {
return false;
@ -110,9 +116,7 @@ export default class KoenigCardButtonComponent extends Component {
@action
leaveEditMode() {
const {html, buttonText, buttonUrl} = this.args.payload;
if (isBlank(html) && isBlank(buttonText) && isBlank(buttonUrl)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.args.deleteCard);

View File

@ -14,6 +14,10 @@ export default class KoenigCardCalloutComponent extends Component {
@service membersUtils;
@service ui;
get isEmpty() {
return isBlank(this.args.payload.calloutText);
}
get formattedHtml() {
return formatTextReplacementHtml(this.payload.calloutText);
}
@ -69,9 +73,7 @@ export default class KoenigCardCalloutComponent extends Component {
@action
leaveEditMode() {
const {calloutText} = this.args.payload;
if (isBlank(calloutText)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.args.deleteCard);

View File

@ -36,6 +36,10 @@ export default Component.extend({
moveCursorToPrevSection() {},
addParagraphAfterCard() {},
isEmpty: computed('payload.code', function () {
return isBlank(this.payload.code);
}),
counts: computed('payload.code', function () {
return {wordCount: countWords(this.payload.code)};
}),
@ -111,7 +115,7 @@ export default Component.extend({
leaveEditMode() {
this._removeMousemoveHandler();
if (isBlank(this.payload.code)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.deleteCard);

View File

@ -25,6 +25,12 @@ export default class KoenigCardEmailCtaComponent extends Component {
buttonTextInputId = 'button-text-input-' + guidFor(this);
urlInputId = 'url-input-' + guidFor(this);
get isEmpty() {
const {html, buttonText, buttonUrl} = this.args.payload;
return isBlank(html) && isBlank(buttonText) && isBlank(buttonUrl);
}
get formattedHtml() {
return formatTextReplacementHtml(this.args.payload.html);
}
@ -186,9 +192,7 @@ export default class KoenigCardEmailCtaComponent extends Component {
@action
leaveEditMode() {
const {html, buttonText, buttonUrl} = this.args.payload;
if (isBlank(html) && isBlank(buttonText) && isBlank(buttonUrl)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.args.deleteCard);

View File

@ -23,6 +23,10 @@ export default Component.extend({
addParagraphAfterCard() {},
registerComponent() {},
isEmpty: computed('payload.html', function () {
return isBlank(this.payload.html);
}),
formattedHtml: computed('payload.html', function () {
return formatTextReplacementHtml(this.payload.html);
}),
@ -79,7 +83,7 @@ export default Component.extend({
},
leaveEditMode() {
if (isBlank(this.payload.html)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.deleteCard);

View File

@ -34,6 +34,10 @@ export default Component.extend({
addParagraphAfterCard() {},
registerComponent() {},
isEmpty: computed('payload.html', function () {
return isBlank(this.payload.html);
}),
counts: computed('payload.{html,caption}', function () {
return {
imageCount: this.payload.html ? 1 : 0,
@ -73,7 +77,9 @@ export default Component.extend({
if (this.payload.url && !this.payload.html && !this.hasError) {
this.convertUrl.perform(this.payload.url);
} else {
this._deleteIfEmpty();
if (this.isEmpty && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);
}
}
},
@ -332,11 +338,5 @@ export default Component.extend({
window.removeEventListener('resize', this._windowResizeHandler);
this._windowResizeHandler = run.bind(this, this._resizeIframe, iframe);
window.addEventListener('resize', this._windowResizeHandler, {passive: true});
},
_deleteIfEmpty() {
if (isBlank(this.payload.html) && !this.convertUrl.isRunning && !this.hasError) {
this.deleteCard(NO_CURSOR_MOVEMENT);
}
}
});

View File

@ -22,6 +22,10 @@ export default Component.extend({
deleteCard() {},
registerComponent() {},
isEmpty: computed('payload.html', function () {
return isBlank(this.payload.html);
}),
counts: computed('payload.html', function () {
return {
wordCount: countWords(this.payload.html),
@ -66,7 +70,7 @@ export default Component.extend({
},
leaveEditMode() {
if (isBlank(this.payload.html)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.deleteCard);

View File

@ -39,6 +39,10 @@ export default Component.extend({
addParagraphAfterCard() {},
registerComponent() {},
isEmpty: computed('payload.{imageSelector,src}', function () {
return !this.payload.imageSelector && !this.payload.src;
}),
imageSelector: computed('payload.imageSelector', function () {
let selector = this.payload.imageSelector;
let imageSelectors = {

View File

@ -30,6 +30,10 @@ export default Component.extend({
deleteCard() {},
registerComponent() {},
isEmpty: computed('payload.markdown', function () {
return isBlank(this.payload.markdown);
}),
counts: computed('renderedMarkdown', function () {
return {
wordCount: countWords(this.renderedMarkdown),
@ -83,7 +87,7 @@ export default Component.extend({
},
leaveEditMode() {
if (isBlank(this.payload.markdown)) {
if (this.isEmpty) {
// afterRender is required to avoid double modification of `isSelected`
// TODO: see if there's a way to avoid afterRender
run.scheduleOnce('afterRender', this, this.deleteCard);

View File

@ -29,10 +29,10 @@ import {getOwner} from '@ember/application';
import {getParent} from '../lib/dnd/utils';
import {utils as ghostHelperUtils} from '@tryghost/helpers';
import {guidFor} from '@ember/object/internals';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {svgJar} from 'ghost-admin/helpers/svg-jar';
import {task, waitForProperty} from 'ember-concurrency';
const {countWords} = ghostHelperUtils;
const UNDO_DEPTH = 100;
@ -249,6 +249,10 @@ export default Component.extend({
return this.saveSnippet ? this.saveCardAsSnippet : undefined;
}),
allComponentCardsRegistered: computed('componentCards.@each.component', function () {
return this.componentCards.every(card => typeof card.component === 'object');
}),
/* lifecycle hooks ------------------------------------------------------ */
init() {
@ -449,7 +453,7 @@ export default Component.extend({
}
if (this._cleanupScheduled) {
run.schedule('afterRender', this, this._cleanup);
run.schedule('afterRender', this, this._cleanupTask.perform);
}
});
@ -777,38 +781,10 @@ export default Component.extend({
this._skipNextNewline = true;
},
// HACK: this scheduled cleanup is a bit hacky. We call .cleanup when
// initializing Koenig in our editor controller but we have to wait for
// rendering to finish so that componentCards is populated, even then
// it's unlikely the card.component registration has finished.
//
// TODO: see if there's a way we can perform cleanup directly on the
// mobiledoc, maybe with a "cleanupOnInit" option so that we modify the
// mobiledoc before we start rendering
cleanup() {
this._cleanupScheduled = true;
},
_cleanup() {
this.componentCards.forEach((card) => {
let shouldDelete = card.koenigOptions.deleteIfEmpty;
if (!shouldDelete) {
return;
}
if (typeof shouldDelete === 'string') {
let payloadKey = shouldDelete;
shouldDelete = cardToDelete => isBlank(get(cardToDelete, payloadKey));
}
if (shouldDelete(card)) {
this.deleteCard(card, NO_CURSOR_MOVEMENT);
}
});
this._cleanupScheduled = false;
},
/* mobiledoc event handlers --------------------------------------------- */
postDidChange(editor) {
@ -1261,8 +1237,14 @@ export default Component.extend({
},
deleteCard(card, cursorDirection) {
let section = card.env.postModel;
if (!section.parent) {
// card has already been deleted, skip
return;
}
this.editor.run((postEditor) => {
let section = card.env.postModel;
let nextPosition;
if (cursorDirection === CURSOR_BEFORE) {
@ -1342,6 +1324,18 @@ export default Component.extend({
/* internal methods ----------------------------------------------------- */
_cleanupTask: task(function* () {
yield waitForProperty(this, 'allComponentCardsRegistered');
this.componentCards.forEach((card) => {
if (typeof card.component.isEmpty === 'boolean' && card.component.isEmpty) {
this.deleteCard(card, NO_CURSOR_MOVEMENT);
}
});
this._cleanupScheduled = false;
}),
// nested editor.run loops will create additional undo steps so this is a
// shortcut for when we already have a postEditor
_performEdit(editOperation, postEditor) {

View File

@ -44,30 +44,20 @@ export const CARD_ICON_MAP = {
// themselves so that they are available on card.component
export default [
createComponentCard('card-markdown'), // backwards-compat with markdown editor
createComponentCard('code', {deleteIfEmpty: 'payload.code'}),
createComponentCard('embed', {hasEditMode: false, deleteIfEmpty: 'payload.html'}),
createComponentCard('bookmark', {hasEditMode: false, deleteIfEmpty: 'payload.metadata'}),
createComponentCard('code'),
createComponentCard('embed', {hasEditMode: false}),
createComponentCard('bookmark', {hasEditMode: false}),
createComponentCard('hr', {hasEditMode: false, selectAfterInsert: false}),
createComponentCard('html', {deleteIfEmpty: 'payload.html'}),
createComponentCard('image', {hasEditMode: false, deleteIfEmpty(card) {
return card.payload.imageSelector && !card.payload.src;
}}),
createComponentCard('markdown', {deleteIfEmpty: 'payload.markdown'}),
createComponentCard('html'),
createComponentCard('image', {hasEditMode: false}),
createComponentCard('markdown'),
createComponentCard('gallery', {hasEditMode: false}),
createComponentCard('email', {deleteIfEmpty: 'payload.html'}),
createComponentCard('email-cta', {deleteIfEmpty(card) {
return !card.payload.html && !card.payload.buttonText && !card.payload.buttonUrl;
}}),
createComponentCard('button', {deleteIfEmpty(card) {
return !card.payload.buttonText && !card.payload.buttonUrl;
}}),
createComponentCard('callout', {deleteIfEmpty(card) {
return !card.payload.calloutText;
}}),
createComponentCard('email'),
createComponentCard('email-cta'),
createComponentCard('button'),
createComponentCard('callout'),
createComponentCard('nft', {hasEditMode: false}),
createComponentCard('toggle', {deleteIfEmpty(card) {
return !card.payload.header && !card.payload.content;
}}),
createComponentCard('toggle'),
createComponentCard('paywall', {hasEditMode: false, selectAfterInsert: false})
];