Skip to content

Commit

Permalink
perf(rich-text-editor): adding extra functionality to link
Browse files Browse the repository at this point in the history
adding extra functionality to link

✅ Closes: COMUI-2981
  • Loading branch information
gavin-everett-genesys committed Jan 9, 2025
1 parent 183ce38 commit ce98a6b
Show file tree
Hide file tree
Showing 20 changed files with 455 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

### Used by

- [gux-rich-text-editor-action-link](../gux-rich-text-editor/gux-rich-text-editor-action/gux-rich-text-editor-action-link)
- [gux-toast](../../stable/gux-toast)

### Graph
```mermaid
graph TD;
gux-rich-text-editor-action-link --> gux-cta-group
gux-toast --> gux-cta-group
style gux-cta-group fill:#f9f,stroke:#333,stroke-width:4px
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
inset-block-start: 0;
inset-inline-start: 0;
min-inline-size: 280px; // TODO: add new token once UX has added it: GDS-2672
padding: var(--gse-ui-popover-gap);
padding: var(--gse-ui-popover-padding);
overflow: hidden;
background-color: var(--gse-ui-popover-backgroundColor);
border: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ <h1>gux-rich-text-editor</h1>
<gux-rich-text-editor-action
action="blockQuote"
></gux-rich-text-editor-action>
<gux-rich-text-editor-action-link></gux-rich-text-editor-action-link>
<gux-rich-text-editor-action action="undo"></gux-rich-text-editor-action>
<gux-rich-text-editor-action action="redo"></gux-rich-text-editor-action>
</gux-rich-text-editor-action-group>
Expand Down Expand Up @@ -84,22 +85,29 @@ <h2>No action divider</h2>
<style
onload="(function () {
(async () => {
// This example code uses Tiptap 2.0.
// However, it can be configured or adapted to work with other editors of your choice.
// Import required editor and extension modules
// - Editor: Main class to create and manipulate the text editor.
// - StarterKit: Provides basic functionality like bold, italic, lists, etc.
// - Underline: Adds underline support to the editor.
// - Heading: Adds heading levels (H1, H2, H3) to the editor.
// - Link : Creates a hyperlink for highlighted text.
const { Editor } = await import('https://cdn.jsdelivr.net/npm/@tiptap/core@2.2.2/+esm');
const StarterKit = (await import('https://cdn.jsdelivr.net/npm/@tiptap/starter-kit@2.2.2/+esm')).default;
const Underline = (await import('https://cdn.jsdelivr.net/npm/@tiptap/extension-underline@2.2.2/+esm')).default;
const Heading = (await import('https://cdn.jsdelivr.net/npm/@tiptap/extension-heading@2.7.4/+esm')).default;
const Link = (await import('https://cdn.jsdelivr.net/npm/@tiptap/extension-link@2.7.4/+esm')).default;
const Highlight = (await import('https://cdn.jsdelivr.net/npm/@tiptap/extension-highlight@2.7.4/+esm')).default;
// Initialize the editor
const editor = new Editor({
element: document.querySelector('.editorElement'),
extensions: [StarterKit, Underline, Heading.configure({
levels: [1, 2, 3], // Allows heading levels 1, 2, and 3.
})],
extensions: [StarterKit, Underline, Highlight
,Link.configure({
openOnClick: true, // Automatically opens links when clicked.
autolink: false, // Automatically creates links when typing URLs.
defaultProtocol: 'https', // Default protocol for links if not specified.
}),],
content: 'Start typing here...',
injectCSS: false,
editorProps: {
Expand Down Expand Up @@ -152,10 +160,16 @@ <h2>No action divider</h2>
}
// Attach event listeners to the editor
// These listeners update the action state when the editor's content or selection changes.
// These listeners update the action states when the editor's content or selection changes.
editor.on('transaction', updateActionState);
editor.on('selectionUpdate', updateActionState);
editor.on('selectionUpdate', handleSelectionUpdate);
function handleSelectionUpdate() {
updateActionState();
applyTextToInput();
}
/* Heading action */
// Apply heading styles (H1, H2, H3, paragraph) by listening for clicks on specific typography settings.
const heading1 = document.querySelector(`gux-rich-style-list-item[value='heading-1']`);
Expand All @@ -178,6 +192,60 @@ <h2>No action divider</h2>
editor.commands.setParagraph();
})
/* Link action */
/* The code below is just an example and can be altered to your preference. */
// Global variable to store the starting position of the selected text in the text editor.
let selectedTextStartPosition = 0;
// Flag to prevent the applyTextToInput function from running during a link update.
let isLinkActionInProgress = false;
// Event listener for the Link action.
// Adds or updates a hyperlink when a user specifies a URL.
const linkAction = document.querySelector('gux-rich-text-editor-action-link');
linkAction.addEventListener('linkOptions', (event) => {
isLinkActionInProgress = true;
// Retrieve the text to display value and calculate its end position.
const textToDisplay = event.detail.textToDisplay;
const selectedTextEndPosition = selectedTextStartPosition + textToDisplay.length;
// Insert the specified text and apply the link to it.
editor.commands.insertContent(textToDisplay);
editor.commands.setTextSelection({ from: selectedTextStartPosition, to: selectedTextEndPosition });
editor.chain().focus().extendMarkRange('link').setLink({ href: event.detail.href }).run();
// Allow selection updates again after the link action is complete.
isLinkActionInProgress = false;
});
// Updates the 'Text to display' input with the currently selected text in the editor.
function applyTextToInput() {
// Skip updating the input if a link action is in progress.
if (isLinkActionInProgress) {
return;
}
// Get the current selection range and the selected text.
const { view, state } = editor;
const { from, to } = view.state.selection;
const selectedText = state.doc.textBetween(from, to, '');
// Store the starting position of the selected text.
selectedTextStartPosition = from;
// Access the shadow DOM of the `gux-rich-text-editor-action-link` component.
const shadowRoot = linkAction.shadowRoot;
if (shadowRoot) {
// Query the 'Text to display' input element inside the shadow DOM.
const textInput = shadowRoot.querySelector('#textToDisplay');
if (textInput) {
// Set the value of the input to the selected text.
textInput.value = selectedText;
}
}
}
})();
})()"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
:host {
display: block;
}

gux-popover {
div.gux-popover-content-wrapper {
display: flex;
flex-direction: column;
gap: var(--gse-ui-popover-gap);
}
}

gux-button-slot {
inline-size: var(--gse-ui-button-iconOnly-width);
block-size: var(--gse-ui-button-default-height);

button.gux-is-pressed {
color: var(--gse-ui-button-ghost-active-foregroundColor);
background-color: var(--gse-ui-button-ghost-active-backgroundColor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
Component,
Prop,
State,
Element,
EventEmitter,
Event,
Listen,
h,
Host
} from '@stencil/core';
import { OnClickOutside } from '@utils/decorator/on-click-outside';
import { trackComponent } from '@utils/tracking/usage';
import { buildI18nForComponent, GetI18nValue } from 'i18n';
import translationResources from '../i18n/en.json';
import { afterNextRender } from '@utils/dom/after-next-render';

@Component({
tag: 'gux-rich-text-editor-action-link',
styleUrl: 'gux-rich-text-editor-action-link.scss',
shadow: true
})
export class GuxRichTextEditorActionLink {
private i18n: GetI18nValue;
actionButton: HTMLElement;
linkAddressInputElement: HTMLInputElement;
textToDisplayInputElement: HTMLInputElement;

@Element()
private root: HTMLElement;

@Prop()
disabled: boolean = false;

@State()
isOpen: boolean = false;

@Event() linkOptions: EventEmitter<{ textToDisplay: string; href: string }>;

@OnClickOutside({ triggerEvents: 'mousedown' })
onClickOutside(): void {
this.isOpen = false;
}

@Listen('keydown')
handleKeydown(event: KeyboardEvent): void {
const composedPath = event.composedPath();
switch (event.key) {
case 'Escape':
this.isOpen = false;
this.actionButton.focus();
break;
case 'ArrowDown':
case 'Enter':
if (composedPath.includes(this.actionButton)) {
event.preventDefault();
this.isOpen = true;
this.focusTextToDisplayInputElement();
}
break;
}
}

async componentWillLoad(): Promise<void> {
trackComponent(this.root);
this.i18n = await buildI18nForComponent(this.root, translationResources);
}

private emitLinkOptions(): void {
const linkOptions = {
textToDisplay: this.textToDisplayInputElement.value,
href: this.linkAddressInputElement.value
};
this.linkOptions.emit(linkOptions);

this.textToDisplayInputElement.value = null;
this.linkAddressInputElement.value = null;

this.isOpen = false;
}

private togglePopover(): void {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.focusTextToDisplayInputElement();
} else {
this.actionButton.focus();
}
}

private focusTextToDisplayInputElement(): void {
afterNextRender(() => {
this.textToDisplayInputElement.focus();
});
}

private renderTooltip(): JSX.Element {
if (!this.disabled) {
return (
<gux-tooltip>
<div slot="content">{this.i18n('link')}</div>
</gux-tooltip>
) as JSX.Element;
}
}

render(): JSX.Element {
return (
<Host>
<gux-button-slot accent="ghost" icon-only>
<button
id="popover-target"
class={{ 'gux-is-pressed': this.isOpen }}
onClick={() => this.togglePopover()}
ref={el => (this.actionButton = el)}
type="button"
disabled={this.disabled}
aria-label={this.i18n('link')}
aria-haspopup="true"
aria-expanded={this.isOpen.toString()}
>
<gux-icon
size="small"
icon-name="fa/link-simple-regular"
decorative
></gux-icon>
</button>
{this.renderTooltip()}
</gux-button-slot>
<gux-popover is-open={this.isOpen} for="popover-target">
<div class="gux-popover-content-wrapper">
<gux-form-field-text-like>
<input
id="textToDisplay"
ref={el => (this.textToDisplayInputElement = el)}
slot="input"
type="text"
/>
<label slot="label">{this.i18n('textToDisplay')}</label>
</gux-form-field-text-like>
<gux-form-field-text-like>
<input
ref={el => (this.linkAddressInputElement = el)}
slot="input"
type="text"
/>
<label slot="label">{this.i18n('linkAddress')}</label>
</gux-form-field-text-like>
<gux-cta-group align="end">
<gux-button onClick={() => this.emitLinkOptions()} slot="primary">
{this.i18n('insert')}
</gux-button>
<gux-button onClick={() => this.togglePopover()} slot="dismiss">
{this.i18n('cancel')}
</gux-button>
</gux-cta-group>
</div>
</gux-popover>
</Host>
) as JSX.Element;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# gux-rich-text-editor-action-link



<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ----------- | --------- | ------- |
| `disabled` | `disabled` | | `boolean` | `false` |


## Events

| Event | Description | Type |
| ------------- | ----------- | ------------------------------------------------------- |
| `linkOptions` | | `CustomEvent<{ textToDisplay: string; href: string; }>` |


## Dependencies

### Depends on

- [gux-tooltip](../../../../stable/gux-tooltip)
- [gux-button-slot](../../../../stable/gux-button-slot)
- [gux-icon](../../../../stable/gux-icon)
- [gux-popover](../../../../stable/gux-popover)
- [gux-form-field-text-like](../../../../stable/gux-form-field/components/gux-form-field-text-like)
- [gux-cta-group](../../../gux-cta-group)
- [gux-button](../../../../stable/gux-button)

### Graph
```mermaid
graph TD;
gux-rich-text-editor-action-link --> gux-tooltip
gux-rich-text-editor-action-link --> gux-button-slot
gux-rich-text-editor-action-link --> gux-icon
gux-rich-text-editor-action-link --> gux-popover
gux-rich-text-editor-action-link --> gux-form-field-text-like
gux-rich-text-editor-action-link --> gux-cta-group
gux-rich-text-editor-action-link --> gux-button
gux-popover --> gux-dismiss-button
gux-dismiss-button --> gux-button-slot
gux-dismiss-button --> gux-icon
gux-form-field-text-like --> gux-radial-loading
gux-form-field-text-like --> gux-form-field-label-indicator
gux-form-field-text-like --> gux-form-field-input-clear-button
gux-form-field-text-like --> gux-icon
gux-form-field-input-clear-button --> gux-icon
gux-button --> gux-tooltip-beta
gux-tooltip-beta --> gux-tooltip-base-beta
style gux-rich-text-editor-action-link fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`gux-rich-text-editor-action-link #render should display component as expected (1) 1`] = `"<gux-rich-text-editor-action-link hydrated=""><template shadowrootmode="open"><gux-button-slot icon-only="" accent="ghost" aria-describedby="gux-tooltip-u8cxamafev" hydrated=""><button id="popover-target" type="button" aria-label="Link" aria-haspopup="true" aria-expanded="false"><gux-icon icon-name="fa/link-simple-regular" size="small" hydrated=""></gux-icon></button><gux-tooltip id="gux-tooltip-u8cxamafev" role="tooltip" hydrated=""><div slot="content">Link</div></gux-tooltip></gux-button-slot><gux-popover hydrated=""><div class="gux-popover-content-wrapper"><gux-form-field-text-like hydrated=""><input id="textToDisplay" slot="input" type="text"><label slot="label" for="textToDisplay">Text to display</label></gux-form-field-text-like><gux-form-field-text-like hydrated=""><input slot="input" type="text" id="gux-form-field-input-92s2uvdu4a"><label slot="label" for="gux-form-field-input-92s2uvdu4a">Link Address</label></gux-form-field-text-like><gux-cta-group hydrated=""><gux-button slot="primary" hydrated="">Insert</gux-button><gux-button slot="dismiss" hydrated="">Cancel</gux-button></gux-cta-group></div></gux-popover></template></gux-rich-text-editor-action-link>"`;
Loading

0 comments on commit ce98a6b

Please sign in to comment.