UNPKG

@atlaskit/editor-plugin-emoji

Version:

Emoji plugin for @atlaskit/editor-core

303 lines (297 loc) 18.6 kB
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator"; import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; import _regeneratorRuntime from "@babel/runtime/regenerator"; import isEqual from 'lodash/isEqual'; import { isSSR } from '@atlaskit/editor-common/core-utils'; import { messages, EmojiSharedCssClassName, defaultEmojiHeight } from '@atlaskit/editor-common/emoji'; import { logException } from '@atlaskit/editor-common/monitoring'; import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity'; import { DOMSerializer } from '@atlaskit/editor-prosemirror/model'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { emojiToDom } from './emojiNodeSpec'; /** * Check if we can nicely fallback to the nodes text * * @param fallbackText string of the nodes fallback text * * @example * isSingleEmoji('😀') // true */ export function isSingleEmoji(fallbackText) { // Regular expression to match a single emoji character var emojiRegex = // @ts-ignore - TS1501 TypeScript 5.9.2 upgrade /^((?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5-\uDED8\uDEDC-\uDEDF\uDEEB\uDEEC\uDEF4-\uDEFC\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDDFF\uDE70-\uDE7C\uDE80-\uDE8A\uDE8E-\uDEC6\uDEC8\uDECD-\uDEDC\uDEDF-\uDEEA\uDEEF-\uDEF8])|(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDC2C-\uDC2F\uDC94-\uDC9F\uDCAF\uDCB0\uDCC0\uDCCF\uDCD0\uDCF6-\uDCFF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAE-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDE5F\uDE66-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFA]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDEE5\uDEE9\uDEEB-\uDEF0\uDEF3-\uDEFF\uDFDA-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE\uDCAF\uDCBC-\uDCBF\uDCC2-\uDCCF\uDCD9-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDDFF\uDE58-\uDE5F\uDE6E-\uDEFF]|\uD83F[\uDC00-\uDFFD])\uFE0F?(?:\u200D(?:[\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26A7\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDC2C-\uDC2F\uDC94-\uDC9F\uDCAF\uDCB0\uDCC0\uDCCF\uDCD0\uDCF6-\uDCFF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDAE-\uDDE5\uDE01-\uDE0F\uDE1A\uDE2F\uDE32-\uDE3A\uDE3C-\uDE3F\uDE49-\uDE5F\uDE66-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFA]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5-\uDEE5\uDEE9\uDEEB-\uDEF0\uDEF3-\uDEFF\uDFDA-\uDFFF]|\uD83E[\uDC0C-\uDC0F\uDC48-\uDC4F\uDC5A-\uDC5F\uDC88-\uDC8F\uDCAE\uDCAF\uDCBC-\uDCBF\uDCC2-\uDCCF\uDCD9-\uDCFF\uDD0C-\uDD3A\uDD3C-\uDD45\uDD47-\uDDFF\uDE58-\uDE5F\uDE6E-\uDEFF]|\uD83F[\uDC00-\uDFFD])\uFE0F?)+|(?:\uD83C[\uDDE6-\uDDFF])(?:\uD83C[\uDDE6-\uDDFF]))$/; return emojiRegex.test(fallbackText); } /** * Emoji node view for renderering emoji nodes */ export var EmojiNodeView = /*#__PURE__*/function () { /** * Prosemirror node view for rendering emoji nodes. This class is responsible for * rendering emoji nodes in the editor, handling updates, and managing fallback rendering. * * @param node - The ProseMirror node representing the emoji. * @param extraProps - An object containing additional parameters. * @param extraProps.intl - The internationalization object for formatting messages. * @param extraProps.api - The editor API for accessing shared state and connectivity features. * @param extraProps.emojiNodeDataProvider - (Optional) A provider for fetching emoji data. * * @example * const emojiNodeView = new EmojiNodeView(node, { intl, api, emojiNodeDataProvider }); */ function EmojiNodeView(node, _ref) { var _this = this; var intl = _ref.intl, api = _ref.api, emojiNodeDataProvider = _ref.emojiNodeDataProvider; _classCallCheck(this, EmojiNodeView); _defineProperty(this, "renderingFallback", false); _defineProperty(this, "destroy", function () {}); this.node = node; this.intl = intl; var _DOMSerializer$render = DOMSerializer.renderSpec(document, emojiToDom(this.node)), dom = _DOMSerializer$render.dom; this.dom = dom; this.domElement = this.isHTMLElement(dom) ? dom : undefined; if (emojiNodeDataProvider) { var previousEmojiDescription; emojiNodeDataProvider.getData(node, function (payload) { if (payload.error) { EmojiNodeView.logError(payload.error); _this.renderFallback(); return; } var optionalEmojiDescription = payload.data; if (!optionalEmojiDescription) { _this.renderFallback(); return; } var emojiRepresentation = optionalEmojiDescription === null || optionalEmojiDescription === void 0 ? void 0 : optionalEmojiDescription.representation; if (!EmojiNodeView.isEmojiRepresentationSupported(emojiRepresentation)) { _this.renderFallback(); return; } if (isEqual(previousEmojiDescription, optionalEmojiDescription)) { // Do not re-render if the emoji description is the same as before return; } previousEmojiDescription = optionalEmojiDescription; _this.renderEmoji(optionalEmojiDescription, emojiRepresentation); }); } else { var _api$emoji, _sharedState$currentS, _api$connectivity; if (isSSR()) { // The provider doesn't work in SSR, and we don't want to render fallback in SSR, // that's why we don't need to continue node rendering. // In SSR we want to show a placeholder, that `emojiToDom()` returns. return; } // We use the `emojiProvider` from the shared state // because it supports the `emojiProvider` prop in the `ComposableEditor` options // as well as the `emojiProvider` in the `EmojiPlugin` options. var sharedState = api === null || api === void 0 || (_api$emoji = api.emoji) === null || _api$emoji === void 0 ? void 0 : _api$emoji.sharedState; if (!sharedState) { return; } var emojiProvider = (_sharedState$currentS = sharedState.currentState()) === null || _sharedState$currentS === void 0 ? void 0 : _sharedState$currentS.emojiProvider; if (emojiProvider) { void this.updateDom(emojiProvider); } var unsubscribe = sharedState.onChange(function (_ref2) { var nextSharedState = _ref2.nextSharedState; if (emojiProvider === (nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.emojiProvider)) { // Do not update if the provider is the same return; } emojiProvider = nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.emojiProvider; void _this.updateDom(emojiProvider); }); // Refresh emojis if we go back online var subscribeToConnection = api === null || api === void 0 || (_api$connectivity = api.connectivity) === null || _api$connectivity === void 0 ? void 0 : _api$connectivity.sharedState.onChange(function (_ref3) { var prevSharedState = _ref3.prevSharedState, nextSharedState = _ref3.nextSharedState; if (isOfflineMode(prevSharedState === null || prevSharedState === void 0 ? void 0 : prevSharedState.mode) && (nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.mode) === 'online' && _this.renderingFallback && editorExperiment('platform_editor_offline_editing_web', true)) { var _sharedState$currentS2; _this.updateDom((_sharedState$currentS2 = sharedState.currentState()) === null || _sharedState$currentS2 === void 0 ? void 0 : _sharedState$currentS2.emojiProvider); } }); this.destroy = function () { unsubscribe(); subscribeToConnection === null || subscribeToConnection === void 0 || subscribeToConnection(); }; } } /** Type guard to check if a Node is an HTMLElement in a safe way. */ return _createClass(EmojiNodeView, [{ key: "isHTMLElement", value: function isHTMLElement(element) { if (element === null) { return false; } // In SSR `HTMLElement` is not defined, so we need to use duck typing here return 'innerHTML' in element && 'style' in element && 'classList' in element; } }, { key: "updateDom", value: function () { var _updateDom = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(emojiProvider) { var _this$node$attrs, shortName, id, fallback, emojiDescription, emojiRepresentation; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: _context.prev = 0; _this$node$attrs = this.node.attrs, shortName = _this$node$attrs.shortName, id = _this$node$attrs.id, fallback = _this$node$attrs.text; _context.next = 4; return emojiProvider === null || emojiProvider === void 0 ? void 0 : emojiProvider.fetchByEmojiId({ id: id, shortName: shortName, fallback: fallback }, true); case 4: emojiDescription = _context.sent; if (emojiDescription) { _context.next = 9; break; } EmojiNodeView.logError(new Error('Emoji description is not loaded')); this.renderFallback(); return _context.abrupt("return"); case 9: emojiRepresentation = emojiDescription === null || emojiDescription === void 0 ? void 0 : emojiDescription.representation; if (EmojiNodeView.isEmojiRepresentationSupported(emojiRepresentation)) { _context.next = 14; break; } EmojiNodeView.logError(new Error('Emoji representation is not supported')); this.renderFallback(); return _context.abrupt("return"); case 14: this.renderEmoji(emojiDescription, emojiRepresentation); _context.next = 21; break; case 17: _context.prev = 17; _context.t0 = _context["catch"](0); EmojiNodeView.logError(_context.t0 instanceof Error ? _context.t0 : new Error('Unknown error on EmojiNodeView updateDom')); this.renderFallback(); case 21: case "end": return _context.stop(); } }, _callee, this, [[0, 17]]); })); function updateDom(_x) { return _updateDom.apply(this, arguments); } return updateDom; }() }, { key: "cleanUpAndRenderCommonAttributes", value: // Pay attention, this method should be called only when the emoji provider returns // emoji data to prevent rendering empty emoji during loading. function cleanUpAndRenderCommonAttributes() { // Clean up the DOM before rendering the new emoji if (this.domElement) { this.domElement.innerHTML = ''; this.domElement.style.cssText = ''; this.domElement.classList.remove(EmojiSharedCssClassName.EMOJI_PLACEHOLDER); this.domElement.removeAttribute('aria-label'); // The label is set in the renderEmoji method this.domElement.removeAttribute('aria-busy'); } } }, { key: "renderFallback", value: function renderFallback() { this.renderingFallback = true; this.cleanUpAndRenderCommonAttributes(); var fallbackElement = document.createElement('span'); fallbackElement.innerText = this.node.attrs.text || this.node.attrs.shortName; fallbackElement.setAttribute('data-testid', "fallback-emoji-".concat(this.node.attrs.shortName)); fallbackElement.setAttribute('data-emoji-type', 'fallback'); this.dom.appendChild(fallbackElement); } }, { key: "renderEmoji", value: function renderEmoji(description, representation) { this.renderingFallback = false; this.cleanUpAndRenderCommonAttributes(); var emojiType = 'sprite' in representation ? 'sprite' : 'image'; // Add wrapper for the emoji var containerElement = document.createElement('span'); containerElement.setAttribute('role', 'img'); containerElement.setAttribute('title', description.shortName); containerElement.classList.add(EmojiSharedCssClassName.EMOJI_CONTAINER); containerElement.setAttribute('data-testid', "".concat(emojiType, "-emoji-").concat(description.shortName)); containerElement.setAttribute('data-emoji-type', emojiType); containerElement.setAttribute('aria-label', "".concat(this.intl.formatMessage(messages.emojiNodeLabel), " ").concat(description.shortName)); var emojiElement = 'sprite' in representation ? this.createSpriteEmojiElement(representation) : this.createImageEmojiElement(description, representation); containerElement.appendChild(emojiElement); this.dom.appendChild(containerElement); } }, { key: "createSpriteEmojiElement", value: function createSpriteEmojiElement(representation) { var spriteElement = document.createElement('span'); spriteElement.classList.add(EmojiSharedCssClassName.EMOJI_SPRITE); var sprite = representation.sprite; var xPositionInPercent = 100 / (sprite.column - 1) * representation.xIndex; var yPositionInPercent = 100 / (sprite.row - 1) * representation.yIndex; spriteElement.style.backgroundImage = "url(".concat(sprite.url, ")"); spriteElement.style.backgroundPosition = "".concat(xPositionInPercent, "% ").concat(yPositionInPercent, "%"); spriteElement.style.backgroundSize = "".concat(sprite.column * 100, "% ").concat(sprite.row * 100, "%"); spriteElement.style.minWidth = "".concat(defaultEmojiHeight, "px"); spriteElement.style.minHeight = "".concat(defaultEmojiHeight, "px"); if (!expValEquals('platform_editor_lovability_emoji_scaling', 'isEnabled', true)) { spriteElement.style.width = "".concat(defaultEmojiHeight, "px"); spriteElement.style.height = "".concat(defaultEmojiHeight, "px"); } return spriteElement; } }, { key: "createImageEmojiElement", value: function createImageEmojiElement(emojiDescription, representation) { var _this2 = this; var imageElement = document.createElement('img'); imageElement.classList.add(EmojiSharedCssClassName.EMOJI_IMAGE); imageElement.src = 'imagePath' in representation ? representation.imagePath : representation.mediaPath; imageElement.loading = 'lazy'; imageElement.alt = emojiDescription.name || emojiDescription.shortName; imageElement.style.minWidth = "".concat(defaultEmojiHeight, "px"); imageElement.style.objectFit = 'contain'; imageElement.height = defaultEmojiHeight; imageElement.onerror = function () { if (editorExperiment('platform_editor_offline_editing_web', true)) { // If there's an error (ie. offline) render the ascii fallback if possible, otherwise // mark the node to refresh when returning online. // Create a check that confirms if this.node.attrs.text if an ascii emoji if (isSingleEmoji(_this2.node.attrs.text)) { _this2.renderFallback(); } else { _this2.renderingFallback = true; } } else { _this2.renderFallback(); } }; return imageElement; } }], [{ key: "logError", value: function logError(error) { void logException(error, { location: 'editor-plugin-emoji/EmojiNodeView' }); } }, { key: "isEmojiRepresentationSupported", value: function isEmojiRepresentationSupported(representation) { return !!representation && ('sprite' in representation || 'imagePath' in representation || 'mediaPath' in representation); } }]); }();