@atlaskit/editor-plugin-unsupported-content
Version:
Unsupported Content plugin for @atlaskit/editor-core
155 lines (148 loc) • 7.8 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { bind } from 'bind-event-listener';
import { ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics/types/enums';
import { unsupportedContentMessages } from '@atlaskit/editor-common/messages/unsupportedContent';
import { UnsupportedSharedCssClassName } from '@atlaskit/editor-common/styles/unsupported-content';
import { trackUnsupportedContentTooltipDisplayedFor } from '@atlaskit/editor-common/utils/track-unsupported-content';
import { VANILLA_TOOLTIP_DEFAULT_CLASS, VanillaTooltip } from '@atlaskit/editor-common/vanilla-tooltip';
import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
import { getUnsupportedContent } from './get-unsupported-content';
import { getUnsupportedContentIconSpec } from './get-unsupported-content-icon-spec';
/**
* CSS class names for the vanilla unsupported block node view elements.
* Corresponding styles are defined in EditorContentContainer (gated behind
* the platform_editor_vanilla_node_views_phase1 experiment).
*
* ⚠️ Renaming any of these requires the same change in BOTH copies of the styles:
* - `editor-core/src/ui/EditorContentContainer/styles/unsupportedStyles.ts` (emotion)
* - `editor-core/src/ui/EditorContentContainer/EditorContentContainer-compiled.tsx` (cssMap)
*
* These cannot be imported by either stylesheet: `cssMap` keys must be statically defined, so
* the compiled copy hardcodes the strings — as it already does for `UnsupportedSharedCssClassName`.
* A rename that misses a stylesheet fails loudly in the unsupported-content VR tests, which
* assert the styled output rather than the class names.
*/
const VanillaUnsupportedCssClassName = {
BLOCK_INNER: 'unsupported-block-vanilla-inner',
ICON: 'unsupported-vanilla-icon'
};
const TOOLTIP_CLASS_NAME = 'unsupported-content-tooltip';
/** Distinct from the inline node view's prefix, so the counter fallback below cannot collide. */
const TOOLTIP_ID_PREFIX = 'unsupported-block-content-tooltip';
/** Classes applied to the tooltip element: the shared default look, plus our own hook. */
const TOOLTIP_CLASS_NAMES = `${VANILLA_TOOLTIP_DEFAULT_CLASS} ${TOOLTIP_CLASS_NAME}`;
/**
* `crypto.randomUUID()` is undefined outside a secure context. The counter is not the best
* fallback, but given how widely available the crypto API is, it should be fine.
*
* Ideally `VanillaTooltip` would take an optional id and generate its own when one is not
* supplied, rather than leaving this to every caller.
*/
const nextTooltipId = (() => {
let count = 0;
return () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? `${TOOLTIP_ID_PREFIX}-${crypto.randomUUID()}` : `${TOOLTIP_ID_PREFIX}-${count += 1}`;
})();
/**
* Spec for an unsupported block node.
*
* The outer wrapper carries the shared CSS class so selection and cursor styles apply.
* The inner element's `unsupported` class is retained for parity with the React
* implementation, which products and tests may still select on.
*/
function getBlockSpec(label) {
return ['div', {
class: UnsupportedSharedCssClassName.BLOCK_CONTAINER
}, ['div', {
class: `unsupported ${VanillaUnsupportedCssClassName.BLOCK_INNER}`
}, ['span', {}, label], getUnsupportedContentIconSpec(VanillaUnsupportedCssClassName.ICON)]];
}
/**
* Creates the DOM structure for an unsupported content node view.
*
* Matches the React version's two-level structure:
* outer wrapper (gets the shared CSS class — unsupportedBlockView-content-wrap)
* └── inner div (gets visual styles from EditorContentContainer CSS)
* ├── text label (span)
* └── icon (span, tooltip trigger)
*
* Visual styles (background, border, flex) are applied via EditorContentContainer CSS on
* `.unsupportedBlockView-content-wrap > div`, matching how the existing selection/cursor
* styles already work.
*/
function createUnsupportedDOM(node, intl) {
var _domElement$querySele;
const label = getUnsupportedContent(unsupportedContentMessages.unsupportedBlockContent, `${unsupportedContentMessages.unsupportedBlockContent.defaultMessage}:`, node, intl);
const {
dom
} = DOMSerializer.renderSpec(document, getBlockSpec(label));
const domElement = dom instanceof HTMLElement ? dom : undefined;
const icon = (_domElement$querySele = domElement === null || domElement === void 0 ? void 0 : domElement.querySelector(`.${VanillaUnsupportedCssClassName.ICON}`)) !== null && _domElement$querySele !== void 0 ? _domElement$querySele : null;
return {
dom,
icon
};
}
export class UnsupportedBlockNodeView {
constructor(node, intl, dispatchAnalyticsEvent) {
_defineProperty(this, "tooltip", null);
_defineProperty(this, "unbindMouseenter", null);
this.node = node;
const {
dom,
icon
} = createUnsupportedDOM(node, intl);
this.dom = dom;
if (!icon) {
return;
}
// Lazy-init: create the VanillaTooltip on first mouseenter.
// Inline styles are required because the Popover API promotes the tooltip to the
// browser's top layer, where ancestor CSS selectors cannot reach it.
const initTooltip = event => {
var _this$unbindMouseente;
// Unbind before re-dispatching below, so this listener is not re-entered by the
// synthetic event. Matches `EmojiNodeView`.
(_this$unbindMouseente = this.unbindMouseenter) === null || _this$unbindMouseente === void 0 ? void 0 : _this$unbindMouseente.call(this);
this.unbindMouseenter = null;
const tooltipText = intl.formatMessage(unsupportedContentMessages.unsupportedContentTooltip);
this.tooltip = new VanillaTooltip(icon, tooltipText, nextTooltipId(), TOOLTIP_CLASS_NAMES, 300,
// Styling comes from the classes above, defined in the EditorContentContainer stylesheets.
undefined,
// Fired once the tooltip is actually visible, rather than on hover intent, so the
// analytics match the React implementation's `<Tooltip onShow>`.
dispatchAnalyticsEvent ? () => {
var _this$node, _this$node$attrs, _this$node$attrs$orig;
return trackUnsupportedContentTooltipDisplayedFor(dispatchAnalyticsEvent, ACTION_SUBJECT_ID.ON_UNSUPPORTED_BLOCK, (_this$node = this.node) === null || _this$node === void 0 ? void 0 : (_this$node$attrs = _this$node.attrs) === null || _this$node$attrs === void 0 ? void 0 : (_this$node$attrs$orig = _this$node$attrs.originalValue) === null || _this$node$attrs$orig === void 0 ? void 0 : _this$node$attrs$orig.type);
} : undefined);
// VanillaTooltip binds its own `mouseenter` listener inside its constructor.
// Listeners added while an event is being dispatched are not invoked for that
// same event, so without re-dispatching, the tooltip would stay hidden until
// the user hovered away and back. Matches `EmojiNodeView`, which lazy-inits
// the same way.
icon.dispatchEvent(new Event(event.type));
};
this.unbindMouseenter = bind(icon, {
type: 'mouseenter',
listener: initTooltip,
options: {
once: true
}
});
}
update(node) {
if (node.type !== this.node.type) {
return false;
}
// originalValue attrs are immutable — label never changes.
// Track node reference so analytics can read the latest attrs.
this.node = node;
return true;
}
destroy() {
var _this$tooltip, _this$unbindMouseente2;
(_this$tooltip = this.tooltip) === null || _this$tooltip === void 0 ? void 0 : _this$tooltip.destroy();
this.tooltip = null;
(_this$unbindMouseente2 = this.unbindMouseenter) === null || _this$unbindMouseente2 === void 0 ? void 0 : _this$unbindMouseente2.call(this);
this.unbindMouseenter = null;
}
}