import { render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { Signal, effect } from "/js/signals.js"; import { shallowEquals } from "/js/utils.js"; import { tokenizeRichText, tokensHaveFacetType } from "/js/richTextHelpers.js"; import { richTextTokensTemplate } from "/js/templates/richText.template.js"; class PluginRichText extends Component { static placeholderFallbackMs = 500; static get observedAttributes() { return ["truncate-urls"]; } set text(value) { this._text = value; this.$text?.set(value); } set facets(value) { this._facets = value; this.$facets?.set(value); } set transformContext(value) { this._transformContext = value; this.$transformContext?.set(value); } connectedCallback() { if (this.initialized) return; this.initialized = true; if (!this.pluginService) { throw new Error("pluginService is required"); } this.$text = new Signal.State(this._text ?? ""); this.$facets = new Signal.State(this._facets ?? null); this.$transformContext = new Signal.State(this._transformContext ?? null, { equals: shallowEquals, }); this.$truncateUrls = new Signal.State(this.hasAttribute("truncate-urls")); this.disposeRender = effect(() => this.renderContent()); } attributeChangedCallback(name, oldValue, newValue) { if (!this.initialized || oldValue === newValue) return; if (name === "truncate-urls") { this.$truncateUrls.set(newValue !== null); } } disconnectedCallback() { if (!this.initialized) return; this.disposeRender?.(); this.disposeRender = null; this._currentRequest = null; if (this._claimedFacetFallbackTimer) { clearTimeout(this._claimedFacetFallbackTimer); this._claimedFacetFallbackTimer = null; } this.initialized = false; } renderContent() { const text = this.$text.get() ?? ""; const facets = this.$facets.get() ?? []; const transformContext = this.$transformContext.get(); const truncateUrls = this.$truncateUrls.get(); const pluginService = this.pluginService; const baseTokens = tokenizeRichText({ text, facets }); // Subscribe to the version signal so registering/unregistering a transform will re-render pluginService.$richTextTransformsVersion.get(); if (this._claimedFacetFallbackTimer) { clearTimeout(this._claimedFacetFallbackTimer); this._claimedFacetFallbackTimer = null; } // Request transformed tokens from pluginService if (!transformContext) { this._currentRequest = null; this._renderTokens({ tokens: baseTokens, truncateUrls, placeholderFacetTypes: null, }); return; } const request = pluginService.transformRichTextTokens(baseTokens, { numberOfLines: null, ...transformContext, source: { text, facets }, }); this._currentRequest = request; // Hide facet tokens whose feature $type is claimed by a pending transform, // so shortcodes don't briefly flash as plaintext before the transform swaps // them for their rendered form (e.g. bluemoji). const claimedFacetTypes = pluginService.getClaimedFacetTypes(); this._renderTokens({ tokens: baseTokens, truncateUrls, placeholderFacetTypes: claimedFacetTypes, }); // Fallback + null-resolve re-render only matter when something was hidden. const hasClaimedFacets = tokensHaveFacetType(baseTokens, claimedFacetTypes); if (hasClaimedFacets) { this._claimedFacetFallbackTimer = setTimeout(() => { this._claimedFacetFallbackTimer = null; if (this._currentRequest !== request || !this.initialized) return; this._renderTokens({ tokens: baseTokens, truncateUrls, placeholderFacetTypes: null, }); }, PluginRichText.placeholderFallbackMs); } request.then( (transformed) => { if ( // Ignore results that resolved after a newer render pass this._currentRequest !== request || !this.initialized ) return; if (this._claimedFacetFallbackTimer) { clearTimeout(this._claimedFacetFallbackTimer); this._claimedFacetFallbackTimer = null; } if (!transformed) { if (hasClaimedFacets) { this._renderTokens({ tokens: baseTokens, truncateUrls, placeholderFacetTypes: null, }); } return; } this._renderTokens({ tokens: transformed, truncateUrls, placeholderFacetTypes: null, }); }, (error) => { console.error("Rich text transform request failed", error); }, ); } _renderTokens({ tokens, truncateUrls, placeholderFacetTypes }) { render( richTextTokensTemplate({ tokens, truncateUrls, placeholderFacetTypes, renderNodeToken: (token) => this.pluginService.renderRichTextNodeToken(token, this), }), this, ); } } PluginRichText.register();