Ghost/ghost/admin/lib/koenig-editor/addon/components/koenig-card-callout.js

222 lines
6.5 KiB
JavaScript
Raw Normal View History

import * as storage from '../utils/localstorage';
import Browser from 'mobiledoc-kit/utils/browser';
import Component from '@glimmer/component';
import {EmojiButton} from '@joeattardi/emoji-button';
import {action} from '@ember/object';
import {isBlank} from '@ember/utils';
import {run} from '@ember/runloop';
import {inject as service} from '@ember/service';
import {set} from '@ember/object';
import {tracked} from '@glimmer/tracking';
const storageKey = 'gh-kg-callout-emoji';
export default class KoenigCardCalloutComponent extends Component {
@service config;
@service feature;
@service store;
@service membersUtils;
@service ui;
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
2021-11-10 17:45:54 +03:00
get isEmpty() {
return isBlank(this.args.payload.calloutText) && isBlank(this.args.payload.calloutEmoji);
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
2021-11-10 17:45:54 +03:00
}
backgroundColors = [
{name: 'Grey', color: 'grey'},
{name: 'White', color: 'white'},
{name: 'Blue', color: 'blue'},
{name: 'Green', color: 'green'},
{name: 'Yellow', color: 'yellow'},
{name: 'Red', color: 'red'},
{name: 'Pink', color: 'pink'},
{name: 'Purple', color: 'purple'},
{name: 'Brand color', color: 'accent'}
];
latestEmojiUsed = null;
@tracked
isPickerVisible = false;
get selectedBackgroundColor() {
return this.backgroundColors.find(option => option.color === this.args.payload.backgroundColor);
}
get toolbar() {
if (this.args.isEditing) {
return false;
}
return {
items: [{
buttonClass: 'fw4 flex items-center white',
icon: 'koenig/kg-edit',
iconClass: 'fill-white',
title: 'Edit',
text: '',
action: run.bind(this, this.args.editCard)
}]
};
}
get defaultEmoji() {
return this.latestEmojiUsed || storage.get(storageKey) || '💡';
}
constructor() {
super(...arguments);
this.args.registerComponent(this);
const payloadDefaults = {
calloutEmoji: this.defaultEmoji,
calloutText: '',
backgroundColor: 'grey'
};
Object.entries(payloadDefaults).forEach(([key, value]) => {
if (this.args.payload[key] === undefined) {
this._updatePayloadAttr(key, value);
}
});
// Create a container for the emoji picker that will prevent clicks deselecting the card.
// Container element survives beyond this component's lifecycle so it can be re-used
// TODO: if emoji button is re-used elsewhere encapsulate behaviour into a modifier/component
let emojiButtonContainer = document.getElementById('emoji-button-container');
if (!emojiButtonContainer) {
emojiButtonContainer = document.createElement('div');
emojiButtonContainer.id = 'emoji-button-container';
emojiButtonContainer.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
});
document.body.appendChild(emojiButtonContainer);
}
2021-11-16 15:34:36 +03:00
this.picker = new EmojiButton({
position: 'bottom',
recentsCount: 24,
showPreview: false,
initialCategory: 'recents',
rootElement: emojiButtonContainer
2021-11-16 15:34:36 +03:00
});
this.picker.on('emoji', (selection) => {
this.setCalloutEmoji(selection.emoji);
});
this.picker.on('hidden', () => {
this.isPickerVisible = false;
});
}
willDestroy() {
super.willDestroy(...arguments);
this.picker?.destroyPicker();
}
// required for snippet rects to be calculated - editor reaches in to component,
// expecting a non-Glimmer component with a .element property
@action
registerElement(element) {
this.element = element;
}
@action
setCalloutText(text) {
this._updatePayloadAttr('calloutText', text);
}
@action
setCalloutEmoji(emoji) {
// Store in payload
this._updatePayloadAttr('calloutEmoji', emoji);
// Store in component in case the emoji is toggled off and then on
this.latestEmojiUsed = emoji;
// Store in localStorage for the next callout to use the same emoji
storage.set(storageKey, emoji);
}
@action
setBackgroundColor(option) {
this._updatePayloadAttr('backgroundColor', option.color);
}
@action
leaveEditMode() {
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
2021-11-10 17:45:54 +03:00
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);
}
this.picker?.hidePicker();
}
@action
focusElement(selector, event) {
event.preventDefault();
document.querySelector(selector)?.focus();
}
@action
registerEditor(calloutTextEditor) {
let commands = {
'META+ENTER': run.bind(this, this._metaEnter, 'meta'),
'CTRL+ENTER': run.bind(this, this._metaEnter, 'ctrl'),
ENTER: run.bind(this, this.args.addParagraphAfterCard)
};
Object.keys(commands).forEach((str) => {
calloutTextEditor.registerKeyCommand({
str,
run() {
return commands[str](calloutTextEditor, str);
}
});
});
this._calloutTextEditor = calloutTextEditor;
run.scheduleOnce('afterRender', this, this._placeCursorAtEnd);
}
@action
changeEmoji(event) {
this.picker.showPicker(event.target);
this.isPickerVisible = true;
}
@action
toggleEmoji() {
this._updatePayloadAttr('calloutEmoji', this.args.payload.calloutEmoji ? '' : this.defaultEmoji);
}
_metaEnter(modifier) {
if (this.args.isEditing && (modifier === 'meta' || (modifier === 'crtl' && Browser.isWin()))) {
this.args.editCard();
}
}
_placeCursorAtEnd() {
if (!this._calloutTextEditor) {
return;
}
let tailPosition = this._calloutTextEditor.post.tailPosition();
let rangeToSelect = tailPosition.toRange();
this._calloutTextEditor.selectRange(rangeToSelect);
}
_updatePayloadAttr(attr, value) {
let payload = this.args.payload;
set(payload, attr, value);
// update the mobiledoc and stay in edit mode
this.args.saveCard?.(payload, false);
}
}