mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 18:31:57 +03:00
c35cdae491
refs https://github.com/TryGhost/Team/issues/1206 Clicking inside the emoji picker was causing focus to be lost which then deselected the card causing an annoying jump between rendered/edit mode whilst working on the card's content. A secondary issue was the picker sticking around after you intentionally clicked elsewhere in the document to leave edit mode. - before initiating the emoji-button instance, create a container that's appended at the bottom of the document body and that prevents any click events on elements inside the container from bubbling up and causing focus changes. Updated the emoji-button instance to render the picker inside that container - added a call to hide the picker any time the card leaves edit mode
221 lines
6.4 KiB
JavaScript
221 lines
6.4 KiB
JavaScript
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;
|
|
|
|
get isEmpty() {
|
|
return isBlank(this.args.payload.calloutText) && isBlank(this.args.payload.calloutEmoji);
|
|
}
|
|
|
|
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.addEventListener('click', function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
});
|
|
|
|
document.body.appendChild(emojiButtonContainer);
|
|
}
|
|
|
|
this.picker = new EmojiButton({
|
|
position: 'bottom',
|
|
recentsCount: 24,
|
|
showPreview: false,
|
|
initialCategory: 'recents',
|
|
rootElement: emojiButtonContainer
|
|
});
|
|
|
|
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() {
|
|
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);
|
|
}
|
|
}
|