├── .changeset
├── README.md
└── config.json
├── .circleci
└── config.yml
├── .coveralls.yml
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── Dockerfile
├── LICENSE
├── README.md
├── README.zh-CN.md
├── angular.json
├── commitlint.config.js
├── custom-types.d.ts
├── demo
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── components
│ │ ├── button
│ │ │ └── button.component.ts
│ │ ├── editable-button
│ │ │ └── editable-button.component.ts
│ │ ├── editable-void
│ │ │ ├── editable-void.component.html
│ │ │ ├── editable-void.component.scss
│ │ │ └── editable-void.component.ts
│ │ ├── image
│ │ │ └── image-component.ts
│ │ ├── link
│ │ │ └── link.component.ts
│ │ ├── text
│ │ │ └── text.component.ts
│ │ └── video
│ │ │ ├── video.component.html
│ │ │ ├── video.component.scss
│ │ │ └── video.component.ts
│ ├── editable-voids
│ │ ├── editable-voids.component.html
│ │ ├── editable-voids.component.scss
│ │ └── editable-voids.component.ts
│ ├── embeds
│ │ ├── embeds.component.html
│ │ ├── embeds.component.scss
│ │ └── embeds.component.ts
│ ├── huge-document
│ │ ├── huge-document.component.html
│ │ └── huge-document.component.ts
│ ├── images
│ │ ├── images.component.html
│ │ └── images.component.ts
│ ├── inlines
│ │ ├── inlines.component.html
│ │ └── inlines.component.ts
│ ├── markdown-shorcuts
│ │ ├── markdown-shortcuts.component.html
│ │ └── markdown-shortcuts.component.ts
│ ├── mentions
│ │ ├── mentions.component.html
│ │ ├── mentions.component.scss
│ │ └── mentions.component.ts
│ ├── placeholder
│ │ └── placeholder.component.ts
│ ├── plugins
│ │ └── block-cards.plugin.ts
│ ├── readonly
│ │ └── readonly.component.ts
│ ├── richtext
│ │ ├── richtext.component.html
│ │ └── richtext.component.ts
│ ├── search-highlighting
│ │ ├── leaf.component.ts
│ │ ├── search-highlighting.component.html
│ │ ├── search-highlighting.component.scss
│ │ └── search-highlighting.component.ts
│ └── tables
│ │ ├── tables.component.html
│ │ └── tables.component.ts
├── assets
│ ├── logo.svg
│ ├── materialicons
│ │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2
│ │ └── icon.css
│ ├── photo.jpeg
│ └── text.svg
├── editor-typo.scss
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
└── tsconfig.spec.json
├── docs
├── compatible.md
└── images
│ └── banner.jpeg
├── e2e
├── protractor.conf.js
├── src
│ ├── app.e2e-spec.ts
│ └── app.po.ts
└── tsconfig.json
├── karma.conf.js
├── nginx.conf
├── package-lock.json
├── package.json
├── packages
├── CHANGELOG.md
├── README.md
├── karma.conf.js
├── ng-package.json
├── package.json
├── src
│ ├── components
│ │ ├── block-card
│ │ │ ├── block-card.component.html
│ │ │ ├── block-card.component.scss
│ │ │ ├── block-card.component.spec.ts
│ │ │ └── block-card.component.ts
│ │ ├── children
│ │ │ ├── children-outlet.component.ts
│ │ │ └── children.component.ts
│ │ ├── editable
│ │ │ ├── editable.component.html
│ │ │ ├── editable.component.spec.ts
│ │ │ └── editable.component.ts
│ │ ├── element
│ │ │ ├── default-element.component.token.ts
│ │ │ ├── default-element.component.ts
│ │ │ └── element.component.ts
│ │ ├── leaf
│ │ │ ├── default-leaf.component.ts
│ │ │ └── token.ts
│ │ ├── leaves
│ │ │ └── leaves.component.ts
│ │ ├── string
│ │ │ ├── default-string.component.ts
│ │ │ ├── string.component.spec.ts
│ │ │ ├── string.component.ts
│ │ │ ├── template.component.html
│ │ │ └── template.component.ts
│ │ └── text
│ │ │ ├── default-text.component.ts
│ │ │ ├── token.ts
│ │ │ └── void-text.component.ts
│ ├── custom-event
│ │ ├── BeforeInputEventPlugin.ts
│ │ ├── DOMTopLevelEventTypes.ts
│ │ ├── FallbackCompositionState.ts
│ │ └── before-input-polyfill.ts
│ ├── module.ts
│ ├── plugins
│ │ ├── angular-editor.spec.ts
│ │ ├── angular-editor.ts
│ │ ├── with-angular.spec.ts
│ │ └── with-angular.ts
│ ├── public-api.ts
│ ├── styles
│ │ └── index.scss
│ ├── test.ts
│ ├── testing
│ │ ├── advanced-editable.component.ts
│ │ ├── basic-editable.component.ts
│ │ ├── create-document.ts
│ │ ├── dispatcher-events.ts
│ │ ├── editable-with-outlet.component.ts
│ │ ├── element-focus.ts
│ │ ├── events.ts
│ │ ├── image-editable.component.ts
│ │ ├── index.ts
│ │ ├── leaf.component.ts
│ │ ├── module.ts
│ │ └── types.ts
│ ├── types
│ │ ├── clipboard.ts
│ │ ├── error.ts
│ │ ├── feature.ts
│ │ ├── index.ts
│ │ └── view.ts
│ ├── utils
│ │ ├── block-card.ts
│ │ ├── clipboard
│ │ │ ├── clipboard.ts
│ │ │ ├── common.ts
│ │ │ ├── data-transfer.ts
│ │ │ ├── index.ts
│ │ │ └── navigator-clipboard.ts
│ │ ├── constants.ts
│ │ ├── dom.ts
│ │ ├── environment.ts
│ │ ├── global-normalize.spec.ts
│ │ ├── global-normalize.ts
│ │ ├── hotkeys.ts
│ │ ├── index.ts
│ │ ├── key.ts
│ │ ├── lines.ts
│ │ ├── range-list.ts
│ │ ├── restore-dom.ts
│ │ ├── throttle.ts
│ │ ├── view.ts
│ │ └── weak-maps.ts
│ └── view
│ │ ├── base.ts
│ │ ├── container-item.ts
│ │ ├── container.ts
│ │ ├── context-change.ts
│ │ ├── context.ts
│ │ ├── render
│ │ ├── leaves-render.ts
│ │ ├── list-render.spec.ts
│ │ ├── list-render.ts
│ │ └── utils.ts
│ │ └── types.ts
├── tsconfig.lib.json
├── tsconfig.lib.prod.json
└── tsconfig.spec.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── types
└── custom-types.d.ts
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | {
6 | "repo": "worktile/slate-angular"
7 | }
8 | ],
9 | "commit": false,
10 | "linked": [],
11 | "access": "public",
12 | "baseBranch": "master",
13 | "updateInternalDependencies": "patch",
14 | "ignore": []
15 | }
16 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | browser-tools: circleci/browser-tools@1.4.3
4 | jobs:
5 | build:
6 | working_directory: ~/slate-angular
7 | docker:
8 | - image: cimg/node:20.16.0-browsers
9 | steps:
10 | - browser-tools/install-chrome
11 | - checkout
12 | - run: |
13 | node --version
14 | google-chrome --version
15 | which google-chrome
16 | - restore_cache:
17 | key: slate-angular-{{ .Branch }}-{{ checksum "package-lock.json" }}
18 | - run: npm ci --force
19 | - save_cache:
20 | key: slate-angular-{{ .Branch }}-{{ checksum "package-lock.json" }}
21 | paths:
22 | - 'node_modules'
23 | - run: npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
24 | - run: npm run report-coverage
25 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: 5MUHe1rNtCfuUbqtr7ebinZSzCpYGc5nq
2 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.css]
12 | charset = utf-8
13 | indent_style = space
14 | indent_size = 2
15 | insert_final_newline = true
16 | trim_trailing_whitespace = true
17 |
18 | [*.json]
19 | indent_size = 2
20 |
21 | [*.html]
22 | indent_size = 2
23 |
24 | [*.md]
25 | max_line_length = off
26 | trim_trailing_whitespace = false
27 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["projects/**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "parserOptions": {
8 | "project": ["tsconfig.json", "e2e/tsconfig.json"],
9 | "createDefaultProgram": true
10 | },
11 | "extends": ["plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
12 | "rules": {
13 | "@angular-eslint/directive-selector": [
14 | "error",
15 | {
16 | "prefix": "slate",
17 | "style": "camelCase",
18 | "type": "attribute"
19 | }
20 | ],
21 | "@angular-eslint/component-class-suffix": 0,
22 | "@angular-eslint/component-selector": 0,
23 | "@angular-eslint/no-empty-lifecycle-method": 0,
24 | "@angular-eslint/no-host-metadata-property": 0,
25 | "@angular-eslint/no-output-on-prefix": 0,
26 | "@angular-eslint/no-conflicting-lifecycle": 0,
27 | "@angular-eslint/prefer-standalone": "off"
28 | }
29 | },
30 | {
31 | "files": ["*.html"],
32 | "extends": ["plugin:@angular-eslint/template/recommended"],
33 | "rules": {}
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-demo
6 | /tmp
7 | /out-tsc
8 | # Only exists if Bazel was run
9 | /bazel-out
10 |
11 | # dependencies
12 | /node_modules
13 |
14 | # profiling files
15 | chrome-profiler-events*.json
16 | speed-measure-plugin*.json
17 |
18 | # IDEs and editors
19 | /.idea
20 | .project
21 | .classpath
22 | .c9/
23 | *.launch
24 | .settings/
25 | *.sublime-workspace
26 |
27 | # IDE - VSCode
28 | .vscode/*
29 | !.vscode/settings.json
30 | !.vscode/tasks.json
31 | !.vscode/launch.json
32 | !.vscode/extensions.json
33 | .history/*
34 |
35 | # misc
36 | /.angular/cache
37 | /.sass-cache
38 | /connect.lock
39 | /coverage
40 | /libpeerconnection.log
41 | npm-debug.log
42 | yarn-error.log
43 | testem.log
44 | /typings
45 |
46 | # System Files
47 | .DS_Store
48 | Thumbs.db
49 |
50 | # Demo
51 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": true,
4 | "arrowParens": "avoid",
5 | "trailingComma": "none",
6 | "printWidth": 140
7 | }
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:mainline-alpine
2 | RUN rm /etc/nginx/conf.d/*
3 | ADD nginx.conf /etc/nginx/conf.d/
4 | COPY dist-demo /etc/nginx/html
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2018 - 2021 Worktile Inc. https://worktile.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "packages",
5 | "projects": {
6 | "demo": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "demo",
15 | "prefix": "demo",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:application",
19 | "options": {
20 | "outputPath": {
21 | "base": "dist-demo"
22 | },
23 | "index": "demo/index.html",
24 | "polyfills": [
25 | "demo/polyfills.ts"
26 | ],
27 | "tsConfig": "tsconfig.app.json",
28 | "assets": [
29 | {
30 | "glob": "favicon.ico",
31 | "input": "demo",
32 | "output": "/"
33 | },
34 | {
35 | "glob": "**/*",
36 | "input": "demo/assets",
37 | "output": "/assets"
38 | }
39 | ],
40 | "styles": ["demo/styles.scss"],
41 | "scripts": [],
42 | "allowedCommonJsDependencies": [
43 | "lodash",
44 | "debug",
45 | "esrever",
46 | "unified",
47 | "remark-parse",
48 | "remark-slate",
49 | "rxjs/internal/scheduler/asap",
50 | "faker"
51 | ],
52 | "extractLicenses": false,
53 | "sourceMap": true,
54 | "optimization": false,
55 | "namedChunks": true,
56 | "browser": "demo/main.ts"
57 | },
58 | "configurations": {
59 | "production": {
60 | "fileReplacements": [
61 | {
62 | "replace": "demo/environments/environment.ts",
63 | "with": "demo/environments/environment.prod.ts"
64 | }
65 | ],
66 | "optimization": true,
67 | "outputHashing": "all",
68 | "sourceMap": false,
69 | "namedChunks": false,
70 | "extractLicenses": true,
71 | "budgets": [
72 | {
73 | "type": "initial",
74 | "maximumWarning": "2mb",
75 | "maximumError": "5mb"
76 | },
77 | {
78 | "type": "anyComponentStyle",
79 | "maximumWarning": "6kb",
80 | "maximumError": "10kb"
81 | }
82 | ]
83 | }
84 | },
85 | "defaultConfiguration": ""
86 | },
87 | "serve": {
88 | "builder": "@angular-devkit/build-angular:dev-server",
89 | "options": {
90 | "port": 8000,
91 | "buildTarget": "demo:build"
92 | },
93 | "configurations": {
94 | "production": {
95 | "buildTarget": "demo:build:production"
96 | }
97 | }
98 | },
99 | "extract-i18n": {
100 | "builder": "@angular-devkit/build-angular:extract-i18n",
101 | "options": {
102 | "buildTarget": "demo:build"
103 | }
104 | },
105 | "test": {
106 | "builder": "@angular-devkit/build-angular:karma",
107 | "options": {
108 | "main": "demo/test.ts",
109 | "polyfills": "demo/polyfills.ts",
110 | "tsConfig": "tsconfig.spec.json",
111 | "karmaConfig": "karma.conf.js",
112 | "assets": ["demo/favicon.ico", "demo/assets"],
113 | "styles": ["demo/styles.scss"],
114 | "scripts": []
115 | }
116 | },
117 | "lint": {
118 | "builder": "@angular-eslint/builder:lint",
119 | "options": {
120 | "lintFilePatterns": ["demo/**/*.ts", "demo/**/*.html"]
121 | }
122 | },
123 | "e2e": {
124 | "builder": "@angular-devkit/build-angular:protractor",
125 | "options": {
126 | "protractorConfig": "e2e/protractor.conf.js",
127 | "devServerTarget": "demo:serve"
128 | },
129 | "configurations": {
130 | "production": {
131 | "devServerTarget": "demo:serve:production"
132 | }
133 | }
134 | }
135 | }
136 | },
137 | "slate-angular": {
138 | "projectType": "library",
139 | "root": "packages",
140 | "sourceRoot": "packages/src",
141 | "prefix": "slate",
142 | "architect": {
143 | "build": {
144 | "builder": "@angular-devkit/build-angular:ng-packagr",
145 | "options": {
146 | "tsConfig": "packages/tsconfig.lib.json",
147 | "project": "packages/ng-package.json"
148 | },
149 | "configurations": {
150 | "production": {
151 | "tsConfig": "packages/tsconfig.lib.prod.json"
152 | }
153 | }
154 | },
155 | "test": {
156 | "builder": "@angular-devkit/build-angular:karma",
157 | "options": {
158 | "main": "packages/src/test.ts",
159 | "tsConfig": "packages/tsconfig.spec.json",
160 | "karmaConfig": "packages/karma.conf.js",
161 | "codeCoverage": true,
162 | "codeCoverageExclude": ["packages/src/testing/**/*"]
163 | }
164 | },
165 | "lint": {
166 | "builder": "@angular-eslint/builder:lint",
167 | "options": {
168 | "lintFilePatterns": ["packages/**/*.ts", "packages/**/*.html"]
169 | }
170 | }
171 | }
172 | }
173 | },
174 | "cli": {
175 | "analytics": "8db354c6-b560-4f8e-9ad8-56c64f03d2c8",
176 | "schematicCollections": ["@angular-eslint/schematics"]
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'header-max-length': [2, 'always', 200],
5 | 'scope-empty': [2, 'never'],
6 | 'scope-enum': [
7 | 2,
8 | 'always',
9 | [
10 | 'demo',
11 | 'deps',
12 | 'core',
13 | 'view',
14 | 'release',
15 | 'plugin',
16 | 'config',
17 | 'template',
18 | 'browser',
19 | 'docs',
20 | 'decorate',
21 | 'pretter',
22 | 'types',
23 | 'block-card',
24 | 'global-normalize',
25 | 'placeholder'
26 | ]
27 | ]
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/custom-types.d.ts:
--------------------------------------------------------------------------------
1 | import { Descendant, BaseEditor } from 'slate';
2 | import { AngularEditor } from 'slate-angular';
3 |
4 | export type BlockQuoteElement = { type: 'block-quote'; children: Descendant[] };
5 |
6 | export type BulletedListElement = {
7 | type: 'bulleted-list';
8 | children: Descendant[];
9 | };
10 |
11 | export type NumberedListElement = {
12 | type: 'numbered-list';
13 | children: Descendant[];
14 | };
15 |
16 | export type CheckListItemElement = {
17 | type: 'check-list-item';
18 | checked: boolean;
19 | children: Descendant[];
20 | };
21 |
22 | export type EditableVoidElement = {
23 | type: 'editable-void';
24 | children: EmptyText[];
25 | };
26 |
27 | export type HeadingOneElement = { type: 'heading-one'; children: Descendant[] };
28 | export type HeadingTwoElement = { type: 'heading-two'; children: Descendant[] };
29 | export type HeadingThreeElement = {
30 | type: 'heading-three';
31 | children: Descendant[];
32 | };
33 | export type HeadingFourElement = {
34 | type: 'heading-four';
35 | children: Descendant[];
36 | };
37 | export type HeadingFiveElement = {
38 | type: 'heading-five';
39 | children: Descendant[];
40 | };
41 | export type HeadingSixElement = { type: 'heading-six'; children: Descendant[] };
42 |
43 | export type ImageElement = {
44 | type: 'image';
45 | url: string;
46 | children: EmptyText[];
47 | };
48 |
49 | export type LinkElement = { type: 'link'; url: string; children: Descendant[] };
50 |
51 | export type ButtonElement = { type: 'button'; children: Descendant[] };
52 |
53 | export type ListItemElement = { type: 'list-item'; children: Descendant[] };
54 |
55 | export type MentionElement = {
56 | type: 'mention';
57 | character: string;
58 | children: CustomText[];
59 | };
60 |
61 | export type ParagraphElement = { type: 'paragraph'; children: Descendant[] };
62 |
63 | export type TableElement = { type: 'table'; children: TableRowElement[] };
64 |
65 | export type TableCellElement = { type: 'table-cell'; children: Descendant[] };
66 |
67 | export type TableRowElement = {
68 | type: 'table-row';
69 | children: TableCellElement[];
70 | };
71 |
72 | export type TitleElement = { type: 'title'; children: Descendant[] };
73 |
74 | export type VideoElement = {
75 | type: 'video';
76 | url: string;
77 | children: EmptyText[];
78 | };
79 |
80 | type CustomElement =
81 | | BlockQuoteElement
82 | | NumberedListElement
83 | | BulletedListElement
84 | | CheckListItemElement
85 | | EditableVoidElement
86 | | HeadingOneElement
87 | | HeadingTwoElement
88 | | HeadingThreeElement
89 | | HeadingFourElement
90 | | HeadingFiveElement
91 | | HeadingSixElement
92 | | ImageElement
93 | | LinkElement
94 | | ListItemElement
95 | | MentionElement
96 | | ParagraphElement
97 | | TableElement
98 | | TableRowElement
99 | | TableCellElement
100 | | TitleElement
101 | | VideoElement
102 | | ButtonElement;
103 |
104 | export type CustomText = {
105 | placeholder?: string;
106 | bold?: boolean;
107 | italic?: boolean;
108 | code?: boolean;
109 | text: string;
110 | 'code-line'?: boolean;
111 | };
112 |
113 | export type EmptyText = {
114 | text: string;
115 | };
116 |
117 | export type CustomEditor = BaseEditor & AngularEditor;
118 |
119 | declare module 'slate' {
120 | interface CustomTypes {
121 | Editor: CustomEditor;
122 | Element: CustomElement;
123 | Text: CustomText | EmptyText;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/demo/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { Routes, RouterModule } from '@angular/router';
3 | import { DemoRichtextComponent } from './richtext/richtext.component';
4 | import { DemoHugeDocumentComponent } from './huge-document/huge-document.component';
5 | import { DemoMarkdownShortcutsComponent } from './markdown-shorcuts/markdown-shortcuts.component';
6 | import { DemoTablesComponent } from './tables/tables.component';
7 | import { DemoImagesComponent } from './images/images.component';
8 | import { DemoSearchHighlightingComponent } from './search-highlighting/search-highlighting.component';
9 | import { DemoMentionsComponent } from './mentions/mentions.component';
10 | import { DemoReadonlyComponent } from './readonly/readonly.component';
11 | import { DemoPlaceholderComponent } from './placeholder/placeholder.component';
12 | import { DemoInlinesComponent } from './inlines/inlines.component';
13 | import { DemoEditableVoidsComponent } from './editable-voids/editable-voids.component';
14 | import { DemoEmbedsComponent } from './embeds/embeds.component';
15 |
16 | const routes: Routes = [
17 | {
18 | path: 'readonly',
19 | component: DemoReadonlyComponent
20 | },
21 | {
22 | path: '',
23 | component: DemoRichtextComponent
24 | },
25 | {
26 | path: 'huge-document',
27 | component: DemoHugeDocumentComponent
28 | },
29 | {
30 | path: 'markdown-shortcuts',
31 | component: DemoMarkdownShortcutsComponent
32 | },
33 | {
34 | path: 'tables',
35 | component: DemoTablesComponent
36 | },
37 | {
38 | path: 'images',
39 | component: DemoImagesComponent
40 | },
41 | {
42 | path: 'inlines',
43 | component: DemoInlinesComponent
44 | },
45 | {
46 | path: 'search-highlighting',
47 | component: DemoSearchHighlightingComponent
48 | },
49 | {
50 | path: 'mentions',
51 | component: DemoMentionsComponent
52 | },
53 | {
54 | path: 'placeholder',
55 | component: DemoPlaceholderComponent
56 | },
57 | {
58 | path: 'editable-voids',
59 | component: DemoEditableVoidsComponent
60 | },
61 | {
62 | path: 'embeds',
63 | component: DemoEmbedsComponent
64 | }
65 | ];
66 | @NgModule({
67 | imports: [
68 | RouterModule.forRoot(routes, {
69 | useHash: false
70 | })
71 | ],
72 | exports: [RouterModule]
73 | })
74 | export class AppRoutingModule {}
75 |
--------------------------------------------------------------------------------
/demo/app/app.component.html:
--------------------------------------------------------------------------------
1 |
13 |
17 |
18 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/demo/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, async } from '@angular/core/testing';
2 | import { RouterTestingModule } from '@angular/router/testing';
3 | import { AppComponent } from './app.component';
4 |
5 | describe('AppComponent', () => {
6 | beforeEach(async(() => {
7 | TestBed.configureTestingModule({
8 | imports: [RouterTestingModule],
9 | declarations: [AppComponent]
10 | }).compileComponents();
11 | }));
12 |
13 | it('should create the app', () => {
14 | const fixture = TestBed.createComponent(AppComponent);
15 | const app = fixture.componentInstance;
16 | expect(app).toBeTruthy();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/demo/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
2 | @Component({
3 | selector: 'demo-app-root',
4 | templateUrl: './app.component.html',
5 | providers: [],
6 | standalone: false
7 | })
8 | export class AppComponent implements OnInit {
9 | menus: Array<{ url: string; name: string }> = [
10 | {
11 | url: '/readonly',
12 | name: 'Readonly'
13 | },
14 | {
15 | url: '/',
16 | name: 'RichText'
17 | },
18 | {
19 | url: '/huge-document',
20 | name: 'Huge Document'
21 | },
22 | {
23 | url: '/markdown-shortcuts',
24 | name: 'Markdown Shortcuts'
25 | },
26 | {
27 | url: '/mentions',
28 | name: 'Mentions'
29 | },
30 | {
31 | url: '/tables',
32 | name: 'Tables'
33 | },
34 | {
35 | url: '/images',
36 | name: 'Images'
37 | },
38 | {
39 | url: '/inlines',
40 | name: 'Inlines'
41 | },
42 | {
43 | url: '/search-highlighting',
44 | name: 'Search Highlighting'
45 | },
46 | {
47 | url: '/placeholder',
48 | name: 'Placeholder'
49 | },
50 | {
51 | url: '/editable-voids',
52 | name: 'Editable voids'
53 | },
54 | {
55 | url: '/embeds',
56 | name: 'Embeds'
57 | }
58 | ];
59 |
60 | showSideNav: boolean;
61 |
62 | get activeNav() {
63 | return this.menus.filter(item => window.location.href.endsWith(item.url))[0];
64 | }
65 |
66 | @ViewChild('sideNav', { static: false }) sideNav: ElementRef;
67 |
68 | isSelected(item) {
69 | return window.location.href.endsWith(item.url);
70 | }
71 |
72 | onBreadClick() {
73 | this.showSideNav = !this.showSideNav;
74 | }
75 |
76 | ngOnInit(): void {}
77 | }
78 |
--------------------------------------------------------------------------------
/demo/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { AppRoutingModule } from './app-routing.module';
2 | import { AppComponent } from './app.component';
3 | import { DemoMarkdownShortcutsComponent } from './markdown-shorcuts/markdown-shortcuts.component';
4 | import { DemoRichtextComponent } from './richtext/richtext.component';
5 | import { DemoHugeDocumentComponent } from './huge-document/huge-document.component';
6 | import { FormsModule } from '@angular/forms';
7 | import { BrowserModule } from '@angular/platform-browser';
8 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
9 | import { NgModule } from '@angular/core';
10 | import { SlateModule } from 'slate-angular';
11 | import { DemoElementImageComponent } from './components/image/image-component';
12 | import { DemoTextMarkComponent } from './components/text/text.component';
13 | import { DemoButtonComponent } from './components/button/button.component';
14 | import { DemoTablesComponent } from './tables/tables.component';
15 | import { DemoImagesComponent } from './images/images.component';
16 | import { DemoSearchHighlightingComponent } from './search-highlighting/search-highlighting.component';
17 | import { DemoLeafComponent } from './search-highlighting/leaf.component';
18 | import { DemoMentionsComponent } from './mentions/mentions.component';
19 | import { DemoReadonlyComponent } from './readonly/readonly.component';
20 | import { DemoPlaceholderComponent } from './placeholder/placeholder.component';
21 | import { DemoElementEditableButtonComponent } from './components/editable-button/editable-button.component';
22 | import { DemoInlinesComponent } from './inlines/inlines.component';
23 | import { DemoElementLinkComponent } from './components/link/link.component';
24 | import { DemoEditableVoidsComponent } from './editable-voids/editable-voids.component';
25 | import { DemoEmbedsComponent } from './embeds/embeds.component';
26 |
27 | @NgModule({
28 | declarations: [AppComponent],
29 | imports: [
30 | BrowserModule,
31 | BrowserAnimationsModule,
32 | AppRoutingModule,
33 | FormsModule,
34 | SlateModule,
35 | DemoButtonComponent,
36 | DemoRichtextComponent,
37 | DemoMarkdownShortcutsComponent,
38 | DemoHugeDocumentComponent,
39 | DemoElementImageComponent,
40 | DemoTextMarkComponent,
41 | DemoTablesComponent,
42 | DemoTablesComponent,
43 | DemoImagesComponent,
44 | DemoSearchHighlightingComponent,
45 | DemoLeafComponent,
46 | DemoMentionsComponent,
47 | DemoReadonlyComponent,
48 | DemoPlaceholderComponent,
49 | DemoElementEditableButtonComponent,
50 | DemoInlinesComponent,
51 | DemoElementLinkComponent,
52 | DemoEditableVoidsComponent,
53 | DemoEmbedsComponent
54 | ],
55 | providers: [],
56 | bootstrap: [AppComponent]
57 | })
58 | export class AppModule {
59 | constructor() {}
60 | }
61 |
--------------------------------------------------------------------------------
/demo/app/components/button/button.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | Directive,
4 | ElementRef,
5 | EventEmitter,
6 | HostBinding,
7 | HostListener,
8 | Input,
9 | OnChanges,
10 | Output,
11 | Renderer2
12 | } from '@angular/core';
13 |
14 | @Component({
15 | selector: 'demo-button',
16 | template: '',
17 | host: {
18 | style: 'cursor: pointer'
19 | },
20 | standalone: true
21 | })
22 | export class DemoButtonComponent implements OnChanges {
23 | @Input() active = false;
24 |
25 | @Output() onMouseDown: EventEmitter = new EventEmitter();
26 |
27 | constructor(
28 | private elementRef: ElementRef,
29 | private renderer2: Renderer2
30 | ) {}
31 |
32 | @HostListener('mousedown', ['$event'])
33 | mousedown(event: MouseEvent) {
34 | event.preventDefault();
35 | this.onMouseDown.emit(event);
36 | }
37 |
38 | ngOnChanges() {
39 | this.renderer2.setStyle(this.elementRef.nativeElement, 'color', this.active ? 'black' : '#ccc');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/demo/app/components/editable-button/editable-button.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
2 | import { ButtonElement } from '../../../../custom-types';
3 | import { BaseElementComponent } from 'slate-angular';
4 | import { SlateChildrenOutlet } from '../../../../packages/src/components/children/children-outlet.component';
5 |
6 | @Component({
7 | selector: 'span[demo-element-button]',
8 | template: `
9 | {{ inlineChromiumBugfix }}
10 |
11 | {{ inlineChromiumBugfix }}
12 | `,
13 | host: {
14 | class: 'demo-element-button'
15 | },
16 | changeDetection: ChangeDetectionStrategy.OnPush,
17 | imports: [SlateChildrenOutlet]
18 | })
19 | export class DemoElementEditableButtonComponent extends BaseElementComponent {
20 | // Put this at the start and end of an inline component to work around this Chromium bug:
21 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
22 | inlineChromiumBugfix = '$' + String.fromCodePoint(160);
23 |
24 | @HostListener('click', ['$event'])
25 | click(event: MouseEvent) {
26 | event.preventDefault();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/demo/app/components/editable-void/editable-void.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Name:
4 |
5 |
6 |
7 |
Left or right handed
8 |
12 |
13 |
17 |
Tell us about yourself:
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/demo/app/components/editable-void/editable-void.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | .box {
3 | box-shadow: 0 0 0 3px;
4 | padding: 8px;
5 | }
6 |
7 | .input {
8 | margin: 8px 0;
9 |
10 | color: #ccc;
11 | border: 2px solid #ccc;
12 | border-radius: 4px;
13 |
14 | input {
15 | border: none;
16 | outline: none;
17 | background-color: transparent;
18 | }
19 | }
20 |
21 | .unsetWidth {
22 | width: unset;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo/app/components/editable-void/editable-void.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 |
3 | import { DemoRichtextComponent } from 'demo/app/richtext/richtext.component';
4 | import { BaseElementComponent } from 'slate-angular';
5 | import { EditableVoidElement } from 'custom-types';
6 |
7 | @Component({
8 | selector: 'demo-editable-void',
9 | imports: [DemoRichtextComponent],
10 | templateUrl: './editable-void.component.html',
11 | styleUrls: ['./editable-void.component.scss'],
12 | changeDetection: ChangeDetectionStrategy.OnPush
13 | })
14 | export class DemoElementEditableVoid extends BaseElementComponent {
15 | inputValue: string = '';
16 |
17 | setInputValue(event: Event) {
18 | this.inputValue = (event.target as HTMLInputElement).value;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/demo/app/components/image/image-component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { ImageElement } from '../../../../custom-types';
3 | import { BaseElementComponent } from 'slate-angular';
4 |
5 | @Component({
6 | selector: 'demo-element-image',
7 | template: `
`,
8 | host: {
9 | class: 'demo-element-image'
10 | }
11 | })
12 | export class DemoElementImageComponent extends BaseElementComponent {}
13 |
--------------------------------------------------------------------------------
/demo/app/components/link/link.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, HostBinding, HostListener } from '@angular/core';
2 | import { LinkElement } from 'custom-types';
3 | import { BaseElementComponent } from 'slate-angular';
4 | import { SlateChildrenOutlet } from '../../../../packages/src/components/children/children-outlet.component';
5 |
6 | @Component({
7 | selector: 'a[demo-element-link]',
8 | template: `
9 | {{ inlineChromiumBugfix }}
10 |
11 | {{ inlineChromiumBugfix }}
12 | `,
13 | changeDetection: ChangeDetectionStrategy.OnPush,
14 | imports: [SlateChildrenOutlet]
15 | })
16 | export class DemoElementLinkComponent extends BaseElementComponent {
17 | // Put this at the start and end of an inline component to work around this Chromium bug:
18 | // https://bugs.chromium.org/p/chromium/issues/detail?id=1249405
19 | inlineChromiumBugfix = '$' + String.fromCodePoint(160);
20 |
21 | @HostBinding('class.demo-element-link-active')
22 | get active() {
23 | return this.isCollapsed;
24 | }
25 |
26 | @HostBinding('attr.href')
27 | get herf() {
28 | return this.element.url;
29 | }
30 |
31 | @HostListener('click', ['$event'])
32 | click(event: MouseEvent) {
33 | event.preventDefault();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/demo/app/components/text/text.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, Renderer2 } from '@angular/core';
2 | import { BaseTextComponent } from 'slate-angular';
3 |
4 | export enum MarkTypes {
5 | bold = 'bold',
6 | italic = 'italic',
7 | underline = 'underlined',
8 | strike = 'strike',
9 | code = 'code-line'
10 | }
11 |
12 | @Component({
13 | selector: 'span[textMark]',
14 | template: ``,
15 | host: {
16 | 'data-slate-node': 'text'
17 | }
18 | })
19 | export class DemoTextMarkComponent extends BaseTextComponent {
20 | attributes = [];
21 |
22 | public renderer2 = inject(Renderer2);
23 |
24 | applyTextMark() {
25 | this.attributes.forEach(attr => {
26 | this.renderer2.removeAttribute(this.elementRef.nativeElement, attr);
27 | });
28 | this.attributes = [];
29 | for (const key in this.text) {
30 | if (Object.prototype.hasOwnProperty.call(this.text, key) && key !== 'text' && !!this.text[key]) {
31 | const attr = `slate-${key}`;
32 | this.renderer2.setAttribute(this.elementRef.nativeElement, attr, 'true');
33 | this.attributes.push(attr);
34 | }
35 | }
36 | }
37 |
38 | onContextChange() {
39 | super.onContextChange();
40 | this.applyTextMark();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/demo/app/components/video/video.component.html:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/demo/app/components/video/video.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/demo/app/components/video/video.component.scss
--------------------------------------------------------------------------------
/demo/app/components/video/video.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject } from '@angular/core';
2 | import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
3 | import { VideoElement } from 'custom-types';
4 | import { Element as SlateElement, Transforms } from 'slate';
5 | import { AngularEditor, BaseElementComponent } from 'slate-angular';
6 |
7 | @Component({
8 | selector: 'demo-video',
9 | templateUrl: './video.component.html',
10 | styleUrls: ['./video.component.scss']
11 | })
12 | export class DemoElementVideoComponent extends BaseElementComponent {
13 | private sanitizer = inject(DomSanitizer);
14 |
15 | get url(): SafeUrl {
16 | return this.element.url ? this.sanitizer.bypassSecurityTrustResourceUrl(this.element.url + '?title=0&byline=0&portrait=0') : '';
17 | }
18 |
19 | inputChange(event: Event) {
20 | const newUrl = (event.target as HTMLInputElement).value;
21 | const path = AngularEditor.findPath(this.editor, this.element);
22 | const newProperties: Partial = {
23 | url: newUrl
24 | };
25 | Transforms.setNodes(this.editor, newProperties, {
26 | at: path
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo/app/editable-voids/editable-voids.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | @for (toolbarItem of toolbarItems; track $index) {
4 | {{ toolbarItem.icon }}
7 | }
8 |
9 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/demo/app/editable-voids/editable-voids.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | [data-slate-editor] > * + * {
3 | margin-top: 1em;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/demo/app/editable-voids/editable-voids.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { Descendant, Editor, Element as SlateElement, Text, Transforms, createEditor } from 'slate';
4 | import { SlateEditable, withAngular } from 'slate-angular';
5 | import { withHistory } from 'slate-history';
6 | import { DemoButtonComponent } from '../components/button/button.component';
7 |
8 | import { EditableVoidElement } from 'custom-types';
9 | import { DemoElementEditableVoid } from '../components/editable-void/editable-void.component';
10 |
11 | interface ToolbarItem {
12 | icon: string;
13 | active: () => boolean;
14 | action: (event: Event) => void;
15 | }
16 |
17 | @Component({
18 | selector: 'demo-editable-voids',
19 | templateUrl: './editable-voids.component.html',
20 | styleUrls: ['./editable-voids.component.scss'],
21 | imports: [SlateEditable, FormsModule, DemoButtonComponent]
22 | })
23 | export class DemoEditableVoidsComponent {
24 | value = initialValue;
25 |
26 | editor = withEditableVoids(withHistory(withAngular(createEditor())));
27 |
28 | toolbarItems: Array = [
29 | {
30 | icon: 'add',
31 | active: () => true,
32 | action: event => {
33 | event.preventDefault();
34 | const text: Text = { text: '' };
35 | const voidNode: EditableVoidElement = {
36 | type: 'editable-void',
37 | children: [text]
38 | };
39 | Transforms.insertNodes(this.editor, voidNode);
40 | }
41 | }
42 | ];
43 |
44 | renderElement = (element: SlateElement) => {
45 | if (element.type === 'editable-void') {
46 | return DemoElementEditableVoid;
47 | }
48 | return null;
49 | };
50 | }
51 |
52 | const withEditableVoids = (editor: Editor) => {
53 | const { isVoid } = editor;
54 |
55 | editor.isVoid = element => {
56 | return element.type === 'editable-void' || isVoid(element);
57 | };
58 |
59 | return editor;
60 | };
61 |
62 | const initialValue: Descendant[] = [
63 | {
64 | type: 'paragraph',
65 | children: [
66 | {
67 | text: 'In addition to nodes that contain editable text, you can insert void nodes, which can also contain editable elements, inputs, or an entire other Slate editor.'
68 | }
69 | ]
70 | },
71 | {
72 | type: 'editable-void',
73 | children: [{ text: '' }]
74 | },
75 | {
76 | type: 'paragraph',
77 | children: [
78 | {
79 | text: ''
80 | }
81 | ]
82 | }
83 | ];
84 |
--------------------------------------------------------------------------------
/demo/app/embeds/embeds.component.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/app/embeds/embeds.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/demo/app/embeds/embeds.component.scss
--------------------------------------------------------------------------------
/demo/app/embeds/embeds.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { Descendant, Editor, createEditor, Element as SlateElement } from 'slate';
4 | import { SlateEditable, withAngular } from 'slate-angular';
5 | import { DemoElementVideoComponent } from '../components/video/video.component';
6 |
7 | @Component({
8 | selector: 'demo-embeds',
9 | templateUrl: './embeds.component.html',
10 | styleUrls: ['./embeds.component.scss'],
11 | imports: [SlateEditable, FormsModule]
12 | })
13 | export class DemoEmbedsComponent {
14 | value = initialValue;
15 |
16 | editor = withEmbed(withAngular(createEditor()));
17 |
18 | renderElement = (element: SlateElement) => {
19 | if (element.type === 'video') {
20 | return DemoElementVideoComponent;
21 | }
22 | return null;
23 | };
24 | }
25 |
26 | const withEmbed = (editor: Editor) => {
27 | const { isVoid } = editor;
28 |
29 | editor.isVoid = element => element.type === 'video' || isVoid(element);
30 |
31 | return editor;
32 | };
33 |
34 | const initialValue: Descendant[] = [
35 | {
36 | type: 'paragraph',
37 | children: [
38 | {
39 | text: 'In addition to simple image nodes, you can actually create complex embedded nodes. For example, this one contains an input element that lets you change the video being rendered!'
40 | }
41 | ]
42 | },
43 | {
44 | type: 'video',
45 | url: 'https://player.vimeo.com/video/26689853',
46 | children: [{ text: '' }]
47 | },
48 | {
49 | type: 'paragraph',
50 | children: [
51 | {
52 | text: 'Try it out! This editor is built to handle Vimeo embeds, but you could handle any type.'
53 | }
54 | ]
55 | }
56 | ];
57 |
--------------------------------------------------------------------------------
/demo/app/huge-document/huge-document.component.html:
--------------------------------------------------------------------------------
1 | @if (mode === 'default') {
2 |
3 |
10 |
11 |
12 | }
13 | @if (mode === 'component') {
14 |
15 | @for (item of value; track $index) {
16 |
23 |
24 | }
25 |
26 | }
27 |
28 | @if (context.element.type === 'heading-one') {
29 |
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/demo/app/huge-document/huge-document.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild, TemplateRef, AfterViewInit, NgZone } from '@angular/core';
2 | import { faker } from '@faker-js/faker';
3 | import { createEditor } from 'slate';
4 | import { withAngular } from 'slate-angular';
5 | import { take } from 'rxjs/operators';
6 | import { SlateElement } from '../../../packages/src/components/element/element.component';
7 | import { FormsModule } from '@angular/forms';
8 | import { SlateEditable } from '../../../packages/src/components/editable/editable.component';
9 |
10 | @Component({
11 | selector: 'demo-huge-document',
12 | templateUrl: 'huge-document.component.html',
13 | imports: [SlateEditable, FormsModule, SlateElement]
14 | })
15 | export class DemoHugeDocumentComponent implements OnInit, AfterViewInit {
16 | mode: 'default' | 'component' = 'default';
17 |
18 | value = buildInitialValue();
19 |
20 | componentValue = [
21 | {
22 | type: 'paragraph',
23 | children: [{ text: faker.lorem.paragraph() }]
24 | }
25 | ];
26 |
27 | editor = withAngular(createEditor());
28 |
29 | @ViewChild('elementTemplate', { read: TemplateRef, static: true })
30 | elementTemplate: TemplateRef;
31 |
32 | constructor(private ngZone: NgZone) {}
33 |
34 | ngOnInit() {
35 | console.time();
36 | }
37 |
38 | ngAfterViewInit(): void {
39 | this.ngZone.onStable.pipe(take(1)).subscribe(() => {
40 | console.timeEnd();
41 | });
42 | }
43 |
44 | renderElement() {
45 | return (element: any) => {
46 | if (element.type === 'heading-one') {
47 | return this.elementTemplate;
48 | }
49 | return null;
50 | };
51 | }
52 |
53 | valueChange(event) {}
54 | }
55 |
56 | export const buildInitialValue = () => {
57 | const HEADINGS = 2000;
58 | const PARAGRAPHS = 7;
59 | const initialValue = [];
60 |
61 | for (let h = 0; h < HEADINGS; h++) {
62 | initialValue.push({
63 | type: 'heading-one',
64 | children: [{ text: faker.lorem.sentence() }]
65 | });
66 |
67 | for (let p = 0; p < PARAGRAPHS; p++) {
68 | initialValue.push({
69 | type: 'paragraph',
70 | children: [{ text: faker.lorem.paragraph() }]
71 | });
72 | }
73 | }
74 | return initialValue;
75 | };
76 |
--------------------------------------------------------------------------------
/demo/app/images/images.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | image
5 |
6 |
7 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/demo/app/images/images.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { withAngular } from 'slate-angular';
3 | import { createEditor, Transforms, Editor, Element } from 'slate';
4 | import { DemoElementImageComponent } from '../components/image/image-component';
5 | import { ImageElement } from '../../../custom-types';
6 | import { FormsModule } from '@angular/forms';
7 | import { SlateEditable } from '../../../packages/src/components/editable/editable.component';
8 | import { DemoButtonComponent } from '../components/button/button.component';
9 |
10 | @Component({
11 | selector: 'demo-images',
12 | templateUrl: 'images.component.html',
13 | imports: [DemoButtonComponent, SlateEditable, FormsModule]
14 | })
15 | export class DemoImagesComponent implements OnInit {
16 | value = initialValue;
17 |
18 | editor = withImage(withAngular(createEditor()));
19 |
20 | constructor() {}
21 |
22 | renderElement() {
23 | return (element: Element) => {
24 | if (element.type === 'image') {
25 | return DemoElementImageComponent;
26 | }
27 | return null;
28 | };
29 | }
30 |
31 | ngOnInit() {}
32 |
33 | isImgUrl(imgUrl: string) {
34 | return new Promise((resolve, reject) => {
35 | const imgObj = new Image();
36 | imgObj.src = imgUrl;
37 | imgObj.onload = () => {
38 | resolve(true);
39 | };
40 | imgObj.onerror = () => {
41 | reject(false);
42 | };
43 | }).catch(error => {});
44 | }
45 |
46 | createImageNode(imgUrl: string) {
47 | const imageNode: ImageElement = {
48 | type: 'image',
49 | url: imgUrl,
50 | children: [
51 | {
52 | text: ''
53 | }
54 | ]
55 | };
56 | Transforms.insertNodes(this.editor, imageNode);
57 | }
58 |
59 | addImages() {
60 | const imgUrl = window.prompt('Enter the URL of the image:');
61 | if (imgUrl) {
62 | this.isImgUrl(imgUrl).then(value => {
63 | if (value) {
64 | this.createImageNode(imgUrl);
65 | } else {
66 | window.alert('URL is not an image');
67 | }
68 | });
69 | }
70 | }
71 |
72 | valueChange(event) {}
73 | }
74 | const initialValue = [
75 | {
76 | type: 'paragraph',
77 | children: [
78 | {
79 | text: 'In addition to nodes that contain editable text, you can also create other types of nodes, like images or videos.'
80 | }
81 | ],
82 | key: 'HdSTK'
83 | },
84 | {
85 | type: 'image',
86 | url: 'https://github.com/images/modules/search/light2x.png',
87 | children: [
88 | {
89 | text: ''
90 | }
91 | ],
92 | key: 'EwcCn',
93 | voids: true
94 | },
95 | {
96 | type: 'paragraph',
97 | children: [
98 | {
99 | text: 'This example shows images in action. It features two ways to add images. You can either add an image via the toolbar icon above, or if you want in on a little secret, copy an image URL to your keyboard and paste it anywhere in the editor!'
100 | }
101 | ],
102 | key: 'ecJaY'
103 | },
104 | {
105 | type: 'paragraph',
106 | children: [
107 | {
108 | text: ''
109 | }
110 | ],
111 | key: 'zRTHT'
112 | }
113 | ];
114 |
115 | const withImage = (editor: Editor) => {
116 | const { isVoid } = editor;
117 |
118 | editor.isVoid = element => {
119 | return element.type === 'image' || isVoid(element);
120 | };
121 |
122 | editor.isBlockCard = (element: Element) => {
123 | return element.type === 'image' || isVoid(element);
124 | };
125 |
126 | return editor;
127 | };
128 |
--------------------------------------------------------------------------------
/demo/app/inlines/inlines.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | @for (toolbarItem of toolbarItems; track $index) {
4 | {{ toolbarItem.icon }}
7 | }
8 |
9 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/demo/app/markdown-shorcuts/markdown-shortcuts.component.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/demo/app/mentions/mentions.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 | @{{ context.element.character }}
14 |
15 |
16 |
17 | @for (item of suggestions; track $index; let i = $index) {
18 |
19 | {{ item }}
20 |
21 | }
22 |
23 |
24 |
--------------------------------------------------------------------------------
/demo/app/mentions/mentions.component.scss:
--------------------------------------------------------------------------------
1 | .demo-mention-view {
2 | background: #eee;
3 | padding: 0px 8px;
4 | border-radius: 12px;
5 | color: #666;
6 | white-space: nowrap;
7 | display: inline-block;
8 | margin: 0 1px;
9 | line-height: 22px;
10 | &.focus {
11 | box-shadow: 0 0 0 2px #b4d5ff;
12 | }
13 | cursor: pointer;
14 | }
15 |
16 | .demo-mention-suggestion-list {
17 | position: fixed;
18 | left: -999px;
19 | top: -999px;
20 | width: 150px;
21 | max-height: 300px;
22 | overflow-y: auto;
23 | z-index: 1;
24 | background-color: white;
25 | div {
26 | padding: 0;
27 | margin: 0;
28 | line-height: 35px;
29 | padding-left: 15px;
30 | cursor: pointer;
31 | &.active {
32 | background-color: #b4d5ff;
33 | border-radius: 3px;
34 | }
35 | }
36 | padding: 3px;
37 | border-radius: 4px;
38 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2);
39 | }
40 |
--------------------------------------------------------------------------------
/demo/app/placeholder/placeholder.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { createEditor, Descendant, Editor, Node } from 'slate';
3 | import { SlatePlaceholder, withAngular } from 'slate-angular';
4 | import { FormsModule } from '@angular/forms';
5 | import { SlateEditable } from '../../../packages/src/components/editable/editable.component';
6 |
7 | @Component({
8 | selector: 'demo-placeholder',
9 | template: `
10 |
11 |
17 |
18 |
19 |
25 |
26 | `,
27 | imports: [SlateEditable, FormsModule]
28 | })
29 | export class DemoPlaceholderComponent {
30 | constructor() {}
31 |
32 | value = initialValue;
33 |
34 | otherValue = [
35 | {
36 | type: 'paragraph',
37 | children: [
38 | {
39 | text: 'Press Enter to make new paragraph and will show placeholder'
40 | }
41 | ]
42 | }
43 | ];
44 |
45 | placeholderDecorate: (editor: Editor) => SlatePlaceholder[] = editor => {
46 | const cursorAnchor = editor.selection?.anchor;
47 | if (cursorAnchor) {
48 | const parent = Node.parent(editor, cursorAnchor.path);
49 | if (parent.children.length === 1 && Array.from(Node.texts(parent)).length === 1 && Node.string(parent) === '') {
50 | const start = Editor.start(editor, cursorAnchor);
51 | return [
52 | {
53 | placeholder: 'advance placeholder use with placeholderDecoration',
54 | anchor: start,
55 | focus: start
56 | }
57 | ];
58 | } else {
59 | return [];
60 | }
61 | }
62 | return [];
63 | };
64 |
65 | editor = withAngular(createEditor());
66 |
67 | editorWithCustomDecoration = withAngular(createEditor());
68 | }
69 |
70 | const initialValue: Descendant[] = [
71 | {
72 | type: 'paragraph',
73 | children: [
74 | {
75 | text: ''
76 | }
77 | ]
78 | }
79 | ];
80 |
--------------------------------------------------------------------------------
/demo/app/plugins/block-cards.plugin.ts:
--------------------------------------------------------------------------------
1 | import { Editor, Path, Transforms, Location } from 'slate';
2 | import { AngularEditor, hasBlockCard, isCardLeft } from 'slate-angular';
3 | import { HistoryEditor } from 'slate-history';
4 |
5 | export const withBlockCard = editor => {
6 | const { insertBreak, deleteBackward, deleteForward, insertText } = editor;
7 |
8 | editor.insertBreak = () => {
9 | const domSelection = window.getSelection();
10 | const anchorNode = domSelection.anchorNode;
11 | if (domSelection && domSelection.isCollapsed && hasBlockCard(domSelection)) {
12 | const isLeftCursor = isCardLeft(anchorNode);
13 | const cardEntry = AngularEditor.toSlateCardEntry(editor, anchorNode);
14 | const cursorRootPath = cardEntry[1];
15 | insertParagraph(editor, isLeftCursor ? cursorRootPath : Path.next(cursorRootPath));
16 | if (!isLeftCursor) {
17 | Transforms.select(editor, Path.next(cursorRootPath));
18 | }
19 | return;
20 | }
21 | insertBreak();
22 | };
23 |
24 | editor.deleteBackward = unit => {
25 | const domSelection = window.getSelection();
26 | const anchorNode = domSelection.anchorNode;
27 | if (domSelection && domSelection.isCollapsed && hasBlockCard(domSelection)) {
28 | const isLeftCursor = isCardLeft(anchorNode);
29 | const cardEntry = AngularEditor.toSlateCardEntry(editor, anchorNode);
30 | const cursorRootPath = cardEntry[1];
31 | if (isLeftCursor) {
32 | const previousPath = Path.previous(cursorRootPath);
33 | HistoryEditor.withoutMerging(editor, () => {
34 | Transforms.select(editor, Editor.end(editor, previousPath));
35 | });
36 | return;
37 | } else {
38 | insertParagraph(editor, cursorRootPath);
39 | Transforms.select(editor, cursorRootPath);
40 | Transforms.removeNodes(editor, {
41 | at: Path.next(cursorRootPath)
42 | });
43 | return;
44 | }
45 | }
46 | deleteBackward(unit);
47 | };
48 |
49 | editor.deleteForward = unit => {
50 | const domSelection = window.getSelection();
51 | const anchorNode = domSelection.anchorNode;
52 | if (domSelection && domSelection.isCollapsed && hasBlockCard(domSelection)) {
53 | const isLeftCursor = isCardLeft(anchorNode);
54 | const cardEntry = AngularEditor.toSlateCardEntry(editor, anchorNode);
55 | const cursorRootPath = cardEntry[1];
56 | if (isLeftCursor) {
57 | insertParagraph(editor, cursorRootPath);
58 | Transforms.select(editor, cursorRootPath);
59 | Transforms.removeNodes(editor, {
60 | at: Path.next(cursorRootPath)
61 | });
62 | return;
63 | } else {
64 | const nextPath = Path.next(cursorRootPath);
65 | HistoryEditor.withoutMerging(editor, () => {
66 | Transforms.select(editor, Editor.start(editor, nextPath));
67 | });
68 | return;
69 | }
70 | }
71 |
72 | deleteForward(unit);
73 | };
74 |
75 | editor.insertText = (text: string) => {
76 | const domSelection = window.getSelection();
77 | const anchorNode = domSelection?.anchorNode;
78 | if (domSelection && domSelection.isCollapsed && hasBlockCard(domSelection)) {
79 | const isLeftCursor = isCardLeft(anchorNode);
80 | const cardEntry = AngularEditor.toSlateCardEntry(editor, anchorNode);
81 | const cursorRootPath = cardEntry[1];
82 | if (isLeftCursor) {
83 | insertParagraph(editor, cursorRootPath);
84 | Transforms.select(editor, cursorRootPath);
85 | } else {
86 | const nextPath = Path.next(cursorRootPath);
87 | insertParagraph(editor, nextPath);
88 | Transforms.select(editor, nextPath);
89 | }
90 | }
91 | insertText(text);
92 | };
93 |
94 | return editor;
95 | };
96 |
97 | const insertParagraph = (editor: Editor, at: Location) => {
98 | Transforms.insertNodes(
99 | editor,
100 | {
101 | type: 'paragraph',
102 | children: [
103 | {
104 | text: ''
105 | }
106 | ]
107 | },
108 | { at }
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/demo/app/readonly/readonly.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { createEditor, Descendant } from 'slate';
3 | import { withAngular } from 'slate-angular';
4 | import { FormsModule } from '@angular/forms';
5 | import { SlateEditable } from '../../../packages/src/components/editable/editable.component';
6 |
7 | @Component({
8 | selector: 'demo-readonly',
9 | template: `
10 |
11 |
12 |
13 | `,
14 | imports: [SlateEditable, FormsModule]
15 | })
16 | export class DemoReadonlyComponent {
17 | constructor() {}
18 |
19 | value = initialValue;
20 |
21 | editor = withAngular(createEditor());
22 | }
23 |
24 | const initialValue: Descendant[] = [
25 | {
26 | type: 'paragraph',
27 | children: [
28 | {
29 | text: 'This example shows what happens when the Editor is set to readOnly, it is not editable'
30 | }
31 | ]
32 | }
33 | ];
34 |
--------------------------------------------------------------------------------
/demo/app/richtext/richtext.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | @for (toolbarItem of toolbarItems; track $index) {
4 | {{ toolbarItem.icon }}
7 | }
8 |
9 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/demo/app/search-highlighting/leaf.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, Renderer2 } from '@angular/core';
2 | import { BaseLeafComponent } from 'slate-angular';
3 | import { SlateString } from '../../../packages/src/components/string/string.component';
4 |
5 | @Component({
6 | selector: 'span[demoLeaf]',
7 | template: ` `,
8 | imports: [SlateString]
9 | })
10 | export class DemoLeafComponent extends BaseLeafComponent {
11 | private renderer = inject(Renderer2);
12 |
13 | onContextChange() {
14 | super.onContextChange();
15 | this.changeStyle();
16 | }
17 |
18 | changeStyle() {
19 | const backgroundColor = this.leaf['highlight'] ? '#ffeeba' : null;
20 | this.renderer.setStyle(this.nativeElement, 'backgroundColor', backgroundColor);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/demo/app/search-highlighting/search-highlighting.component.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/demo/app/search-highlighting/search-highlighting.component.scss:
--------------------------------------------------------------------------------
1 | .search-highlighting-toolbar {
2 | position: relative;
3 | display: flex;
4 | align-items: center;
5 | top: 0.5em;
6 | left: 0.5em;
7 | color: #ccc;
8 | border: 2px solid #ccc;
9 | border-radius: 4px;
10 |
11 | input {
12 | border: none;
13 | outline: none;
14 | background-color: transparent;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/demo/app/search-highlighting/search-highlighting.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
2 | import { createEditor, NodeEntry, Range, Text } from 'slate';
3 | import { withAngular } from 'slate-angular';
4 | import { DemoTextMarkComponent, MarkTypes } from '../components/text/text.component';
5 | import { DemoLeafComponent } from './leaf.component';
6 | import { SlateEditable } from '../../../packages/src/components/editable/editable.component';
7 | import { FormsModule } from '@angular/forms';
8 |
9 | @Component({
10 | selector: 'demo-search-highlight',
11 | templateUrl: './search-highlighting.component.html',
12 | styleUrls: ['./search-highlighting.component.scss'],
13 | imports: [FormsModule, SlateEditable]
14 | })
15 | export class DemoSearchHighlightingComponent implements OnInit {
16 | keywords = '';
17 |
18 | value = initialValue;
19 |
20 | editor = withAngular(createEditor());
21 |
22 | decorate: (nodeEntry: NodeEntry) => Range[];
23 |
24 | constructor(private cdr: ChangeDetectorRef) {}
25 |
26 | ngOnInit(): void {
27 | this.generateDecorate();
28 | }
29 |
30 | keywordsChange(event) {
31 | this.generateDecorate();
32 | this.cdr.markForCheck();
33 | }
34 |
35 | generateDecorate() {
36 | this.decorate = ([node, path]) => {
37 | const ranges = [];
38 |
39 | if (this.keywords && Text.isText(node)) {
40 | const { text } = node;
41 | const parts = text.split(this.keywords);
42 | let offset = 0;
43 |
44 | parts.forEach((part, i) => {
45 | if (i !== 0) {
46 | ranges.push({
47 | anchor: {
48 | path,
49 | offset: offset - this.keywords.length
50 | },
51 | focus: { path, offset },
52 | highlight: true
53 | });
54 | }
55 |
56 | offset = offset + part.length + this.keywords.length;
57 | });
58 | }
59 |
60 | return ranges;
61 | };
62 | }
63 |
64 | renderText = (text: Text) => {
65 | if (text[MarkTypes.bold] || text[MarkTypes.italic] || text[MarkTypes.code] || text[MarkTypes.underline]) {
66 | return DemoTextMarkComponent;
67 | }
68 | };
69 |
70 | renderLeaf = (text: Text) => {
71 | if (text['highlight']) {
72 | return DemoLeafComponent;
73 | }
74 | return null;
75 | };
76 | }
77 |
78 | const initialValue = [
79 | {
80 | children: [
81 | {
82 | text: 'This is editable text that you can search. As you search, it looks for matching strings of text, and adds '
83 | },
84 | { text: 'decorations', bold: true },
85 | { text: ' to them in realtime.' }
86 | ]
87 | },
88 | {
89 | children: [
90 | {
91 | text: 'Try it out for yourself by typing in the search box above!'
92 | }
93 | ]
94 | }
95 | ];
96 |
--------------------------------------------------------------------------------
/demo/app/tables/tables.component.html:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/assets/materialicons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/demo/assets/materialicons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2
--------------------------------------------------------------------------------
/demo/assets/materialicons/icon.css:
--------------------------------------------------------------------------------
1 | /* fallback */
2 | @font-face {
3 | font-family: 'Material Icons';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: url(/assets/materialicons/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2) format('woff2');
7 | }
8 |
9 | .material-icons {
10 | font-family: 'Material Icons';
11 | font-weight: normal;
12 | font-style: normal;
13 | font-size: 24px;
14 | line-height: 1;
15 | letter-spacing: normal;
16 | text-transform: none;
17 | display: inline-block;
18 | white-space: nowrap;
19 | word-wrap: normal;
20 | direction: ltr;
21 | -webkit-font-feature-settings: 'liga';
22 | -webkit-font-smoothing: antialiased;
23 | }
24 |
--------------------------------------------------------------------------------
/demo/assets/photo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/demo/assets/photo.jpeg
--------------------------------------------------------------------------------
/demo/assets/text.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
67 |
--------------------------------------------------------------------------------
/demo/editor-typo.scss:
--------------------------------------------------------------------------------
1 | .slate-editable-container {
2 | [slate-underlined][slate-strike] {
3 | text-decoration: underline line-through;
4 | }
5 | [slate-strike] {
6 | text-decoration: line-through;
7 | }
8 | [slate-underlined] {
9 | text-decoration: underline;
10 | }
11 | [slate-italic] {
12 | font-style: italic;
13 | }
14 | [slate-bold] {
15 | font-weight: bold;
16 | }
17 | [slate-code-line] {
18 | margin: 0 4px;
19 | padding: 2px 3px;
20 | border: 1px solid rgba($color: #000000, $alpha: 0.08);
21 | border-radius: 2px;
22 | background-color: rgba($color: #000000, $alpha: 0.06);
23 | }
24 | blockquote {
25 | margin: 0;
26 | margin-left: 0;
27 | margin-right: 0;
28 | color: #888;
29 | padding-left: 10px !important;
30 | border-left: 4px solid #eee;
31 | }
32 | h1,
33 | h2,
34 | h3 {
35 | margin: 0px;
36 | }
37 | & > [data-slate-node='element'],
38 | & > slate-block-card {
39 | margin-bottom: 12px;
40 | }
41 | .demo-element-button {
42 | margin: 0 0.1em;
43 | background-color: #efefef;
44 | padding: 2px 6px;
45 | border: 1px solid #767676;
46 | border-radius: 2px;
47 | font-size: 0.9em;
48 | }
49 | .demo-element-link-active {
50 | box-shadow: 0 0 0 3px #ddd;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/demo/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/demo/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 |
--------------------------------------------------------------------------------
/demo/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/demo/favicon.ico
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Slate Angular Examples - Angular view layer for Slate
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/demo/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch(err => console.error(err));
14 |
--------------------------------------------------------------------------------
/demo/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 | import 'core-js/features/global-this';
50 |
51 | window['global'] = window as any;
52 | /***************************************************************************************************
53 | * APPLICATION IMPORTS
54 | */
55 |
--------------------------------------------------------------------------------
/demo/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import './app/app.component.scss';
3 | @import './editor-typo.scss';
4 | @import '../packages/src/styles/index.scss';
5 | @import './assets/materialicons/icon.css';
6 |
7 | html,
8 | input,
9 | textarea {
10 | font-family: 'Roboto', sans-serif;
11 | line-height: 1.4;
12 | background: #fafafa;
13 | }
14 |
15 | body {
16 | margin: 0;
17 | }
18 |
--------------------------------------------------------------------------------
/demo/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/testing';
4 | import { getTestBed } from '@angular/core/testing';
5 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
6 |
7 | // First, initialize the Angular testing environment.
8 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
9 | teardown: { destroyAfterEach: false }
10 | });
11 |
--------------------------------------------------------------------------------
/demo/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/app",
5 | "types": []
6 | },
7 | "exclude": ["test.ts", "**/*.spec.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/demo/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "types": ["jasmine", "node"]
6 | },
7 | "files": ["test.ts", "polyfills.ts"],
8 | "include": ["**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/docs/compatible.md:
--------------------------------------------------------------------------------
1 | ## Safari
2 |
3 | IME input handle by beforeinput
4 |
5 | ## Chrome
6 |
7 | IME input handle by compositionend
8 |
9 | ## Edge
10 |
11 | IME input handle by compositionend
12 |
13 | ## Firefox
14 |
15 | IME input handle by compositionend
16 |
17 | ## QQ
18 |
19 | IME input handle by compositionend
20 |
--------------------------------------------------------------------------------
/docs/images/banner.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/docs/images/banner.jpeg
--------------------------------------------------------------------------------
/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: ['./demo/**/*.e2e-spec.ts'],
13 | capabilities: {
14 | browserName: 'chrome'
15 | },
16 | directConnect: true,
17 | baseUrl: 'http://localhost:4200/',
18 | framework: 'jasmine',
19 | jasmineNodeOpts: {
20 | showColors: true,
21 | defaultTimeoutInterval: 30000,
22 | print: function () {}
23 | },
24 | onPrepare() {
25 | require('ts-node').register({
26 | project: require('path').join(__dirname, './tsconfig.json')
27 | });
28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 | import { browser, logging } from 'protractor';
3 |
4 | describe('workspace-project App', () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it('should display welcome message', () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual('test');
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER);
19 | expect(logs).not.toContain(
20 | jasmine.objectContaining({
21 | level: logging.Level.SEVERE
22 | } as logging.Entry)
23 | );
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo(): Promise {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText(): Promise {
9 | return element(by.css('demo-app-root .content span')).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es2018",
7 | "types": ["jasmine", "jasminewd2", "node"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | jasmineHtmlReporter: {
19 | suppressAll: true // removes the duplicated traces
20 | },
21 | coverageReporter: {
22 | dir: 'coverage/slate-angular',
23 | subdir: '.',
24 | reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'lcovonly' }]
25 | },
26 | angularCli: {
27 | environment: 'dev'
28 | },
29 | files: [],
30 | reporters: ['progress', 'kjhtml'],
31 | port: 9876,
32 | colors: true,
33 | logLevel: config.LOG_INFO,
34 | autoWatch: true,
35 | browsers: ['Chrome', 'ChromeHeadless', 'ChromeHeadlessCI'],
36 | singleRun: false,
37 | customLaunchers: {
38 | ChromeHeadlessCI: {
39 | base: 'ChromeHeadless',
40 | flags: ['--no-sandbox']
41 | }
42 | },
43 | restartOnFileChange: true
44 | });
45 | };
46 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080;
3 |
4 | location / {
5 |
6 | if ($request_filename ~ .*\.(htm|html)$) {
7 | add_header Cache-Control no-cache;
8 | }
9 | try_files $uri $uri/ /index.html;
10 | port_in_redirect off;
11 | proxy_redirect off;
12 | proxy_set_header Host $host;
13 | proxy_set_header X-Real-IP $remote_addr;
14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15 | proxy_set_header X-Forwarded-Proto http;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slate-angular",
3 | "workspaces": [
4 | "packages"
5 | ],
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve demo",
9 | "build": "ng build slate-angular --configuration production && cpx \"./packages/src/**/*.scss\" ./dist/",
10 | "build:demo": "ng build demo",
11 | "pub": "npm run build && cd dist && npm publish --access public",
12 | "pub-next": "npm run build && cd dist && npm publish --tag next --access public",
13 | "patch": "cd packages && npm version patch",
14 | "minor": "cd packages && npm version minor",
15 | "major": "cd packages && npm version major",
16 | "release": "standard-version",
17 | "test": "ng test slate-angular",
18 | "report-coverage": "cat ./coverage/lcov.info | coveralls",
19 | "lint": "ng lint",
20 | "e2e": "ng e2e",
21 | "format": "prettier --check --write \"**/*\""
22 | },
23 | "husky": {
24 | "hooks": {
25 | "hooks": {
26 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
27 | "pre-commit": "lint-staged"
28 | }
29 | }
30 | },
31 | "private": true,
32 | "repository": {
33 | "type": "git",
34 | "url": "https://github.com/worktile/slate-angular"
35 | },
36 | "dependencies": {
37 | "@angular/animations": "~19.2.2",
38 | "@angular/common": "~19.2.2",
39 | "@angular/compiler": "^19.2.2",
40 | "@angular/core": "~19.2.2",
41 | "@angular/forms": "~19.2.2",
42 | "@angular/platform-browser": "~19.2.2",
43 | "@angular/platform-browser-dynamic": "~19.2.2",
44 | "@angular/router": "^19.2.2",
45 | "core-js": "3.35.0",
46 | "direction": "^2.0.1",
47 | "is-hotkey": "^0.2.0",
48 | "rxjs": "~7.8.1",
49 | "scroll-into-view-if-needed": "^3.1.0",
50 | "slate": "^0.103.0",
51 | "slate-history": "^0.100.0",
52 | "tslib": "^2.6.2",
53 | "zone.js": "~0.15.0"
54 | },
55 | "devDependencies": {
56 | "@angular-devkit/build-angular": "^19.2.3",
57 | "@angular-devkit/core": "^19.2.3",
58 | "@angular-eslint/builder": "19.2.1",
59 | "@angular-eslint/eslint-plugin": "19.2.1",
60 | "@angular-eslint/eslint-plugin-template": "19.2.1",
61 | "@angular-eslint/schematics": "19.2.1",
62 | "@angular-eslint/template-parser": "19.2.1",
63 | "@angular/cli": "^19.2.3",
64 | "@angular/compiler-cli": "^19.2.2",
65 | "@angular/language-service": "^19.2.2",
66 | "@changesets/changelog-github": "^0.4.8",
67 | "@changesets/cli": "^2.26.0",
68 | "@commitlint/cli": "^19.8.0",
69 | "@commitlint/config-conventional": "^19.8.0",
70 | "@faker-js/faker": "^8.3.1",
71 | "@types/codemirror": "5.60.15",
72 | "@types/is-hotkey": "^0.1.10",
73 | "@types/is-url": "^1.2.32",
74 | "@types/jasmine": "~5.1.4",
75 | "@types/jasminewd2": "~2.0.13",
76 | "@types/node": "^18.13.0",
77 | "@typescript-eslint/eslint-plugin": "^6.10.0",
78 | "@typescript-eslint/parser": "^6.10.0",
79 | "coveralls": "^3.1.1",
80 | "cpx": "^1.5.0",
81 | "eslint": "^8.53.0",
82 | "eslint-config-prettier": "^9.1.0",
83 | "eslint-plugin-prettier": "^5.1.3",
84 | "husky": "^8.0.3",
85 | "is-url": "^1.2.4",
86 | "jasmine": "~5.1.0",
87 | "jasmine-core": "~5.1.1",
88 | "karma": "^6.4.2",
89 | "karma-chrome-launcher": "~3.2.0",
90 | "karma-coverage": "~2.2.1",
91 | "karma-jasmine": "~5.1.0",
92 | "karma-jasmine-html-reporter": "~2.1.0",
93 | "lint-staged": "^15.2.0",
94 | "ng-packagr": "^19.2.0",
95 | "prettier": "^3.1.1",
96 | "pretty-quick": "3.1.3",
97 | "standard-version": "^9.5.0",
98 | "ts-node": "~10.9.2",
99 | "typescript": "~5.5.4"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/packages/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/worktile/slate-angular/2f2fd8396aca8135effa79db10d753bc09853563/packages/README.md
--------------------------------------------------------------------------------
/packages/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | jasmineHtmlReporter: {
19 | suppressAll: true // removes the duplicated traces
20 | },
21 | coverageReporter: {
22 | dir: require('path').join(__dirname, '../coverage/slate-angular'),
23 | subdir: '.',
24 | reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'lcovonly', subdir: '..', file: 'lcov.info' }]
25 | },
26 | files: [],
27 | reporters: ['progress', 'kjhtml'],
28 | port: 9876,
29 | colors: true,
30 | logLevel: config.LOG_INFO,
31 | autoWatch: true,
32 | browsers: ['Chrome'],
33 | singleRun: false,
34 | customLaunchers: {
35 | ChromeHeadlessCI: {
36 | base: 'ChromeHeadless',
37 | flags: ['--no-sandbox']
38 | }
39 | },
40 | restartOnFileChange: true
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/packages/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../dist",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | },
7 | "allowedNonPeerDependencies": ["slate-history", "debug", "direction", "is-hotkey", "slate", "scroll-into-view-if-needed"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "slate-angular",
3 | "version": "19.0.0",
4 | "description": "Angular view layer for Slate",
5 | "author": "pubuzhixing ",
6 | "homepage": "https://github.com/worktile/slate-angular#readme",
7 | "license": "MIT",
8 | "peerDependencies": {
9 | "slate": ">=0.101.0 <=0.104.0",
10 | "slate-history": "^0.100.0",
11 | "debug": "^4.1.1",
12 | "direction": "^2.0.1",
13 | "is-hotkey": "^0.2.0",
14 | "scroll-into-view-if-needed": "^2.2.20"
15 | },
16 | "dependencies": {
17 | "slate": ">=0.101.0 <=0.104.0",
18 | "slate-history": "^0.100.0",
19 | "direction": "^2.0.1",
20 | "is-hotkey": "^0.2.0",
21 | "scroll-into-view-if-needed": "^3.1.0",
22 | "tslib": "^2.6.2"
23 | },
24 | "exports": {
25 | ".": {
26 | "sass": "./styles/index.scss"
27 | },
28 | "./styles": {
29 | "sass": "./styles/index.scss"
30 | },
31 | "./styles/index.scss": {
32 | "sass": "./styles/index.scss"
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/src/components/block-card/block-card.component.html:
--------------------------------------------------------------------------------
1 | {{ '\uFEFF' }}
2 |
3 | {{ '\uFEFF' }}
4 |
--------------------------------------------------------------------------------
/packages/src/components/block-card/block-card.component.scss:
--------------------------------------------------------------------------------
1 | .slate-block-card {
2 | display: block;
3 | position: relative;
4 | .card-left,
5 | .card-right {
6 | bottom: 0px;
7 | position: absolute;
8 | width: 2px;
9 | overflow: hidden;
10 | user-select: text;
11 | }
12 | .card-left {
13 | left: -2px;
14 | text-align: left;
15 | }
16 | .card-right {
17 | right: -2px;
18 | text-align: right;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/src/components/block-card/block-card.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
2 | import { By } from '@angular/platform-browser';
3 | import { ImageEditableComponent, configureBasicEditableTestingModule } from '../../testing';
4 |
5 | describe('Block Card Component', () => {
6 | let component: ImageEditableComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(fakeAsync(() => {
10 | configureBasicEditableTestingModule([ImageEditableComponent]);
11 | fixture = TestBed.createComponent(ImageEditableComponent);
12 | component = fixture.componentInstance;
13 | fixture.detectChanges();
14 | flush();
15 | fixture.detectChanges();
16 | }));
17 |
18 | it('The block-card component should be created', fakeAsync(() => {
19 | let blockCardElement: HTMLElement;
20 | blockCardElement = fixture.debugElement.query(By.css('.slate-block-card')).nativeElement;
21 | expect(blockCardElement).toBeTruthy();
22 | }));
23 | });
24 |
--------------------------------------------------------------------------------
/packages/src/components/block-card/block-card.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'slate-block-card, [slateBlockCard]',
5 | templateUrl: 'block-card.component.html',
6 | standalone: true
7 | })
8 | export class SlateBlockCard implements OnInit {
9 | @ViewChild('centerContianer', { static: true })
10 | centerContianer: ElementRef;
11 |
12 | centerRootNodes: HTMLElement[];
13 |
14 | get nativeElement() {
15 | return this.elementRef.nativeElement;
16 | }
17 |
18 | get centerContainerElement() {
19 | return this.centerContianer.nativeElement as HTMLElement;
20 | }
21 |
22 | constructor(private elementRef: ElementRef) {}
23 |
24 | ngOnInit() {
25 | this.nativeElement.classList.add(`slate-block-card`);
26 | }
27 |
28 | append() {
29 | this.centerRootNodes.forEach(
30 | rootNode => !this.centerContainerElement.contains(rootNode) && this.centerContainerElement.appendChild(rootNode)
31 | );
32 | }
33 |
34 | initializeCenter(rootNodes: HTMLElement[]) {
35 | this.centerRootNodes = rootNodes;
36 | this.append();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/src/components/children/children-outlet.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'slate-children-outlet',
5 | template: ``,
6 | changeDetection: ChangeDetectionStrategy.OnPush,
7 | standalone: true
8 | })
9 | export class SlateChildrenOutlet {
10 | constructor(private elementRef: ElementRef) {}
11 | getNativeElement() {
12 | return this.elementRef.nativeElement;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/src/components/children/children.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
2 | import { Descendant } from 'slate';
3 | import { SlateChildrenContext, SlateViewContext } from '../../view/context';
4 | import { ViewContainer } from '../../view/container';
5 |
6 | @Component({
7 | selector: 'slate-children',
8 | template: ``,
9 | changeDetection: ChangeDetectionStrategy.OnPush,
10 | standalone: true
11 | })
12 | export class SlateChildren extends ViewContainer {
13 | @Input() children: Descendant[];
14 |
15 | @Input() context: SlateChildrenContext;
16 |
17 | @Input() viewContext: SlateViewContext;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/src/components/editable/editable.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/src/components/element/default-element.component.token.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 | import { ComponentType } from '../../types/view';
3 | import { BaseElementComponent } from '../../view/base';
4 |
5 | export const SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN = new InjectionToken>('slate-default-element-token');
6 |
--------------------------------------------------------------------------------
/packages/src/components/element/default-element.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 | import { BaseElementComponent } from '../../view/base';
3 |
4 | @Component({
5 | selector: 'div[slateDefaultElement]',
6 | template: ``,
7 | changeDetection: ChangeDetectionStrategy.OnPush
8 | })
9 | export class SlateDefaultElement extends BaseElementComponent {}
10 |
--------------------------------------------------------------------------------
/packages/src/components/element/element.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component } from '@angular/core';
2 | import { BaseElementComponent } from '../../view/base';
3 |
4 | @Component({
5 | selector: '[slateElement]',
6 | template: '',
7 | changeDetection: ChangeDetectionStrategy.OnPush
8 | })
9 | export class SlateElement extends BaseElementComponent {}
10 |
--------------------------------------------------------------------------------
/packages/src/components/leaf/default-leaf.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnDestroy } from '@angular/core';
2 | import { BaseLeafComponent } from '../../view/base';
3 | import { SlateString } from '../string/string.component';
4 |
5 | @Component({
6 | selector: 'span[slateDefaultLeaf]',
7 | template: ``,
8 | changeDetection: ChangeDetectionStrategy.OnPush,
9 | host: {
10 | 'data-slate-leaf': 'true'
11 | },
12 | imports: [SlateString]
13 | })
14 | export class SlateDefaultLeaf extends BaseLeafComponent implements OnDestroy {
15 | onContextChange(): void {
16 | super.onContextChange();
17 | this.renderPlaceholder();
18 | }
19 | ngOnDestroy(): void {
20 | // Because the placeholder span is not in the current component, it is destroyed along with the current component
21 | this.destroyPlaceholder();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/src/components/leaf/token.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 | import { ComponentType } from '../../types/view';
3 | import { BaseLeafComponent } from '../../view/base';
4 |
5 | export const SLATE_DEFAULT_LEAF_COMPONENT_TOKEN = new InjectionToken>('slate-default-leaf-token');
6 |
--------------------------------------------------------------------------------
/packages/src/components/leaves/leaves.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterViewInit,
3 | ChangeDetectionStrategy,
4 | Component,
5 | Input,
6 | OnChanges,
7 | OnInit,
8 | QueryList,
9 | SimpleChanges,
10 | ViewChildren
11 | } from '@angular/core';
12 | import { Text } from 'slate';
13 | import { SlateLeafContext, SlateTextContext } from '../../view/context';
14 | import { ViewContainer } from '../../view/container';
15 |
16 | @Component({
17 | selector: 'slate-leaves',
18 | template: ``,
19 | changeDetection: ChangeDetectionStrategy.OnPush,
20 | imports: []
21 | })
22 | export class SlateLeaves extends ViewContainer {
23 | initialized = false;
24 | leafContexts: SlateLeafContext[];
25 | leaves: Text[];
26 |
27 | @Input() context: SlateTextContext;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/src/components/string/default-string.component.ts:
--------------------------------------------------------------------------------
1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
2 | import { DOMElement } from '../../utils/dom';
3 | import { BaseComponent } from '../../view/base';
4 | import { SlateStringContext } from '../../view/context';
5 | import { BeforeContextChange } from '../../view/context-change';
6 |
7 | @Component({
8 | selector: 'span[slateDefaultString]',
9 | template: '',
10 | changeDetection: ChangeDetectionStrategy.OnPush,
11 | standalone: true
12 | })
13 | export class SlateDefaultString extends BaseComponent implements OnInit, BeforeContextChange {
14 | textNode?: Text;
15 | brNode?: DOMElement;
16 |
17 | beforeContextChange(value: SlateStringContext) {
18 | if (this.context) {
19 | if (this.context.type === 'lineBreakEmptyString') {
20 | if (value.type === 'string') {
21 | this.removeLineBreakEmptyStringDOM();
22 | } else {
23 | this.textNode?.remove();
24 | this.brNode?.remove();
25 | }
26 | }
27 | if (this.context.type === 'string') {
28 | if (value.type === 'lineBreakEmptyString') {
29 | this.removeStringDOM();
30 | }
31 | }
32 | }
33 | }
34 |
35 | onContextChange() {
36 | if (this.context.type === 'string') {
37 | this.createStringDOM();
38 | } else if (this.context.type === 'lineBreakEmptyString') {
39 | this.createLineBreakEmptyStringDOM();
40 | }
41 | }
42 |
43 | createLineBreakEmptyStringDOM() {
44 | this.nativeElement.setAttribute('data-slate-zero-width', 'n');
45 | this.nativeElement.setAttribute('data-slate-length', `${this.context.elementStringLength}`);
46 | this.textNode = document.createTextNode(`\uFEFF`);
47 | this.brNode = document.createElement('br');
48 | this.nativeElement.append(this.textNode, this.brNode);
49 | }
50 |
51 | removeLineBreakEmptyStringDOM() {
52 | this.brNode?.remove();
53 | // remove zero width character
54 | const zeroWidthCharacterIndex = this.textNode?.textContent.indexOf(`\uFEFF`);
55 | this.textNode?.deleteData(zeroWidthCharacterIndex, 1);
56 | this.nativeElement.removeAttribute('data-slate-zero-width');
57 | this.nativeElement.removeAttribute('data-slate-length');
58 | }
59 |
60 | createStringDOM() {
61 | this.nativeElement.setAttribute('data-slate-string', 'true');
62 | this.updateStringDOM();
63 | }
64 |
65 | updateStringDOM() {
66 | // Avoid breaking some browser default behaviors, such as spellCheck, android composition input state
67 | if (this.nativeElement.textContent !== this.context.text) {
68 | this.nativeElement.textContent = this.context.text;
69 | }
70 | }
71 |
72 | removeStringDOM() {
73 | this.nativeElement.removeAttribute('data-slate-string');
74 | this.nativeElement.textContent = '';
75 | }
76 |
77 | ngOnInit(): void {
78 | this.nativeElement.setAttribute('editable-text', '');
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/packages/src/components/string/string.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
2 | import { AdvancedEditableComponent, TestingLeafComponent, configureBasicEditableTestingModule, dispatchFakeEvent } from '../../testing';
3 | import { Editor, Transforms } from 'slate';
4 |
5 | describe('Default String Render', () => {
6 | let component: AdvancedEditableComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(fakeAsync(() => {
10 | configureBasicEditableTestingModule([AdvancedEditableComponent, TestingLeafComponent], [TestingLeafComponent]);
11 | fixture = TestBed.createComponent(AdvancedEditableComponent);
12 | component = fixture.componentInstance;
13 | }));
14 |
15 | it('should correctly render editable text', fakeAsync(() => {
16 | const text = `Kevin Durant`;
17 | component.value = [
18 | {
19 | type: 'paragraph',
20 | children: [{ text }],
21 | key: 'KD'
22 | }
23 | ];
24 | fixture.detectChanges();
25 | flush();
26 | fixture.detectChanges();
27 | const paragraphElement = document.querySelector('[data-slate-node="element"]');
28 | const editableText = paragraphElement.querySelector('[editable-text]');
29 | expect(editableText).toBeTruthy();
30 | expect(editableText.getAttribute('data-slate-string')).toEqual('true');
31 | expect(editableText.textContent).toEqual(text);
32 | }));
33 |
34 | it('should correctly render editable text with \n', fakeAsync(() => {
35 | const text = `Kevin Durant
36 | Steve Jobs`;
37 | component.value = [
38 | {
39 | type: 'paragraph',
40 | children: [{ text }],
41 | key: 'KD'
42 | }
43 | ];
44 | fixture.detectChanges();
45 | flush();
46 | fixture.detectChanges();
47 | const paragraphElement = document.querySelector('[data-slate-node="element"]');
48 | const editableText = paragraphElement.querySelector('[editable-text]');
49 | expect(editableText).toBeTruthy();
50 | expect(editableText.getAttribute('data-slate-string')).toEqual('true');
51 | expect(editableText.textContent).toEqual(text);
52 | }));
53 |
54 | it('should correctly render line break empty string', fakeAsync(() => {
55 | const text = ``;
56 | component.value = [
57 | {
58 | type: 'paragraph',
59 | children: [{ text }],
60 | key: 'KD'
61 | }
62 | ];
63 | fixture.detectChanges();
64 | flush();
65 | fixture.detectChanges();
66 | const paragraphElement = document.querySelector('[data-slate-node="element"]');
67 | const editableText = paragraphElement.querySelector('[editable-text]');
68 | expect(editableText).toBeTruthy();
69 | expect(editableText.childNodes.length).toEqual(2);
70 | expect(editableText.firstChild.textContent).toEqual(`\uFEFF`);
71 | expect(editableText.lastElementChild.tagName).toEqual(`BR`);
72 | }));
73 |
74 | it('should correctly render text when text transform text from empty string to non-empty string', fakeAsync(() => {
75 | const text = ``;
76 | component.value = [
77 | {
78 | type: 'paragraph',
79 | children: [{ text }],
80 | key: 'KD'
81 | }
82 | ];
83 | fixture.detectChanges();
84 | flush();
85 | fixture.detectChanges();
86 | const paragraphElement = document.querySelector('[data-slate-node="element"]');
87 | const editableText = paragraphElement.querySelector('[editable-text]');
88 | expect(editableText).toBeTruthy();
89 | expect(editableText.childNodes.length).toEqual(2);
90 | expect(editableText.firstChild.textContent).toEqual(`\uFEFF`);
91 | expect(editableText.lastElementChild.tagName).toEqual(`BR`);
92 |
93 | Transforms.select(component.editor, Editor.end(component.editor, [0]));
94 |
95 | const newText = 'Kevin Durant';
96 | Transforms.insertText(component.editor, newText);
97 | fixture.detectChanges();
98 | flush();
99 | fixture.detectChanges();
100 | expect(editableText.childNodes.length).toEqual(1);
101 | expect(editableText.firstChild.textContent).toEqual(newText);
102 | }));
103 |
104 | it('should correctly render text when text transform text from non-empty string empty string', fakeAsync(() => {
105 | const text = `Kevin Durant`;
106 | component.value = [
107 | {
108 | type: 'paragraph',
109 | children: [{ text }],
110 | key: 'KD'
111 | }
112 | ];
113 | fixture.detectChanges();
114 | flush();
115 | fixture.detectChanges();
116 | const paragraphElement = document.querySelector('[data-slate-node="element"]');
117 | const editableText = paragraphElement.querySelector('[editable-text]');
118 | expect(editableText).toBeTruthy();
119 | expect(editableText.childNodes.length).toEqual(1);
120 | expect(editableText.firstChild.textContent).toEqual(text);
121 |
122 | Transforms.select(component.editor, [0]);
123 |
124 | Transforms.delete(component.editor);
125 | fixture.detectChanges();
126 | flush();
127 | fixture.detectChanges();
128 | expect(editableText.childNodes.length).toEqual(2);
129 | expect(editableText.firstChild.textContent).toEqual(`\uFEFF`);
130 | expect(editableText.lastElementChild.tagName).toEqual(`BR`);
131 | }));
132 | });
133 |
--------------------------------------------------------------------------------
/packages/src/components/string/string.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AfterViewInit,
3 | ChangeDetectionStrategy,
4 | Component,
5 | ElementRef,
6 | inject,
7 | Input,
8 | OnChanges,
9 | OnInit,
10 | ViewContainerRef
11 | } from '@angular/core';
12 | import { Node, Text } from 'slate';
13 | import { ViewContainerItem } from '../../view/container-item';
14 | import { SlateLeafContext, SlateStringContext } from '../../view/context';
15 | import { SlateDefaultString } from './default-string.component';
16 |
17 | @Component({
18 | selector: 'span[slateString]',
19 | template: '',
20 | changeDetection: ChangeDetectionStrategy.OnPush,
21 | standalone: true
22 | })
23 | export class SlateString extends ViewContainerItem implements OnInit, OnChanges, AfterViewInit {
24 | @Input() context: SlateLeafContext;
25 |
26 | protected elementRef = inject(ElementRef);
27 |
28 | protected viewContainerRef = inject(ViewContainerRef);
29 |
30 | ngOnInit(): void {
31 | this.createView();
32 | }
33 |
34 | ngOnChanges() {
35 | if (!this.initialized) {
36 | return;
37 | }
38 | this.updateView();
39 | }
40 |
41 | ngAfterViewInit() {
42 | this.elementRef.nativeElement.remove();
43 | }
44 |
45 | // COMPAT: If this is the last text node in an empty block, render a zero-
46 | // width space that will convert into a line break when copying and pasting
47 | // to support expected plain text.
48 | isLineBreakEmptyString() {
49 | return (
50 | this.context.leaf.text === '' &&
51 | this.context.parent.children[this.context.parent.children.length - 1] === this.context.text &&
52 | !this.viewContext.editor.isInline(this.context.parent) &&
53 | // [list-render] performance optimization: reduce the number of calls to the `Editor.string(editor, path)` method
54 | isEmpty(this.viewContext.editor, this.context.parent)
55 | );
56 | }
57 |
58 | // COMPAT: If the text is empty, it's because it's on the edge of an inline
59 | // node, so we render a zero-width space so that the selection can be
60 | // inserted next to it still.
61 | isEmptyText() {
62 | return this.context.leaf.text === '';
63 | }
64 |
65 | // COMPAT: Browsers will collapse trailing new lines at the end of blocks,
66 | // so we need to add an extra trailing new lines to prevent that.
67 | isCompatibleString() {
68 | return this.context.isLast && this.context.leaf.text.slice(-1) === '\n';
69 | }
70 |
71 | // COMPAT: Render text inside void nodes with a zero-width space.
72 | // So the node can contain selection but the text is not visible.
73 | isVoid() {
74 | return this.viewContext.editor.isVoid(this.context.parent);
75 | }
76 |
77 | getViewType() {
78 | if (this.isVoid()) {
79 | return this.viewContext.templateComponent.voidStringTemplate;
80 | }
81 |
82 | if (this.isLineBreakEmptyString()) {
83 | return SlateDefaultString;
84 | }
85 |
86 | if (this.isEmptyText()) {
87 | return this.viewContext.templateComponent.emptyTextTemplate;
88 | }
89 |
90 | if (this.isCompatibleString()) {
91 | return this.viewContext.templateComponent.compatibleStringTemplate;
92 | }
93 |
94 | return SlateDefaultString;
95 | }
96 |
97 | getType(): SlateStringContext['type'] {
98 | if (this.isLineBreakEmptyString()) {
99 | return 'lineBreakEmptyString';
100 | }
101 | return 'string';
102 | }
103 |
104 | getContext(): SlateStringContext {
105 | const stringType = this.getType();
106 | return {
107 | text: this.context.leaf.text,
108 | elementStringLength: Node.string(this.context.parent).length,
109 | type: stringType
110 | };
111 | }
112 |
113 | memoizedContext(prev: SlateStringContext, next: SlateStringContext): boolean {
114 | return false;
115 | }
116 | }
117 |
118 | /**
119 | * TODO: remove when bump slate
120 | * copy from slate
121 | * @param editor
122 | * @param element
123 | * @returns
124 | */
125 | export const isEmpty = (editor, element) => {
126 | const { children } = element;
127 | const [first] = children;
128 | return children.length === 0 || (children.length === 1 && Text.isText(first) && first.text === '' && !editor.isVoid(element));
129 | };
130 |
--------------------------------------------------------------------------------
/packages/src/components/string/template.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ context.text }}{{ '\uFEFF' }}
6 |
7 |
8 | {{ '\uFEFF' }}
9 |
10 |
11 | {{ '\uFEFF' }}
12 |
13 |
--------------------------------------------------------------------------------
/packages/src/components/string/template.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy, ViewChild, TemplateRef } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'slate-string-template',
5 | templateUrl: 'template.component.html',
6 | changeDetection: ChangeDetectionStrategy.OnPush,
7 | standalone: true
8 | })
9 | export class SlateStringTemplate {
10 | @ViewChild('compatibleStringTemplate', { read: TemplateRef, static: true })
11 | compatibleStringTemplate: TemplateRef;
12 |
13 | @ViewChild('voidStringTemplate', { read: TemplateRef, static: true })
14 | voidStringTemplate: TemplateRef;
15 |
16 | @ViewChild('emptyTextTemplate', { read: TemplateRef, static: true })
17 | emptyTextTemplate: TemplateRef;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/src/components/text/default-text.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy } from '@angular/core';
2 | import { BaseTextComponent } from '../../view/base';
3 | @Component({
4 | selector: 'span[slateDefaultText]',
5 | template: ``,
6 | changeDetection: ChangeDetectionStrategy.OnPush,
7 | host: {
8 | 'data-slate-node': 'text'
9 | }
10 | })
11 | export class SlateDefaultText extends BaseTextComponent {}
12 |
--------------------------------------------------------------------------------
/packages/src/components/text/token.ts:
--------------------------------------------------------------------------------
1 | import { InjectionToken } from '@angular/core';
2 | import { ComponentType } from '../../types/view';
3 | import { BaseTextComponent } from '../../view/base';
4 |
5 | export const SLATE_DEFAULT_TEXT_COMPONENT_TOKEN = new InjectionToken>('slate-default-text-token');
6 |
7 | export const SLATE_DEFAULT_VOID_TEXT_COMPONENT_TOKEN = new InjectionToken>(
8 | 'slate-default-void-text-token'
9 | );
10 |
--------------------------------------------------------------------------------
/packages/src/components/text/void-text.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectionStrategy, OnInit, OnChanges } from '@angular/core';
2 | import { BaseTextComponent } from '../../view/base';
3 |
4 | @Component({
5 | selector: 'span[slateVoidText]',
6 | template: ``,
7 | changeDetection: ChangeDetectionStrategy.OnPush,
8 | host: {
9 | 'data-slate-spacer': 'true',
10 | class: 'slate-spacer',
11 | 'data-slate-node': 'text'
12 | }
13 | })
14 | export class SlateVoidText extends BaseTextComponent implements OnInit, OnChanges {
15 | ngOnInit() {
16 | super.ngOnInit();
17 | }
18 |
19 | ngOnChanges() {
20 | if (!this.initialized) {
21 | return;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/src/custom-event/DOMTopLevelEventTypes.ts:
--------------------------------------------------------------------------------
1 | export const TOP_BLUR = 'blur';
2 | export const TOP_COMPOSITION_END = 'compositionend';
3 | export const TOP_COMPOSITION_START = 'compositionstart';
4 | export const TOP_COMPOSITION_UPDATE = 'compositionupdate';
5 | export const TOP_KEY_DOWN = 'keydown';
6 | export const TOP_KEY_PRESS = 'keypress';
7 | export const TOP_KEY_UP = 'keyup';
8 | export const TOP_MOUSE_DOWN = 'mousedown';
9 | export const TOP_MOUSE_MOVE = 'mousemove';
10 | export const TOP_MOUSE_OUT = 'mouseout';
11 | export const TOP_TEXT_INPUT = 'textInput';
12 | export const TOP_PASTE = 'paste';
13 |
--------------------------------------------------------------------------------
/packages/src/custom-event/FallbackCompositionState.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Facebook, Inc. and its affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the
5 | * LICENSE file in the root directory of this source tree.
6 | */
7 |
8 | /**
9 | * These variables store information about text content of a target node,
10 | * allowing comparison of content before and after a given event.
11 | *
12 | * Identify the node where selection currently begins, then observe
13 | * both its text content and its current position in the DOM. Since the
14 | * browser may natively replace the target node during composition, we can
15 | * use its position to find its replacement.
16 | *
17 | *
18 | */
19 |
20 | let root = null;
21 | let startText = null;
22 | let fallbackText = null;
23 |
24 | export function initialize(nativeEventTarget) {
25 | root = nativeEventTarget;
26 | startText = getText();
27 | return true;
28 | }
29 |
30 | export function reset() {
31 | root = null;
32 | startText = null;
33 | fallbackText = null;
34 | }
35 |
36 | export function getData() {
37 | if (fallbackText) {
38 | return fallbackText;
39 | }
40 |
41 | let start;
42 | const startValue = startText;
43 | const startLength = startValue.length;
44 | let end;
45 | const endValue = getText();
46 | const endLength = endValue.length;
47 |
48 | for (start = 0; start < startLength; start++) {
49 | if (startValue[start] !== endValue[start]) {
50 | break;
51 | }
52 | }
53 |
54 | const minEnd = startLength - start;
55 | for (end = 1; end <= minEnd; end++) {
56 | if (startValue[startLength - end] !== endValue[endLength - end]) {
57 | break;
58 | }
59 | }
60 |
61 | const sliceTail = end > 1 ? 1 - end : undefined;
62 | fallbackText = endValue.slice(start, sliceTail);
63 | return fallbackText;
64 | }
65 |
66 | export function getText() {
67 | if ('value' in root) {
68 | return root.value;
69 | }
70 | return root.textContent;
71 | }
72 |
--------------------------------------------------------------------------------
/packages/src/custom-event/before-input-polyfill.ts:
--------------------------------------------------------------------------------
1 | export const BEFORE_INPUT_EVENTS: {
2 | name: string;
3 | handler: string;
4 | isTriggerBeforeInput: boolean;
5 | }[] = [
6 | // { name: 'blur', handler: 'onBlur', isTriggerBeforeInput: true },
7 | // { name: 'compositionstart', handler: 'onCompositionStart', isTriggerBeforeInput: true },
8 | { name: 'compositionupdate', handler: null, isTriggerBeforeInput: true },
9 | // { name: 'compositionend', handler: 'onCompositionEnd', isTriggerBeforeInput: false },
10 | // { name: 'keydown', handler: 'onKeyDown', isTriggerBeforeInput: true },
11 | { name: 'keypress', handler: null, isTriggerBeforeInput: true },
12 | { name: 'keyup', handler: 'onKeyUp', isTriggerBeforeInput: true },
13 | { name: 'mousedown', handler: 'onMouseDown', isTriggerBeforeInput: true },
14 | { name: 'textInput', handler: null, isTriggerBeforeInput: true }
15 | // { name: 'paste', handler: 'onPaste', isTriggerBeforeInput: true }
16 | ];
17 |
--------------------------------------------------------------------------------
/packages/src/module.ts:
--------------------------------------------------------------------------------
1 | import { forwardRef, NgModule } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { SlateEditable } from './components/editable/editable.component';
4 | import { SlateDefaultText } from './components/text/default-text.component';
5 | import { SlateVoidText } from './components/text/void-text.component';
6 | import { SlateElement } from './components/element/element.component';
7 | import { SlateDefaultElement } from './components/element/default-element.component';
8 | import { SlateString } from './components/string/string.component';
9 | import { SlateStringTemplate } from './components/string/template.component';
10 | import { SlateChildren } from './components/children/children.component';
11 | import { SlateChildrenOutlet } from './components/children/children-outlet.component';
12 | import { SlateBlockCard } from './components/block-card/block-card.component';
13 | import { SlateDefaultLeaf } from './components/leaf/default-leaf.component';
14 | import { SlateLeaves } from './components/leaves/leaves.component';
15 | import { SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN } from './components/element/default-element.component.token';
16 | import { SlateDefaultString } from './components/string/default-string.component';
17 |
18 | @NgModule({
19 | imports: [
20 | CommonModule,
21 | SlateEditable,
22 | SlateDefaultElement,
23 | SlateElement,
24 | SlateVoidText,
25 | SlateDefaultText,
26 | SlateString,
27 | SlateStringTemplate,
28 | SlateChildren,
29 | SlateBlockCard,
30 | SlateLeaves,
31 | SlateDefaultLeaf,
32 | SlateDefaultString,
33 | SlateChildrenOutlet
34 | ],
35 | exports: [SlateEditable, SlateChildren, SlateChildrenOutlet, SlateElement, SlateLeaves, SlateString, SlateDefaultString],
36 | providers: [
37 | {
38 | provide: SLATE_DEFAULT_ELEMENT_COMPONENT_TOKEN,
39 | useValue: SlateDefaultElement
40 | }
41 | ]
42 | })
43 | export class SlateModule {}
44 |
--------------------------------------------------------------------------------
/packages/src/plugins/angular-editor.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, ComponentFixture, tick, fakeAsync, flush } from '@angular/core/testing';
2 | import { AngularEditor } from './angular-editor';
3 | import { BasicEditableComponent, configureBasicEditableTestingModule } from '../testing';
4 | import { Transforms, Element } from 'slate';
5 | import { createEmptyDocument } from 'slate-angular/testing/create-document';
6 |
7 | describe('AngularEditor', () => {
8 | let component: BasicEditableComponent;
9 | let fixture: ComponentFixture;
10 |
11 | beforeEach(fakeAsync(() => {
12 | configureBasicEditableTestingModule([BasicEditableComponent]);
13 | fixture = TestBed.createComponent(BasicEditableComponent);
14 | component = fixture.componentInstance;
15 | component.value = createEmptyDocument() as Element[];
16 | fixture.detectChanges();
17 | flush();
18 | fixture.detectChanges();
19 | }));
20 |
21 | afterEach(() => {
22 | fixture.destroy();
23 | });
24 |
25 | it('should fixed cursor after zero width char when text node is empty', () => {
26 | Transforms.select(component.editor, {
27 | anchor: {
28 | path: [0, 0],
29 | offset: 0
30 | },
31 | focus: {
32 | path: [0, 0],
33 | offset: 0
34 | }
35 | });
36 | const nativeRange = AngularEditor.toDOMRange(component.editor, component.editor.selection);
37 | expect(nativeRange.startOffset).toEqual(1);
38 | expect(nativeRange.endOffset).toEqual(1);
39 | });
40 |
41 | it('should fixed cursor to location after inserted text when insertText', fakeAsync(() => {
42 | const insertText = 'test';
43 | Transforms.select(component.editor, {
44 | anchor: {
45 | path: [0, 0],
46 | offset: 0
47 | },
48 | focus: {
49 | path: [0, 0],
50 | offset: 0
51 | }
52 | });
53 | tick(100);
54 | Transforms.insertText(component.editor, insertText);
55 | tick(100);
56 | const nativeRange = AngularEditor.toDOMRange(component.editor, component.editor.selection);
57 | expect(nativeRange.startOffset).toEqual(insertText.length);
58 | expect(nativeRange.endOffset).toEqual(insertText.length);
59 | }));
60 | });
61 |
--------------------------------------------------------------------------------
/packages/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of slate-angular
3 | */
4 | export * from './plugins/angular-editor';
5 | export * from './plugins/with-angular';
6 | export * from './components/editable/editable.component';
7 | export * from './components/element/element.component';
8 | export * from './components/string/string.component';
9 | export * from './components/string/default-string.component';
10 | export * from './components/children/children.component';
11 | export * from './components/children/children-outlet.component';
12 | export * from './components/leaves/leaves.component';
13 | export * from './module';
14 | export * from './types/error';
15 | export * from './view/base';
16 | export * from './view/context';
17 | export * from './view/context-change';
18 | export * from './utils';
19 | export * from './types';
20 |
--------------------------------------------------------------------------------
/packages/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @use '../components/block-card/block-card.component.scss';
2 | .slate-editable-container {
3 | display: block;
4 | outline: none;
5 | padding: 32px;
6 | white-space: break-spaces;
7 | & [contenteditable='true'] {
8 | outline: none;
9 | }
10 | & [data-slate-placeholder] {
11 | position: absolute;
12 | pointer-events: none;
13 | width: 100%;
14 | max-width: 100%;
15 | display: block;
16 | opacity: 0.333;
17 | user-select: none;
18 | text-decoration: none;
19 | top: 0;
20 | }
21 | & [data-slate-leaf='true'] {
22 | &.leaf-with-placeholder {
23 | position: relative;
24 | display: inline-block;
25 | width: 100%;
26 | }
27 | }
28 | &.firefox {
29 | // Compatible for firefox, there are two problems with using inline-block
30 | // Issue-1: paragraph height becomes taller
31 | // Issue-2: blocks focus movement on key down
32 | .leaf-with-placeholder {
33 | display: inline-flex !important;
34 | }
35 | }
36 | }
37 |
38 | .slate-spacer {
39 | height: 0;
40 | color: transparent;
41 | outline: none;
42 | position: absolute;
43 | }
44 |
--------------------------------------------------------------------------------
/packages/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js';
4 | import 'zone.js/testing';
5 | import { getTestBed } from '@angular/core/testing';
6 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
7 |
8 | // First, initialize the Angular testing environment.
9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
10 | teardown: { destroyAfterEach: false }
11 | });
12 |
--------------------------------------------------------------------------------
/packages/src/testing/advanced-editable.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild } from '@angular/core';
2 | import { createEditor, Element, NodeEntry, Text } from 'slate';
3 | import { SlateEditable } from '../components/editable/editable.component';
4 | import { withAngular } from '../plugins/with-angular';
5 | import { createDefaultDocument } from './create-document';
6 | import { TestingLeafComponent } from './leaf.component';
7 | import { AngularEditor } from '../plugins/angular-editor';
8 | import { DOMRange } from '../utils/dom';
9 |
10 | @Component({
11 | selector: 'basic-editable',
12 | template: `
13 |
22 | `,
23 | standalone: false
24 | })
25 | export class AdvancedEditableComponent implements OnInit {
26 | editor = withAngular(createEditor());
27 |
28 | value: any = createDefaultDocument();
29 |
30 | decorate = (nodeEntry: NodeEntry) => [];
31 |
32 | trackBy = (element: Element) => null;
33 |
34 | placeholder: string;
35 |
36 | @ViewChild(SlateEditable, { static: true })
37 | editableComponent: SlateEditable;
38 |
39 | generateDecorate(keywords: string) {
40 | this.decorate = ([node, path]) => {
41 | const ranges = [];
42 |
43 | if (keywords && Text.isText(node)) {
44 | const { text } = node;
45 | const parts = text.split(keywords);
46 | let offset = 0;
47 |
48 | parts.forEach((part, i) => {
49 | if (i !== 0) {
50 | ranges.push({
51 | anchor: { path, offset: offset - keywords.length },
52 | focus: { path, offset },
53 | highlight: true
54 | });
55 | }
56 |
57 | offset = offset + part.length + keywords.length;
58 | });
59 | }
60 |
61 | return ranges;
62 | };
63 | }
64 |
65 | renderLeaf = (text: Text) => {
66 | if (text['highlight']) {
67 | return TestingLeafComponent;
68 | }
69 | return null;
70 | };
71 |
72 | scrollSelectionIntoView = (editor: AngularEditor, domRange: DOMRange) => {};
73 |
74 | ngOnInit() {}
75 |
76 | constructor() {}
77 | }
78 |
--------------------------------------------------------------------------------
/packages/src/testing/basic-editable.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild } from '@angular/core';
2 | import { createEditor, Element } from 'slate';
3 | import { SlateEditable } from '../components/editable/editable.component';
4 | import { withAngular } from '../plugins/with-angular';
5 | import { createDefaultDocument } from './create-document';
6 | @Component({
7 | selector: 'basic-editable',
8 | template: ` `,
9 | standalone: false
10 | })
11 | export class BasicEditableComponent {
12 | editor = withAngular(createEditor());
13 |
14 | value: Element[] = createDefaultDocument() as Element[];
15 |
16 | @ViewChild(SlateEditable, { static: true })
17 | editableComponent: SlateEditable;
18 |
19 | ngModelChange() {}
20 |
21 | constructor() {}
22 | }
23 |
--------------------------------------------------------------------------------
/packages/src/testing/create-document.ts:
--------------------------------------------------------------------------------
1 | export function createEmptyDocument() {
2 | return [
3 | {
4 | type: 'paragraph',
5 | children: [{ text: '' }]
6 | }
7 | ];
8 | }
9 | export function createDefaultDocument() {
10 | return [
11 | {
12 | type: 'paragraph',
13 | children: [{ text: 'This is editable text!' }]
14 | }
15 | ];
16 | }
17 |
18 | export function createMultipleParagraph() {
19 | return [
20 | {
21 | type: 'paragraph',
22 | children: [{ text: '0' }]
23 | },
24 | {
25 | type: 'paragraph',
26 | children: [{ text: '1' }]
27 | },
28 | {
29 | type: 'paragraph',
30 | children: [{ text: '2' }]
31 | },
32 | {
33 | type: 'paragraph',
34 | children: [{ text: '3' }]
35 | },
36 | {
37 | type: 'paragraph',
38 | children: [{ text: '4' }]
39 | },
40 | {
41 | type: 'paragraph',
42 | children: [{ text: '5' }]
43 | }
44 | ];
45 | }
46 |
--------------------------------------------------------------------------------
/packages/src/testing/dispatcher-events.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright Google LLC All Rights Reserved.
4 | *
5 | * Use of this source code is governed by an MIT-style license that can be
6 | * found in the LICENSE file at https://angular.io/license
7 | */
8 | import { createFakeEvent, createKeyboardEvent, createMouseEvent, createTouchEvent } from './events';
9 | import { ModifierKeys } from './types';
10 |
11 | /**
12 | * Utility to dispatch any event on a Node.
13 | * @docs-private
14 | */
15 | export function dispatchEvent(node: Node | Window, event: Event): Event {
16 | node.dispatchEvent(event);
17 | return event;
18 | }
19 |
20 | /**
21 | * Shorthand to dispatch a fake event on a specified node.
22 | * @docs-private
23 | */
24 | export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event {
25 | return dispatchEvent(node, createFakeEvent(type, canBubble));
26 | }
27 |
28 | /**
29 | * Shorthand to dispatch a keyboard event with a specified key code.
30 | * @docs-private
31 | */
32 | export function dispatchKeyboardEvent(
33 | node: Node,
34 | type: string,
35 | keyCode?: number,
36 | key?: string,
37 | target?: Element,
38 | modifiers?: ModifierKeys
39 | ): KeyboardEvent {
40 | return dispatchEvent(node, createKeyboardEvent(type, keyCode, key, target, modifiers)) as KeyboardEvent;
41 | }
42 |
43 | /**
44 | * Shorthand to dispatch a mouse event on the specified coordinates.
45 | * @docs-private
46 | */
47 | export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0, event = createMouseEvent(type, x, y)): MouseEvent {
48 | return dispatchEvent(node, event) as MouseEvent;
49 | }
50 |
51 | /**
52 | * Shorthand to dispatch a touch event on the specified coordinates.
53 | * @docs-private
54 | */
55 | export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) {
56 | return dispatchEvent(node, createTouchEvent(type, x, y));
57 | }
58 |
--------------------------------------------------------------------------------
/packages/src/testing/editable-with-outlet.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewChild } from '@angular/core';
2 | import { createEditor, Element } from 'slate';
3 | import { SlateEditable } from '../components/editable/editable.component';
4 | import { withAngular } from '../plugins/with-angular';
5 | import { SlateChildrenOutlet } from '../components/children/children-outlet.component';
6 | import { BaseElementComponent } from '../view/base';
7 |
8 | const customType = 'custom-with-outlet';
9 |
10 | @Component({
11 | selector: 'editable-with-outlet',
12 | template: ` `,
18 | standalone: false
19 | })
20 | export class EditableWithOutletComponent {
21 | editor = withAngular(createEditor());
22 |
23 | value: Element[] = createDefaultDocument() as Element[];
24 |
25 | @ViewChild(SlateEditable, { static: true })
26 | editableComponent: SlateEditable;
27 |
28 | renderElement() {
29 | return (element: Element) => {
30 | if ((element.type as any) === customType) {
31 | return TestElementWithOutletComponent;
32 | }
33 | return null;
34 | };
35 | }
36 |
37 | ngModelChange() {}
38 |
39 | constructor() {}
40 | }
41 |
42 | export function createDefaultDocument() {
43 | return [
44 | {
45 | type: customType,
46 | children: [
47 | {
48 | type: 'paragraph',
49 | children: [{ text: 'This is editable text!' }]
50 | }
51 | ]
52 | }
53 | ];
54 | }
55 |
56 | @Component({
57 | selector: 'div[test-element-with-outlet]',
58 | template: `
59 | before
60 |
61 | after
62 | `,
63 | host: {
64 | class: 'test-element-with-outlet'
65 | },
66 | imports: [SlateChildrenOutlet]
67 | })
68 | export class TestElementWithOutletComponent extends BaseElementComponent {}
69 |
--------------------------------------------------------------------------------
/packages/src/testing/element-focus.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright Google LLC All Rights Reserved.
4 | *
5 | * Use of this source code is governed by an MIT-style license that can be
6 | * found in the LICENSE file at https://angular.io/license
7 | */
8 |
9 | import { dispatchFakeEvent } from './dispatcher-events';
10 |
11 | /**
12 | * Patches an elements focus and blur methods to emit events consistently and predictably.
13 | * This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
14 | * while others won't fire them at all if the browser window is not focused.
15 | */
16 | export function patchElementFocus(element: HTMLElement) {
17 | element.focus = () => dispatchFakeEvent(element, 'focus');
18 | element.blur = () => dispatchFakeEvent(element, 'blur');
19 | }
20 |
--------------------------------------------------------------------------------
/packages/src/testing/events.ts:
--------------------------------------------------------------------------------
1 | import { ModifierKeys } from './types';
2 |
3 | /**
4 | * Creates a browser MouseEvent with the specified options.
5 | * @docs-private
6 | */
7 | export function createMouseEvent(type: string, x = 0, y = 0, button = 0) {
8 | const event = document.createEvent('MouseEvent');
9 | const originalPreventDefault = event.preventDefault.bind(event);
10 |
11 | event.initMouseEvent(
12 | type,
13 | true /* canBubble */,
14 | true /* cancelable */,
15 | window /* view */,
16 | 0 /* detail */,
17 | x /* screenX */,
18 | y /* screenY */,
19 | x /* clientX */,
20 | y /* clientY */,
21 | false /* ctrlKey */,
22 | false /* altKey */,
23 | false /* shiftKey */,
24 | false /* metaKey */,
25 | button /* button */,
26 | null /* relatedTarget */
27 | );
28 |
29 | // `initMouseEvent` doesn't allow us to pass the `buttons` and
30 | // defaults it to 0 which looks like a fake event.
31 | Object.defineProperty(event, 'buttons', { get: () => 1 });
32 |
33 | // IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
34 | event.preventDefault = function () {
35 | Object.defineProperty(event, 'defaultPrevented', { get: () => true });
36 | return originalPreventDefault();
37 | };
38 |
39 | return event;
40 | }
41 |
42 | /**
43 | * Creates a browser TouchEvent with the specified pointer coordinates.
44 | * @docs-private
45 | */
46 | export function createTouchEvent(type: string, pageX = 0, pageY = 0) {
47 | // In favor of creating events that work for most of the browsers, the event is created
48 | // as a basic UI Event. The necessary details for the event will be set manually.
49 | const event = document.createEvent('UIEvent');
50 | const touchDetails = { pageX, pageY };
51 |
52 | // TS3.6 removes the initUIEvent method and suggests porting to "new UIEvent()".
53 | (event as any).initUIEvent(type, true, true, window, 0);
54 |
55 | // Most of the browsers don't have a "initTouchEvent" method that can be used to define
56 | // the touch details.
57 | Object.defineProperties(event, {
58 | touches: { value: [touchDetails] },
59 | targetTouches: { value: [touchDetails] },
60 | changedTouches: { value: [touchDetails] }
61 | });
62 |
63 | return event;
64 | }
65 |
66 | /**
67 | * Dispatches a keydown event from an element.
68 | * @docs-private
69 | */
70 | export function createKeyboardEvent(type: string, keyCode: number = 0, key: string = '', target?: Element, modifiers: ModifierKeys = {}) {
71 | const event = document.createEvent('KeyboardEvent') as any;
72 | const originalPreventDefault = event.preventDefault;
73 |
74 | // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`.
75 | if (event.initKeyEvent) {
76 | event.initKeyEvent(type, true, true, window, modifiers.control, modifiers.alt, modifiers.shift, modifiers.meta, keyCode);
77 | } else {
78 | // `initKeyboardEvent` expects to receive modifiers as a whitespace-delimited string
79 | // See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/initKeyboardEvent
80 | let modifiersList = '';
81 |
82 | if (modifiers.control) {
83 | modifiersList += 'Control ';
84 | }
85 |
86 | if (modifiers.alt) {
87 | modifiersList += 'Alt ';
88 | }
89 |
90 | if (modifiers.shift) {
91 | modifiersList += 'Shift ';
92 | }
93 |
94 | if (modifiers.meta) {
95 | modifiersList += 'Meta ';
96 | }
97 |
98 | event.initKeyboardEvent(
99 | type,
100 | true /* canBubble */,
101 | true /* cancelable */,
102 | window /* view */,
103 | 0 /* char */,
104 | key /* key */,
105 | 0 /* location */,
106 | modifiersList.trim() /* modifiersList */,
107 | false /* repeat */
108 | );
109 | }
110 |
111 | // Webkit Browsers don't set the keyCode when calling the init function.
112 | // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735
113 | Object.defineProperties(event, {
114 | keyCode: { get: () => keyCode },
115 | key: { get: () => key },
116 | target: { get: () => target },
117 | ctrlKey: { get: () => !!modifiers.control },
118 | altKey: { get: () => !!modifiers.alt },
119 | shiftKey: { get: () => !!modifiers.shift },
120 | metaKey: { get: () => !!modifiers.meta }
121 | });
122 |
123 | // IE won't set `defaultPrevented` on synthetic events so we need to do it manually.
124 | event.preventDefault = function () {
125 | Object.defineProperty(event, 'defaultPrevented', { get: () => true });
126 | return originalPreventDefault.apply(this, arguments);
127 | };
128 |
129 | return event;
130 | }
131 |
132 | /**
133 | * Creates a fake event object with any desired event type.
134 | * @docs-private
135 | */
136 | export function createFakeEvent(type: string, canBubble = false, cancelable = true) {
137 | const event = document.createEvent('Event');
138 | event.initEvent(type, canBubble, cancelable);
139 | return event;
140 | }
141 |
--------------------------------------------------------------------------------
/packages/src/testing/image-editable.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewChild } from '@angular/core';
2 | import { createEditor, Editor, Element, Node } from 'slate';
3 | import { SlateEditable } from '../components/editable/editable.component';
4 | import { withAngular } from '../plugins/with-angular';
5 |
6 | @Component({
7 | selector: 'image-editable',
8 | template: ` `,
9 | standalone: false
10 | })
11 | export class ImageEditableComponent implements OnInit {
12 | editor = withImage(withAngular(createEditor()));
13 |
14 | value = [
15 | {
16 | type: 'image',
17 | url: 'https://source.unsplash.com/kFrdX5IeQzI',
18 | children: [
19 | {
20 | text: ''
21 | }
22 | ]
23 | }
24 | ];
25 |
26 | @ViewChild(SlateEditable, { static: true })
27 | editableComponent: SlateEditable;
28 |
29 | ngOnInit() {}
30 |
31 | constructor() {}
32 | }
33 |
34 | const withImage = (editor: Editor) => {
35 | const { isBlockCard, isVoid } = editor;
36 | editor.isBlockCard = (node: Element) => {
37 | if (Element.isElement(node) && node.type === 'image') {
38 | return true;
39 | }
40 | return isBlockCard(node);
41 | };
42 | editor.isVoid = (node: Element) => {
43 | if (Element.isElement(node) && node.type === 'image') {
44 | return true;
45 | }
46 | return isVoid(node);
47 | };
48 | return editor;
49 | };
50 |
--------------------------------------------------------------------------------
/packages/src/testing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dispatcher-events';
2 | export * from './events';
3 | export * from './element-focus';
4 | export * from './module';
5 | export * from './basic-editable.component';
6 | export * from './advanced-editable.component';
7 | export * from './image-editable.component';
8 | export * from './editable-with-outlet.component';
9 | export * from './leaf.component';
10 |
--------------------------------------------------------------------------------
/packages/src/testing/leaf.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, Renderer2 } from '@angular/core';
2 | import { BaseLeafComponent } from 'slate-angular';
3 |
4 | @Component({
5 | selector: 'span[testingLeaf]',
6 | template: ` `,
7 | host: {
8 | class: 'testing-leaf'
9 | },
10 | standalone: false
11 | })
12 | export class TestingLeafComponent extends BaseLeafComponent {
13 | private renderer = inject(Renderer2);
14 |
15 | onContextChange() {
16 | super.onContextChange();
17 | this.changeStyle();
18 | }
19 |
20 | changeStyle() {
21 | const backgroundColor = this.leaf['highlight'] ? '#ffeeba' : null;
22 | this.renderer.setStyle(this.nativeElement, 'backgroundColor', backgroundColor);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/src/testing/module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { Provider } from '@angular/core';
3 | import { TestBed } from '@angular/core/testing';
4 | import { BrowserModule } from '@angular/platform-browser';
5 | import { SlateModule } from '../module';
6 | import { FormsModule } from '@angular/forms';
7 |
8 | export function configureBasicEditableTestingModule(declarations: any[], entryComponents: any[] = [], providers: Provider[] = []) {
9 | TestBed.configureTestingModule({
10 | declarations: declarations,
11 | imports: [CommonModule, BrowserModule, SlateModule, FormsModule],
12 | providers: [...providers],
13 | teardown: { destroyAfterEach: false }
14 | }).compileComponents();
15 | }
16 |
--------------------------------------------------------------------------------
/packages/src/testing/types.ts:
--------------------------------------------------------------------------------
1 | export interface ModifierKeys {
2 | control?: boolean;
3 | alt?: boolean;
4 | shift?: boolean;
5 | meta?: boolean;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/src/types/clipboard.ts:
--------------------------------------------------------------------------------
1 | import { Element } from 'slate';
2 |
3 | export interface ClipboardData {
4 | files?: File[];
5 | elements?: Element[];
6 | text?: string;
7 | html?: string;
8 | }
9 |
10 | export type OriginEvent = 'drag' | 'copy' | 'cut';
11 |
--------------------------------------------------------------------------------
/packages/src/types/error.ts:
--------------------------------------------------------------------------------
1 | import { Descendant } from 'slate';
2 |
3 | export enum SlateErrorCode {
4 | ToNativeSelectionError = 2100,
5 | ToSlateSelectionError = 2101,
6 | OnDOMBeforeInputError = 2102,
7 | OnSyntheticBeforeInputError = 2103,
8 | OnDOMKeydownError = 2104,
9 | GetStartPointError = 2105,
10 | NotFoundPreviousRootNodeError = 3100,
11 | InvalidValueError = 4100
12 | }
13 |
14 | export interface SlateError {
15 | code?: SlateErrorCode | number;
16 | name?: string;
17 | nativeError?: Error;
18 | data?: Descendant[];
19 | }
20 |
--------------------------------------------------------------------------------
/packages/src/types/feature.ts:
--------------------------------------------------------------------------------
1 | import { BaseRange } from 'slate';
2 |
3 | export interface SlatePlaceholder extends BaseRange {
4 | placeholder: string;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './error';
2 | export * from './view';
3 | export * from './feature';
4 | export * from './clipboard';
5 |
6 | export type SafeAny = any;
7 |
--------------------------------------------------------------------------------
/packages/src/types/view.ts:
--------------------------------------------------------------------------------
1 | import { TemplateRef } from '@angular/core';
2 |
3 | export interface ComponentType {
4 | new (...args: any[]): T;
5 | }
6 |
7 | export type ViewType = TemplateRef | ComponentType;
8 |
--------------------------------------------------------------------------------
/packages/src/utils/block-card.ts:
--------------------------------------------------------------------------------
1 | import { DOMNode, DOMSelection } from './dom';
2 |
3 | export const FAKE_LEFT_BLOCK_CARD_OFFSET = -1;
4 |
5 | export const FAKE_RIGHT_BLOCK_CARD_OFFSET = -2;
6 |
7 | export function hasBlockCardWithNode(node: DOMNode) {
8 | return node && (node.parentElement.hasAttribute('card-target') || (node instanceof HTMLElement && node.hasAttribute('card-target')));
9 | }
10 |
11 | export function hasBlockCard(selection: DOMSelection) {
12 | return hasBlockCardWithNode(selection?.anchorNode) || hasBlockCardWithNode(selection?.focusNode);
13 | }
14 |
15 | export function getCardTargetAttribute(node: DOMNode) {
16 | return node.parentElement.attributes['card-target'] || (node instanceof HTMLElement && node.attributes['card-target']);
17 | }
18 |
19 | export function isCardLeft(node: DOMNode) {
20 | const cardTarget = getCardTargetAttribute(node);
21 | return cardTarget && cardTarget.nodeValue === 'card-left';
22 | }
23 |
24 | export function isCardLeftByTargetAttr(targetAttr: any) {
25 | return targetAttr && targetAttr.nodeValue === 'card-left';
26 | }
27 |
28 | export function isCardRightByTargetAttr(targetAttr: any) {
29 | return targetAttr && targetAttr.nodeValue === 'card-right';
30 | }
31 |
32 | export function isCardCenterByTargetAttr(targetAttr: any) {
33 | return targetAttr && targetAttr.nodeValue === 'card-center';
34 | }
35 |
--------------------------------------------------------------------------------
/packages/src/utils/clipboard/clipboard.ts:
--------------------------------------------------------------------------------
1 | import { Element } from 'slate';
2 | import { ClipboardData } from '../../types/clipboard';
3 | import { SlateFragmentAttributeKey, getSlateFragmentAttribute } from '../dom';
4 | import { isClipboardReadSupported, isClipboardWriteSupported, isClipboardWriteTextSupported } from './common';
5 | import { getDataTransferClipboard, setDataTransferClipboard, setDataTransferClipboardText } from './data-transfer';
6 | import { getNavigatorClipboard, setNavigatorClipboard } from './navigator-clipboard';
7 |
8 | export const buildHTMLText = (wrapper: HTMLElement, attach: HTMLElement, data: Element[]) => {
9 | const stringObj = JSON.stringify(data);
10 | const encoded = window.btoa(encodeURIComponent(stringObj));
11 | attach.setAttribute(SlateFragmentAttributeKey, encoded);
12 | return wrapper.innerHTML;
13 | };
14 |
15 | export const getClipboardFromHTMLText = (html: string): ClipboardData => {
16 | const fragmentAttribute = getSlateFragmentAttribute(html);
17 | if (fragmentAttribute) {
18 | try {
19 | const decoded = decodeURIComponent(window.atob(fragmentAttribute));
20 | const result = JSON.parse(decoded);
21 | if (result && Array.isArray(result) && result.length > 0) {
22 | return {
23 | elements: result
24 | };
25 | }
26 | } catch (error) {
27 | console.error(error);
28 | return null;
29 | }
30 | }
31 | return null;
32 | };
33 |
34 | export const createClipboardData = (html: string, elements: Element[], text: string, files: File[]): ClipboardData => {
35 | const data = { elements, text, html, files };
36 | return data;
37 | };
38 |
39 | export const getClipboardData = async (dataTransfer?: DataTransfer): Promise => {
40 | let clipboardData = null;
41 | if (dataTransfer) {
42 | let filesData = {};
43 | if (dataTransfer.files.length) {
44 | filesData = { ...filesData, files: Array.from(dataTransfer.files) };
45 | }
46 | clipboardData = getDataTransferClipboard(dataTransfer);
47 | return { ...clipboardData, ...filesData };
48 | }
49 | if (isClipboardReadSupported()) {
50 | return await getNavigatorClipboard();
51 | }
52 | return clipboardData;
53 | };
54 |
55 | /**
56 | * @param wrapper get wrapper.innerHTML string which will be written in clipboard
57 | * @param attach attach must be child element of wrapper which will be attached json data
58 | * @returns void
59 | */
60 | export const setClipboardData = async (
61 | clipboardData: ClipboardData,
62 | wrapper: HTMLElement,
63 | attach: HTMLElement,
64 | dataTransfer?: Pick
65 | ) => {
66 | if (!clipboardData) {
67 | return;
68 | }
69 | const { elements, text } = clipboardData;
70 | if (isClipboardWriteSupported()) {
71 | const htmlText = buildHTMLText(wrapper, attach, elements);
72 | // TODO
73 | // maybe fail to write when copy some cell in table
74 | return await setNavigatorClipboard(htmlText, elements, text);
75 | }
76 |
77 | if (dataTransfer) {
78 | const htmlText = buildHTMLText(wrapper, attach, elements);
79 | setDataTransferClipboard(dataTransfer, htmlText);
80 | setDataTransferClipboardText(dataTransfer, text);
81 | return;
82 | }
83 |
84 | // Compatible with situations where navigator.clipboard.write is not supported and dataTransfer is empty
85 | // Such as contextmenu copy in Firefox.
86 | if (isClipboardWriteTextSupported()) {
87 | const htmlText = buildHTMLText(wrapper, attach, elements);
88 | return await navigator.clipboard.writeText(htmlText);
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/packages/src/utils/clipboard/common.ts:
--------------------------------------------------------------------------------
1 | export const isClipboardReadSupported = () => {
2 | return 'clipboard' in navigator && 'read' in navigator.clipboard;
3 | };
4 |
5 | export const isClipboardWriteSupported = () => {
6 | return 'clipboard' in navigator && 'write' in navigator.clipboard;
7 | };
8 |
9 | export const isClipboardWriteTextSupported = () => {
10 | return 'clipboard' in navigator && 'writeText' in navigator.clipboard;
11 | };
12 |
13 | export const isClipboardFile = (item: ClipboardItem) => {
14 | return item.types.find(i => i.match(/^image\//));
15 | };
16 |
17 | export const isInvalidTable = (nodes: Element[] = []) => {
18 | return nodes.some(node => node.tagName.toLowerCase() === 'tr');
19 | };
20 |
21 | export const stripHtml = (html: string) => {
22 | // See
23 | const doc = document.implementation.createHTMLDocument('');
24 | doc.documentElement.innerHTML = html.trim();
25 | return doc.body.textContent || doc.body.innerText || '';
26 | };
27 |
28 | export const blobAsString = (blob: Blob) => {
29 | return new Promise((resolve, reject) => {
30 | const reader = new FileReader();
31 | reader.addEventListener('loadend', () => {
32 | const text = reader.result;
33 | resolve(text as string);
34 | });
35 | reader.addEventListener('error', () => {
36 | reject(reader.error);
37 | });
38 | reader.readAsText(blob);
39 | });
40 | };
41 |
42 | export const completeTable = (fragment: DocumentFragment) => {
43 | const result = document.createDocumentFragment();
44 | const table = document.createElement('table');
45 | result.appendChild(table);
46 | table.appendChild(fragment);
47 | return result;
48 | };
49 |
--------------------------------------------------------------------------------
/packages/src/utils/clipboard/data-transfer.ts:
--------------------------------------------------------------------------------
1 | import { getClipboardFromHTMLText } from './clipboard';
2 | import { ClipboardData } from '../../types/clipboard';
3 |
4 | export const setDataTransferClipboard = (dataTransfer: Pick | null, htmlText: string) => {
5 | dataTransfer?.setData(`text/html`, htmlText);
6 | };
7 |
8 | export const setDataTransferClipboardText = (data: Pick | null, text: string) => {
9 | data?.setData(`text/plain`, text);
10 | };
11 |
12 | export const getDataTransferClipboard = (data: Pick | null): ClipboardData => {
13 | const html = data?.getData(`text/html`);
14 | if (html) {
15 | const htmlClipboardData = getClipboardFromHTMLText(html);
16 | if (htmlClipboardData) {
17 | return htmlClipboardData;
18 | }
19 | const textData = getDataTransferClipboardText(data);
20 | if (textData) {
21 | return {
22 | html,
23 | ...textData
24 | };
25 | } else {
26 | return { html };
27 | }
28 | } else {
29 | const textData = getDataTransferClipboardText(data);
30 | return textData;
31 | }
32 | };
33 |
34 | export const getDataTransferClipboardText = (data: Pick | null): ClipboardData => {
35 | if (!data) {
36 | return null;
37 | }
38 | const text = data?.getData(`text/plain`);
39 | if (text) {
40 | const htmlClipboardData = getClipboardFromHTMLText(text);
41 | if (htmlClipboardData) {
42 | return htmlClipboardData;
43 | }
44 | }
45 | return { text };
46 | };
47 |
--------------------------------------------------------------------------------
/packages/src/utils/clipboard/index.ts:
--------------------------------------------------------------------------------
1 | export * from './clipboard';
2 | export * from './data-transfer';
3 | export * from './navigator-clipboard';
4 | export * from './common';
5 |
--------------------------------------------------------------------------------
/packages/src/utils/clipboard/navigator-clipboard.ts:
--------------------------------------------------------------------------------
1 | import { Element } from 'slate';
2 | import { getClipboardFromHTMLText } from './clipboard';
3 | import { blobAsString, isClipboardFile, isClipboardReadSupported, isClipboardWriteSupported, stripHtml } from './common';
4 | import { ClipboardData } from '../../types/clipboard';
5 |
6 | export const setNavigatorClipboard = async (htmlText: string, data: Element[], text: string = '') => {
7 | let textClipboard = text;
8 | if (isClipboardWriteSupported()) {
9 | await navigator.clipboard.write([
10 | new ClipboardItem({
11 | 'text/html': new Blob([htmlText], {
12 | type: 'text/html'
13 | }),
14 | 'text/plain': new Blob([textClipboard ?? JSON.stringify(data)], { type: 'text/plain' })
15 | })
16 | ]);
17 | }
18 | };
19 |
20 | export const getNavigatorClipboard = async () => {
21 | if (!isClipboardReadSupported()) {
22 | return null;
23 | }
24 | const clipboardItems = await navigator.clipboard.read();
25 | let clipboardData: ClipboardData = {};
26 |
27 | if (Array.isArray(clipboardItems) && clipboardItems[0] instanceof ClipboardItem) {
28 | for (const item of clipboardItems) {
29 | if (isClipboardFile(item)) {
30 | const clipboardFiles = item.types.filter(type => type.match(/^image\//));
31 | const fileBlobs = await Promise.all(clipboardFiles.map(type => item.getType(type)!));
32 | const urls = (fileBlobs.filter(Boolean) as (File | Blob)[]).map(blob => URL.createObjectURL(blob));
33 | const files = await Promise.all(
34 | urls.map(async url => {
35 | const blob = await (await fetch(url)).blob();
36 | return new File([blob], 'file', { type: blob.type });
37 | })
38 | );
39 | clipboardData = {
40 | ...clipboardData,
41 | files
42 | };
43 | }
44 | if (item.types.includes('text/html')) {
45 | const htmlContent = await blobAsString(await item.getType('text/html'));
46 | const htmlClipboardData = getClipboardFromHTMLText(htmlContent);
47 | if (htmlClipboardData) {
48 | clipboardData = { ...clipboardData, ...htmlClipboardData };
49 | return clipboardData;
50 | }
51 | if (htmlContent && htmlContent.trim()) {
52 | clipboardData = { ...clipboardData, html: htmlContent };
53 | }
54 | }
55 | if (item.types.includes('text/plain')) {
56 | const textContent = await blobAsString(await item.getType('text/plain'));
57 | clipboardData = {
58 | ...clipboardData,
59 | text: stripHtml(textContent)
60 | };
61 | }
62 | }
63 | }
64 | return clipboardData;
65 | };
66 |
--------------------------------------------------------------------------------
/packages/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const TRIPLE_CLICK = 3;
2 |
--------------------------------------------------------------------------------
/packages/src/utils/environment.ts:
--------------------------------------------------------------------------------
1 | export const IS_IOS =
2 | typeof navigator !== 'undefined' &&
3 | typeof window !== 'undefined' &&
4 | /iPad|iPhone|iPod/.test(navigator.userAgent) &&
5 | !(window as any).MSStream;
6 |
7 | export const IS_APPLE = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent);
8 |
9 | export const IS_ANDROID = typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent);
10 |
11 | export const IS_FIREFOX = typeof navigator !== 'undefined' && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
12 |
13 | export const IS_SAFARI = typeof navigator !== 'undefined' && /Version\/[\d\.]+.*Safari/.test(navigator.userAgent);
14 |
15 | // "modern" Edge was released at 79.x
16 | export const IS_EDGE_LEGACY = typeof navigator !== 'undefined' && /Edge?\/(?:[0-6][0-9]|[0-7][0-8])(?:\.)/i.test(navigator.userAgent);
17 |
18 | export const IS_CHROME = typeof navigator !== 'undefined' && /Chrome/i.test(navigator.userAgent);
19 |
20 | // Native beforeInput events don't work well with react on Chrome 75 and older, Chrome 76+ can use beforeInput
21 | export const IS_CHROME_LEGACY =
22 | typeof navigator !== 'undefined' &&
23 | /Chrome?\/(?:[0-7][0-5]|[0-6][0-9])/i.test(navigator.userAgent) &&
24 | // Exclude Chrome version greater than 3 bits,Chrome releases v100 on 2022.03.29
25 | !/Chrome?\/(?:\d{3,})/i.test(navigator.userAgent);
26 |
27 | // Firefox did not support `beforeInput` until `v87`.
28 | export const IS_FIREFOX_LEGACY =
29 | typeof navigator !== 'undefined' && /^(?!.*Seamonkey)(?=.*Firefox\/(?:[0-7][0-9]|[0-8][0-6])(?:\.)).*/i.test(navigator.userAgent);
30 |
31 | // qq browser
32 | export const IS_QQBROWSER = typeof navigator !== 'undefined' && /.*QQBrowser/.test(navigator.userAgent);
33 |
34 | // UC mobile browser
35 | export const IS_UC_MOBILE = typeof navigator !== 'undefined' && /.*UCBrowser/.test(navigator.userAgent);
36 |
37 | // Wechat browser
38 | export const IS_WECHATBROWSER = typeof navigator !== 'undefined' && /.*Wechat/.test(navigator.userAgent);
39 |
40 | // COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event
41 | // Chrome Legacy doesn't support `beforeinput` correctly
42 | export const HAS_BEFORE_INPUT_SUPPORT =
43 | !IS_CHROME_LEGACY &&
44 | !IS_EDGE_LEGACY &&
45 | // globalThis is undefined in older browsers
46 | typeof globalThis !== 'undefined' &&
47 | globalThis.InputEvent &&
48 | // @ts-ignore The `getTargetRanges` property isn't recognized.
49 | typeof globalThis.InputEvent.prototype.getTargetRanges === 'function';
50 |
--------------------------------------------------------------------------------
/packages/src/utils/global-normalize.spec.ts:
--------------------------------------------------------------------------------
1 | import { Element } from 'slate';
2 | import { check, normalize } from './global-normalize';
3 |
4 | describe('global-normalize', () => {
5 | const invalidData3: any[] = [
6 | {
7 | type: 'paragraph',
8 | children: [{ text: '' }]
9 | },
10 | {
11 | type: 'numbered-list',
12 | children: [
13 | {
14 | type: 'list-item',
15 | children: [
16 | {
17 | type: 'paragraph',
18 | children: [
19 | {
20 | text: ''
21 | }
22 | ]
23 | },
24 | {
25 | type: 'paragraph',
26 | children: []
27 | }
28 | ]
29 | }
30 | ]
31 | }
32 | ];
33 |
34 | it('should return true', () => {
35 | const validData: Element[] = [
36 | {
37 | type: 'paragraph',
38 | children: [
39 | { text: 'This is editable ' },
40 | { text: 'rich', bold: true },
41 | { text: ' text, ' },
42 | { text: 'much', bold: true, italic: true },
43 | { text: ' better than a ' },
44 | { text: '