Skip to content

Commit

Permalink
feat(demo): add inlines demo
Browse files Browse the repository at this point in the history
  • Loading branch information
pubuzhixing8 committed Jan 14, 2022
1 parent da9a251 commit 0390117
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 13 deletions.
5 changes: 4 additions & 1 deletion custom-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export type ImageElement = {

export type LinkElement = { type: 'link'; url: string; children: Descendant[] }

export type ButtonElement = { type: 'button'; children: Descendant[] }

export type ListItemElement = { type: 'list-item'; children: Descendant[] }

export type MentionElement = {
Expand Down Expand Up @@ -83,7 +85,8 @@ type CustomElement =
| TableRowElement
| TableCellElement
| TitleElement
| VideoElement;
| VideoElement
| ButtonElement;

export type CustomText = {
placeholder?: string
Expand Down
5 changes: 5 additions & 0 deletions demo/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DemoSearchHighlightingComponent } from './search-highlighting/search-hi
import { DemoMentionsComponent } from './mentions/mentions.component';
import { DemoReadonlyComponent } from './readonly/readonly.component';
import { DemoPlaceholderComponent } from './placeholder/placeholder.component';
import { DemoInlinesComponent } from './inlines/inlines.component';

const routes: Routes = [
{
Expand All @@ -35,6 +36,10 @@ const routes: Routes = [
path: 'images',
component: DemoImagesComponent
},
{
path: 'inlines',
component: DemoInlinesComponent
},
{
path: 'search-highlighting',
component: DemoSearchHighlightingComponent
Expand Down
4 changes: 4 additions & 0 deletions demo/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ export class AppComponent implements OnInit {
url: '/images',
name: 'Images'
},
{
url: '/inlines',
name: 'Inlines'
},
{
url: '/search-highlighting',
name: 'Search Highlighting'
Expand Down
8 changes: 7 additions & 1 deletion demo/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { DemoLeafComponent } from './search-highlighting/leaf.component';
import { DemoMentionsComponent } from './mentions/mentions.component';
import { DemoReadonlyComponent } from './readonly/readonly.component';
import { DemoPlaceholderComponent } from './placeholder/placeholder.component';
import { DemoElementEditableButtonComponent } from './components/editable-button/editable-button.component';
import { DemoInlinesComponent } from './inlines/inlines.component';
import { DemoElementLinkComponent } from './components/link/link.component';

@NgModule({
declarations: [
Expand All @@ -35,7 +38,10 @@ import { DemoPlaceholderComponent } from './placeholder/placeholder.component';
DemoLeafComponent,
DemoMentionsComponent,
DemoReadonlyComponent,
DemoPlaceholderComponent
DemoPlaceholderComponent,
DemoElementEditableButtonComponent,
DemoInlinesComponent,
DemoElementLinkComponent
],
imports: [
BrowserModule,
Expand Down
26 changes: 26 additions & 0 deletions demo/app/components/editable-button/editable-button.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import { ButtonElement } from '../../../../custom-types';
import { BaseElementComponent } from 'slate-angular';

@Component({
selector: 'span[demo-element-button]',
template: `
<span contenteditable="false" style="font-size: 0;">{{ inlineChromiumBugfix }}</span>
<slate-children [children]="children" [context]="childrenContext" [viewContext]="viewContext"></slate-children>
<span contenteditable="false" style="font-size: 0;">{{ inlineChromiumBugfix }}</span>
`,
host: {
class: 'demo-element-button'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoElementEditableButtonComponent extends BaseElementComponent<ButtonElement> {
// Put this at the start and end of an inline component to work around this Chromium bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
inlineChromiumBugfix = '$' + String.fromCodePoint(160);

@HostListener('click', ['$event'])
click(event: MouseEvent) {
event.preventDefault();
}
}
34 changes: 34 additions & 0 deletions demo/app/components/link/link.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ChangeDetectionStrategy, Component, HostBinding, HostListener } from '@angular/core';
import { ButtonElement, LinkElement } from '../../../../custom-types';
import { BaseElementComponent } from 'slate-angular';
import { element } from 'protractor';

@Component({
selector: 'a[demo-element-link]',
template: `
<span contenteditable="false" style="font-size: 0;">{{ inlineChromiumBugfix }}</span>
<slate-children [children]="children" [context]="childrenContext" [viewContext]="viewContext"></slate-children>
<span contenteditable="false" style="font-size: 0;">{{ inlineChromiumBugfix }}</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoElementLinkComponent extends BaseElementComponent<LinkElement> {
// Put this at the start and end of an inline component to work around this Chromium bug:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
inlineChromiumBugfix = '$' + String.fromCodePoint(160);

@HostBinding('class.demo-element-link-active')
get active() {
return this.isCollapsed;
}

@HostBinding("attr.href")
get herf() {
return this.element.url;
}

@HostListener('click', ['$event'])
click(event: MouseEvent) {
event.preventDefault();
}
}
8 changes: 8 additions & 0 deletions demo/app/inlines/inlines.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="demo-rich-editor-wrapper" style="position: relative;">
<div class="demo-global-toolbar">
<demo-button *ngFor="let toolbarItem of toolbarItems" [active]="toolbarItem.active()" (onMouseDown)="toolbarItem.action($event)"><span class="material-icons">{{ toolbarItem.icon
}}</span></demo-button>
</div>
<slate-editable class="demo-slate-angular-editor" [editor]="editor" [(ngModel)]="value" (ngModelChange)="valueChange($event)" [renderElement]="renderElement" [keydown]="onKeydown">
</slate-editable>
</div>
254 changes: 254 additions & 0 deletions demo/app/inlines/inlines.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@

import { Component, OnInit } from '@angular/core';
import { Editor, Transforms, createEditor, Element as SlateElement, Range, Descendant } from 'slate';
import { withHistory } from 'slate-history';
import { withAngular } from 'slate-angular';
import { LinkElement, ButtonElement } from 'custom-types';
import isUrl from 'is-url';
import { isKeyHotkey } from 'is-hotkey';
import { DemoElementEditableButtonComponent } from '../components/editable-button/editable-button.component';
import { DemoElementLinkComponent } from '../components/link/link.component';

@Component({
selector: 'demo-inlines',
templateUrl: 'inlines.component.html'
})
export class DemoInlinesComponent implements OnInit {
value = initialValue;

editor = withInlines(withHistory(withAngular(createEditor())));

toolbarItems = [
{
icon: 'link',
active: () => {
return isLinkActive(this.editor);
},
action: (event) => {
event.preventDefault()
const url = window.prompt('Enter the URL of the link:')
if (!url) return
insertLink(this.editor, url);
}
},
{
icon: 'link_off',
active: (event) => {
return isLinkActive(this.editor);
},
action: () => {
if (isLinkActive(this.editor)) {
unwrapLink(this.editor);
}
}
},
{
icon: 'smart_button',
active: () => {
return true;
},
action: (event) => {
event.preventDefault()
if (isButtonActive(this.editor)) {
unwrapButton(this.editor);
} else {
insertButton(this.editor);
}
}
}
];

constructor() { }

ngOnInit(): void {
}

renderElement = (element: SlateElement) => {
if (element.type === 'button') {
return DemoElementEditableButtonComponent;
} else if (element.type === 'link') {
return DemoElementLinkComponent;
}
};

onKeydown = (event: KeyboardEvent) => {
const { selection } = this.editor;

// Default left/right behavior is unit:'character'.
// This fails to distinguish between two cursor positions, such as
// <inline>foo<cursor/></inline> vs <inline>foo</inline><cursor/>.
// Here we modify the behavior to unit:'offset'.
// This lets the user step into and out of the inline without stepping over characters.
// You may wish to customize this further to only use unit:'offset' in specific cases.
if (selection && Range.isCollapsed(selection)) {
const nativeEvent = event
if (isKeyHotkey('left', nativeEvent)) {
event.preventDefault()
Transforms.move(this.editor, { unit: 'offset', reverse: true })
return
}
if (isKeyHotkey('right', nativeEvent)) {
event.preventDefault()
Transforms.move(this.editor, { unit: 'offset' })
return
}
}
};

valueChange(value: Element[]) {
}
}

const withInlines = editor => {
const { insertData, insertText, isInline } = editor

editor.isInline = element =>
['link', 'button'].includes(element.type) || isInline(element)

editor.insertText = text => {
if (text && isUrl(text)) {
wrapLink(editor, text)
} else {
insertText(text)
}
}

editor.insertData = data => {
const text = data.getData('text/plain')

if (text && isUrl(text)) {
wrapLink(editor, text)
} else {
insertData(data)
}
}

return editor
}

const insertLink = (editor, url) => {
if (editor.selection) {
wrapLink(editor, url)
}
}

const insertButton = editor => {
if (editor.selection) {
wrapButton(editor)
}
}

const isLinkActive = editor => {
const [link] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
})
return !!link
}

const isButtonActive = editor => {
const [button] = Editor.nodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
})
return !!button
}

const unwrapLink = editor => {
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'link',
})
}

const unwrapButton = editor => {
Transforms.unwrapNodes(editor, {
match: n =>
!Editor.isEditor(n) && SlateElement.isElement(n) && n.type === 'button',
})
}

const wrapLink = (editor, url: string) => {
if (isLinkActive(editor)) {
unwrapLink(editor)
}

const { selection } = editor
const isCollapsed = selection && Range.isCollapsed(selection)
const link: LinkElement = {
type: 'link',
url,
children: isCollapsed ? [{ text: url }] : [],
}

if (isCollapsed) {
Transforms.insertNodes(editor, link)
} else {
Transforms.wrapNodes(editor, link, { split: true })
Transforms.collapse(editor, { edge: 'end' })
}
}

const wrapButton = editor => {
if (isButtonActive(editor)) {
unwrapButton(editor)
}

const { selection } = editor
const isCollapsed = selection && Range.isCollapsed(selection)
const button: ButtonElement = {
type: 'button',
children: isCollapsed ? [{ text: 'Edit me!' }] : [],
}

if (isCollapsed) {
Transforms.insertNodes(editor, button)
} else {
Transforms.wrapNodes(editor, button, { split: true })
Transforms.collapse(editor, { edge: 'end' })
}
}

const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{
text:
'In addition to block nodes, you can create inline nodes. Here is a ',
},
{
type: 'link',
url: 'https://en.wikipedia.org/wiki/Hypertext',
children: [{ text: 'hyperlink' }],
},
{
text: ', and here is a more unusual inline: an ',
},
{
type: 'button',
children: [{ text: 'editable button' }],
},
{
text: '!',
},
],
},
{
type: 'paragraph',
children: [
{
text:
'There are two ways to add links. You can either add a link via the toolbar icon above, or if you want in on a little secret, copy a URL to your keyboard and paste it while a range of text is selected. ',
},
// The following is an example of an inline at the end of a block.
// This is an edge case that can cause issues.
{
type: 'link',
url: 'https://twitter.com/JustMissEmma/status/1448679899531726852',
children: [{ text: 'Finally, here is our favorite dog video.' }],
},
{ text: '' },
],
},
]
Loading

0 comments on commit 0390117

Please sign in to comment.