diff options
Diffstat (limited to 'polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts')
-rw-r--r-- | polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts | 132 |
1 files changed, 91 insertions, 41 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts index 2812b47cd1..c283876223 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.ts @@ -15,11 +15,11 @@ * limitations under the License. */ import '../../../styles/shared-styles'; -import {PolymerElement} from '@polymer/polymer/polymer-element'; -import {htmlTemplate} from './gr-linked-text_html'; import {GrLinkTextParser, LinkTextParserConfig} from './link-text-parser'; import {GerritNav} from '../../core/gr-navigation/gr-navigation'; -import {customElement, property, observe} from '@polymer/decorators'; +import {LitElement, css, html, PropertyValues} from 'lit'; +import {customElement, property} from 'lit/decorators'; +import {assertIsDefined} from '../../../utils/common-util'; declare global { interface HTMLElementTagNameMap { @@ -27,80 +27,105 @@ declare global { } } -export interface GrLinkedText { - $: { - output: HTMLSpanElement; - }; -} - @customElement('gr-linked-text') -export class GrLinkedText extends PolymerElement { - static get template() { - return htmlTemplate; - } +export class GrLinkedText extends LitElement { + private outputElement?: HTMLSpanElement; @property({type: Boolean}) removeZeroWidthSpace?: boolean; - // content default is null, because this.$.output.textContent is string|null @property({type: String}) - content: string | null = null; + content = ''; - @property({type: Boolean, reflectToAttribute: true}) + @property({type: Boolean, attribute: true}) pre = false; - @property({type: Boolean, reflectToAttribute: true}) + @property({type: Boolean, attribute: true}) disabled = false; + @property({type: Boolean, attribute: true}) + inline = false; + @property({type: Object}) config?: LinkTextParserConfig; - @observe('content') - _contentChanged(content: string | null) { - // In the case where the config may not be set (perhaps due to the - // request for it still being in flight), set the content anyway to - // prevent waiting on the config to display the text. - if (!this.config) { - return; + static override get styles() { + return css` + :host { + display: block; + } + :host([inline]) { + display: inline; + } + :host([pre]) ::slotted(span) { + white-space: var(--linked-text-white-space, pre-wrap); + word-wrap: var(--linked-text-word-wrap, break-word); + } + `; + } + + override render() { + return html`<slot name="insert"></slot>`; + } + + // NOTE: LinkTextParser dynamically creates HTML fragments based on backend + // configuration commentLinks. These commentLinks can contain arbitrary HTML + // fragments. This means that arbitrary HTML needs to be injected into the + // DOM-tree, where this HTML is is controlled on the server-side in the + // server-configuration rather than by arbitrary users. + // To enable this injection of 'unsafe' HTML, LinkTextParser generates + // HTML fragments. Lit does not support inserting html fragments directly + // into its DOM-tree as it controls the DOM-tree that it generates. + // Therefore, to get around this we create a single element that we slot into + // the Lit-owned DOM. This element will not be part of this LitElement as + // it's slotted in and thus can be modified on the fly by handleParseResult. + override firstUpdated(_changedProperties: PropertyValues): void { + this.outputElement = document.createElement('span'); + this.outputElement.id = 'output'; + this.outputElement.slot = 'insert'; + this.append(this.outputElement); + } + + override updated(changedProperties: PropertyValues): void { + if (changedProperties.has('content') || changedProperties.has('config')) { + this._contentOrConfigChanged(); + } else if (changedProperties.has('disabled')) { + this.styleLinks(); } - this.$.output.textContent = content; } /** * Because either the source text or the linkification config has changed, * the content should be re-parsed. + * Private but used in tests. * * @param content The raw, un-linkified source string to parse. * @param config The server config specifying commentLink patterns */ - @observe('content', 'config') - _contentOrConfigChanged( - content: string | null, - config?: LinkTextParserConfig - ) { - if (!config) { + _contentOrConfigChanged() { + if (!this.config) { + assertIsDefined(this.outputElement); + this.outputElement.textContent = this.content; return; } - // TODO(TS): mapCommentlinks always has value, remove - if (!GerritNav.mapCommentlinks) return; - config = GerritNav.mapCommentlinks(config); - const output = this.$.output; - output.textContent = ''; + const config = GerritNav.mapCommentlinks(this.config); + assertIsDefined(this.outputElement); + this.outputElement.textContent = ''; const parser = new GrLinkTextParser( config, (text: string | null, href: string | null, fragment?: DocumentFragment) => - this._handleParseResult(text, href, fragment), + this.handleParseResult(text, href, fragment), this.removeZeroWidthSpace ); - parser.parse(content); + parser.parse(this.content); // Ensure that external links originating from HTML commentlink configs // open in a new tab. @see Issue 5567 // Ensure links to the same host originating from commentlink configs // open in the same tab. When target is not set - default is _self // @see Issue 4616 - output.querySelectorAll('a').forEach(anchor => { + this.outputElement.querySelectorAll('a').forEach(anchor => { if (anchor.hostname === window.location.hostname) { anchor.removeAttribute('target'); } else { @@ -108,6 +133,30 @@ export class GrLinkedText extends PolymerElement { } anchor.setAttribute('rel', 'noopener'); }); + + this.styleLinks(); + } + + /** + * Styles the links based on whether gr-linked-text is disabled or not + */ + private styleLinks() { + assertIsDefined(this.outputElement); + this.outputElement.querySelectorAll('a').forEach(anchor => { + anchor.setAttribute('style', this.computeLinkStyle()); + }); + } + + private computeLinkStyle() { + if (this.disabled) { + return ` + color: inherit; + text-decoration: none; + pointer-events: none; + `; + } else { + return 'color: var(--link-color)'; + } } /** @@ -119,12 +168,13 @@ export class GrLinkedText extends PolymerElement { * - To attach an arbitrary fragment: when called with only the `fragment` * argument, the fragment should be attached to the resulting DOM as is. */ - private _handleParseResult( + private handleParseResult( text: string | null, href: string | null, fragment?: DocumentFragment ) { - const output = this.$.output; + assertIsDefined(this.outputElement); + const output = this.outputElement; if (href) { const a = document.createElement('a'); a.setAttribute('href', href); |