UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

253 lines (236 loc) 12 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.aiGeneratingDecorationPluginKey = void 0; exports.clearAIGeneratingMeta = clearAIGeneratingMeta; exports.createAIGeneratingDecorationPlugin = createAIGeneratingDecorationPlugin; exports.getAIGeneratingDecorationSource = getAIGeneratingDecorationSource; exports.hasAIGeneratingDecoration = hasAIGeneratingDecoration; exports.setAIGeneratingMeta = setAIGeneratingMeta; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _safePlugin = require("@atlaskit/editor-common/safe-plugin"); var _state = require("@atlaskit/editor-prosemirror/state"); var _view = require("@atlaskit/editor-prosemirror/view"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /** * ProseMirror plugin that manages AI-generating decorations on media nodes. * * Instead of storing transient `__isAIGenerating` attributes in the ADF schema * (which pollutes the document model and undo history), this plugin uses * ProseMirror decorations — a view-layer-only mechanism that never affects the * document content, serialization, or undo/redo stack. */ // ── Plugin Key ────────────────────────────────────────────────────────────── var aiGeneratingDecorationPluginKey = exports.aiGeneratingDecorationPluginKey = new _state.PluginKey('aiGeneratingDecoration'); // ── Types ─────────────────────────────────────────────────────────────────── var AI_GENERATING_DECORATION_TYPE = 'ai-generating'; // ── Helpers ───────────────────────────────────────────────────────────────── /** * Build a DecorationSet containing a Decoration.node for every media node * targeted by the given set. * * Targets are matched by `localId` first (the mediaSingle wrapper's or the media * node's own) so that duplicate copies of the same image — which share a media * file `id` — don't all get decorated; only the node the user acted on lights * up. Falls back to the media file `id` for the CWR flow and for any node that * has no localId yet. */ function buildDecorationSet(doc, targetIds) { if (targetIds.size === 0) { return _view.DecorationSet.empty; } var decorations = []; doc.descendants(function (node, pos, parent) { var _parent$attrs; if (node.type.name !== 'media') { return; } var wrapperLocalId = parent === null || parent === void 0 || (_parent$attrs = parent.attrs) === null || _parent$attrs === void 0 ? void 0 : _parent$attrs.localId; var source = wrapperLocalId && targetIds.get(wrapperLocalId) || node.attrs.localId && targetIds.get(node.attrs.localId) || targetIds.get(node.attrs.id); if (source) { decorations.push(_view.Decoration.node(pos, pos + node.nodeSize, {}, // no DOM attrs needed — the NodeView reads the decoration spec { type: AI_GENERATING_DECORATION_TYPE, mediaId: node.attrs.id, source: source })); } }); return _view.DecorationSet.create(doc, decorations); } // ── Public utilities ──────────────────────────────────────────────────────── /** * Returns `true` if the given decorations array contains an AI-generating * decoration. Call this from a NodeView's `update()` / `viewShouldUpdate()` * to determine whether to render the AI border. */ function hasAIGeneratingDecoration(decorations) { return decorations.some(function (d) { return d.spec.type === AI_GENERATING_DECORATION_TYPE; }); } /** * Returns the `source` of the AI-generating decoration present on the given * decorations array, or `undefined` if there is none. Lets a NodeView vary its * rendering by which flow (e.g. `'cwr'`) triggered the generating state. */ function getAIGeneratingDecorationSource(decorations) { var _decorations$find; return (_decorations$find = decorations.find(function (d) { return d.spec.type === AI_GENERATING_DECORATION_TYPE; })) === null || _decorations$find === void 0 ? void 0 : _decorations$find.spec.source; } /** * Dispatch a transaction that sets the AI-generating decoration on a media * node identified by `mediaId`. * * Usage from the editor bridge: * ``` * editorAPI.core.actions.execute(({ tr }) => * setAIGeneratingMeta(tr, mediaId), * ); * ``` */ function setAIGeneratingMeta(tr, mediaId, source) { return tr.setMeta(aiGeneratingDecorationPluginKey, { type: 'SET_GENERATING', mediaId: mediaId, source: source }).setMeta('addToHistory', false); } /** * Dispatch a transaction that clears the AI-generating decoration for a * specific media node. */ function clearAIGeneratingMeta(tr, mediaId) { return tr.setMeta(aiGeneratingDecorationPluginKey, { type: 'CLEAR_GENERATING', mediaId: mediaId }).setMeta('addToHistory', false); } // ── Plugin ────────────────────────────────────────────────────────────────── /** Creates the ProseMirror plugin that manages AI-generating decorations on media nodes. */ function createAIGeneratingDecorationPlugin() { return new _safePlugin.SafePlugin({ key: aiGeneratingDecorationPluginKey, state: { init: function init() { return { generatingMediaIds: new Map(), decorationSet: _view.DecorationSet.empty }; }, apply: function apply(tr, pluginState, _oldState, newState) { // Disabled when gate/experiment off — drop decorations. if (!(0, _platformFeatureFlags.fg)('cc-maui-phase-2') || !(0, _expValEquals.expValEquals)('cc-maui-experiment', 'isEnabled', true)) { if (pluginState.generatingMediaIds.size > 0) { return { generatingMediaIds: new Map(), decorationSet: _view.DecorationSet.empty }; } return pluginState; } var meta = tr.getMeta(aiGeneratingDecorationPluginKey); if (meta) { switch (meta.type) { case 'SET_GENERATING': { var _meta$source; var ids = new Map(pluginState.generatingMediaIds); ids.set(meta.mediaId, (_meta$source = meta.source) !== null && _meta$source !== void 0 ? _meta$source : 'maui'); var _hasCwrIds = (0, _expValEquals.expValEquals)('aifc_page_create_with_rovo_include_infographics', 'isEnabled', true) && (0, _toConsumableArray2.default)(ids.values()).some(function (s) { return s === 'cwr'; }); var newDecoSet = buildDecorationSet(newState.doc, ids); if (_hasCwrIds && newDecoSet.find().length === 0 && ids.size > 0) { // CWR fallback — keep existing decorations during transient doc absence return { generatingMediaIds: ids, decorationSet: pluginState.decorationSet }; } return { generatingMediaIds: ids, decorationSet: newDecoSet }; } case 'CLEAR_GENERATING': { var _ids = new Map(pluginState.generatingMediaIds); _ids.delete(meta.mediaId); var _hasCwrIds2 = (0, _expValEquals.expValEquals)('aifc_page_create_with_rovo_include_infographics', 'isEnabled', true) && (0, _toConsumableArray2.default)(_ids.values()).some(function (s) { return s === 'cwr'; }); var _newDecoSet = buildDecorationSet(newState.doc, _ids); if (_hasCwrIds2 && _newDecoSet.find().length === 0) { // CWR fallback — keep existing decorations during transient doc absence return { generatingMediaIds: _ids, decorationSet: pluginState.decorationSet }; } return { generatingMediaIds: _ids, decorationSet: _newDecoSet }; } case 'CLEAR_ALL': return { generatingMediaIds: new Map(), decorationSet: _view.DecorationSet.empty }; } } // CWR path // Rebuild decorations from scratch because CWR streaming replaces the // entire document on every chunk and map() drops decorations whose // positions can't be mapped. var hasCwrIds = (0, _expValEquals.expValEquals)('aifc_page_create_with_rovo_include_infographics', 'isEnabled', true) && (0, _toConsumableArray2.default)(pluginState.generatingMediaIds.values()).some(function (s) { return s === 'cwr'; }); if (tr.docChanged && hasCwrIds) { var rebuilt = buildDecorationSet(newState.doc, pluginState.generatingMediaIds); var prevCount = pluginState.decorationSet.find().length; var newCount = rebuilt.find().length; // Prevents flickering that results from updating during transient // doc replacements (when nodes are briefly absent) if (newCount !== prevCount && newCount >= pluginState.generatingMediaIds.size) { return _objectSpread(_objectSpread({}, pluginState), {}, { decorationSet: rebuilt }); } return pluginState; } // Remix path // Map decorations through document changes so positions stay in sync if (tr.docChanged && pluginState.decorationSet !== _view.DecorationSet.empty) { try { return _objectSpread(_objectSpread({}, pluginState), {}, { decorationSet: pluginState.decorationSet.map(tr.mapping, newState.doc) }); } catch (_unused) { return { generatingMediaIds: new Map(), decorationSet: _view.DecorationSet.empty }; } } return pluginState; } }, props: { decorations: function decorations(state) { var _aiGeneratingDecorati, _aiGeneratingDecorati2; return (_aiGeneratingDecorati = (_aiGeneratingDecorati2 = aiGeneratingDecorationPluginKey.getState(state)) === null || _aiGeneratingDecorati2 === void 0 ? void 0 : _aiGeneratingDecorati2.decorationSet) !== null && _aiGeneratingDecorati !== void 0 ? _aiGeneratingDecorati : _view.DecorationSet.empty; } } }); }