diff --git a/.changeset/chilly-moose-dance.md b/.changeset/chilly-moose-dance.md new file mode 100644 index 000000000..acd84c57e --- /dev/null +++ b/.changeset/chilly-moose-dance.md @@ -0,0 +1,27 @@ +--- +'@plait/draw': minor +'@plait/flow': minor +'@plait/mind': minor +--- + +Framework agnostic refactoring: + +1. Use `measureElement` to measure text width and height + +2. Use `text-manage` in `@plait/common` to render text + +3. Provide an overridable method `renderEmoji` in `@plait/mind` to remove the dependency on Angular and transform the response generator + +4. Provide an overridable method `renderLabelIcon` in `@plait/flow` to remove the dependency on Angular and transform the response generator + +--- + +Framework agnostic 改造: + +1. 改用 `measureElement` 测量文本宽高 + +2. 改用 `@plait/common` 中的 `text-manage` 实现文本的渲染 + +3. `@plait/mind` 中提供可重写方法 `renderEmoji` 解除对 Angular 的依赖,并且改造响应 generator + +4. `@plait/flow` 中提供可重写方法 `renderLabelIcon` 解除对 Angular 的依赖,并且改造响应 generator diff --git a/.changeset/hip-ducks-do.md b/.changeset/hip-ducks-do.md new file mode 100644 index 000000000..540c54537 --- /dev/null +++ b/.changeset/hip-ducks-do.md @@ -0,0 +1,23 @@ +--- +'@plait/common': minor +'@plait/core': minor +--- + +Framework agnostic refactoring: + +1. Reimplement text-manage in `@plait/common`, and remove the dependency on front-end frameworks such as Angular/React by providing an overridable method renderText + +2. Provide an overridable method renderImage in `@plait/common` + +3. Implement the `measureElement` method based on `canvas`, calculate the width and height of the text in Plait through the `measureText` API of `canvas`, and change all places that originally called `getTextSize` or `measureDivSize` to call `measureElement` + +4. Move the part of `@plait/core` that depends on Angular to `@plait/angular-board` + +--- + +Framework agnostic 改造: + +1. 在 `@plait/common` 中重新实现 text-manage,通过提供可重写方法 renderText 解除和 Angular/React 等前端框架的强依赖 +2. 在 `@plait/common` 中提供可重写方法 renderImage +3. 基于 `canvas` 实现 `measureElement` 方法,通过 `canvas` 的 `measureText` API 计算 Plait 中文本的宽和高,将原本调用 `getTextSize` 或者 `measureDivSize` 的地方全部改为调用 `measureElement` +4. 将 `@plait/core` 中依赖 Angular 的部分移入 `@plait/angular-board` diff --git a/.changeset/moody-hounds-sleep.md b/.changeset/moody-hounds-sleep.md new file mode 100644 index 000000000..6ae562e97 --- /dev/null +++ b/.changeset/moody-hounds-sleep.md @@ -0,0 +1,9 @@ +--- +'@plait/text-plugins': minor +--- + +`@plait/text-plugins` is a split from the original `@plait/text` package, which mainly contains text-related plugins and general processing required by the Plait framework. `@plait/text-plugins` does not rely on any front-end framework + +--- + +`@plait/text-plugins` 是原本的 `@plait/text` 包拆分出来,主要包含 Plait 框架需要的文本相关的插件和通用处理,`@plait/text-plugins` 不依赖任何前端框架 diff --git a/.changeset/twelve-pillows-scream.md b/.changeset/twelve-pillows-scream.md new file mode 100644 index 000000000..0b98f0396 --- /dev/null +++ b/.changeset/twelve-pillows-scream.md @@ -0,0 +1,17 @@ +--- +'@plait/angular-text': minor +--- + +1. Rename package `@plait/text` to `@plait/angular-text` + +2. Move some common processing and plugins into `@plait/text-plugins` + +3. Rename `richtext` component to `text` component + +--- + +1. 将包 `@plait/text` 重命名为 `@plait/angular-text` + +2. 将一些通用处理和插件移入 `@plait/text-plugins` 中 + +3. 将 `richtext` 组件重命名 `text` 组件 diff --git a/.changeset/witty-oranges-fail.md b/.changeset/witty-oranges-fail.md new file mode 100644 index 000000000..9e67482f0 --- /dev/null +++ b/.changeset/witty-oranges-fail.md @@ -0,0 +1,21 @@ +--- +'@plait/angular-board': minor +--- + +Added `@plait/angular-board` package: + +1. Moved the board component from `@plait/core` to `@plait/angular-board`, core no longer depends on the angular framework +2. Provided `renderComponent` method to dynamically render Angular components +3. Implemented `renderText` method to dynamically render text components (angular-text) +4. ... + + +--- + + +新增 `@plait/angular-board` 包: + +1. 将 board 组件从 `@plait/core` 移动到 `@plait/angular-board` 中, core 不再依赖 angular 框架 +2. 提供 `renderComponent` 方法实现动态渲染 Angular 组件 +3. 实现 `renderText` 方法实现动态渲染文本组件(angular-text) +4. ... diff --git a/angular.json b/angular.json index 13f0039d3..85511c9ab 100644 --- a/angular.json +++ b/angular.json @@ -106,23 +106,23 @@ } } }, - "text": { + "angular-text": { "projectType": "library", - "root": "packages/text", - "sourceRoot": "packages/text/src", + "root": "packages/angular-text", + "sourceRoot": "packages/angular-text/src", "prefix": "pla", "architect": { "build": { "builder": "@angular-devkit/build-angular:ng-packagr", "options": { - "project": "packages/text/ng-package.json" + "project": "packages/angular-text/ng-package.json" }, "configurations": { "production": { - "tsConfig": "packages/text/tsconfig.lib.prod.json" + "tsConfig": "packages/angular-text/tsconfig.lib.prod.json" }, "development": { - "tsConfig": "packages/text/tsconfig.lib.json" + "tsConfig": "packages/angular-text/tsconfig.lib.json" } }, "defaultConfiguration": "production" @@ -130,17 +130,17 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "packages/text/src/test.ts", - "tsConfig": "packages/text/tsconfig.spec.json", - "karmaConfig": "packages/text/karma.conf.js" + "main": "packages/angular-text/src/test.ts", + "tsConfig": "packages/angular-text/tsconfig.spec.json", + "karmaConfig": "packages/angular-text/karma.conf.js" } }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { "lintFilePatterns": [ - "packages/text/**/*.ts", - "packages/text/**/*.html" + "packages/angular-text/**/*.ts", + "packages/angular-text/**/*.html" ] } } @@ -186,6 +186,46 @@ } } }, + "angular-board": { + "projectType": "library", + "root": "packages/angular-board", + "sourceRoot": "packages/angular-board/src", + "prefix": "pla", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "packages/angular-board/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "packages/angular-board/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "packages/angular-board/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "packages/angular-board/src/test.ts", + "tsConfig": "packages/angular-board/tsconfig.spec.json", + "karmaConfig": "packages/angular-board/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "packages/angular-board/**/*.ts", + "packages/angular-board/**/*.html" + ] + } + } + } + }, "mind": { "projectType": "library", "root": "packages/mind", @@ -385,6 +425,46 @@ } } } + }, + "text-plugins": { + "projectType": "library", + "root": "packages/text-plugins", + "sourceRoot": "packages/text-plugins/src", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "packages/text-plugins/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "packages/text-plugins/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "packages/text-plugins/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "packages/text-plugins/src/test.ts", + "tsConfig": "packages/text-plugins/tsconfig.spec.json", + "karmaConfig": "packages/text-plugins/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "packages/text-plugins/**/*.ts", + "packages/text-plugins/**/*.html" + ] + } + } + } } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5a2e990fa..bd1f6f8ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5178,6 +5178,10 @@ "node": ">=14" } }, + "node_modules/@plait/angular": { + "resolved": "packages/angular", + "link": true + }, "node_modules/@plait/common": { "resolved": "packages/common", "link": true @@ -5206,6 +5210,10 @@ "resolved": "packages/text", "link": true }, + "node_modules/@plait/text-plugins": { + "resolved": "packages/text-plugins", + "link": true + }, "node_modules/@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", @@ -22697,6 +22705,18 @@ "tslib": "^2.3.0" } }, + "packages/angular": { + "name": "@plait/angular", + "version": "0.57.0", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.2.4", + "@angular/core": "^17.2.4", + "is-hotkey": "^0.2.0" + } + }, "packages/charts": { "name": "@plait/charts", "version": "0.0.1", @@ -22714,11 +22734,6 @@ "version": "0.60.0", "dependencies": { "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^17.2.4", - "@angular/core": "^17.2.4", - "is-hotkey": "^0.2.0" } }, "packages/core": { @@ -22728,9 +22743,11 @@ "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/common": "^17.2.4", - "@angular/core": "^17.2.4", - "is-hotkey": "^0.2.0" + "immer": "^10.0.3", + "is-hotkey": "^0.2.0", + "roughjs": "^4.5.2", + "rxjs": "~7.8.0", + "slate": "^0.101.5" } }, "packages/draw": { @@ -22825,6 +22842,12 @@ "@angular/core": "^17.2.4", "is-hotkey": "^0.2.0" } + }, + "packages/text-plugins": { + "version": "0.57.0", + "dependencies": { + "tslib": "^2.3.0" + } } }, "dependencies": { @@ -26433,6 +26456,12 @@ "dev": true, "optional": true }, + "@plait/angular": { + "version": "file:packages/angular", + "requires": { + "tslib": "^2.3.0" + } + }, "@plait/common": { "version": "file:packages/common", "requires": { @@ -26475,6 +26504,12 @@ "tslib": "^2.3.0" } }, + "@plait/text-plugins": { + "version": "file:packages/text-plugins", + "requires": { + "tslib": "^2.3.0" + } + }, "@rollup/plugin-json": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", diff --git a/package.json b/package.json index b7225a3a7..511fc0ce1 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,16 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "npm run build:core && npm run build:text && npm run build:common && ng build layouts && npm run build:mind && npm run build:flow && npm run build:draw", + "build": "npm run build:core && npm run build:common && npm run build:text-plugins && ng build layouts && npm run build:mind && npm run build:flow && npm run build:draw && npm run build:angular-text && npm run build:angular-board", "build:docs": "docgeni build", "build:core": "ng build core && cpx \"./packages/core/src/**/*.scss\" ./dist/core/", + "build:angular-board": "ng build angular-board && cpx \"./packages/angular-board/src/**/*.scss\" ./dist/angular-board/", "build:mind": " ng build mind && cpx \"./packages/mind/src/**/*.scss\" ./dist/mind/", - "build:text": "ng build text && cpx \"./packages/text/src/**/*.scss\" ./dist/text/", + "build:angular-text": "ng build angular-text && cpx \"./packages/angular-text/src/**/*.scss\" ./dist/angular-text/", "build:flow": "ng build flow && cpx \"./packages/flow/src/**/*.scss\" ./dist/flow/", "build:draw": "ng build draw && cpx \"./packages/draw/src/**/*.scss\" ./dist/draw/", "build:common": "ng build common", + "build:text-plugins": "ng build text-plugins", "build:demo": "ng build demo --configuration production", "pub:core": "cd dist/core && npm publish --access public", "pub:text": "cd dist/text && npm publish --access public", @@ -32,7 +34,8 @@ "start:docs": "docgeni serve --port 4600", "ci:test": "ng test --watch=false --progress=false --browsers=ChromeHeadlessCI", "lint": "ng lint --fix", - "pretty": "pretty-quick --staged" + "pretty": "pretty-quick --staged", + "copy-to": "cp -r dist/* ../plait-board-template/node_modules/@plait" }, "private": true, "repository": { diff --git a/packages/angular-board/.eslintrc.json b/packages/angular-board/.eslintrc.json new file mode 100644 index 000000000..93daf240b --- /dev/null +++ b/packages/angular-board/.eslintrc.json @@ -0,0 +1,44 @@ +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "packages/angular/tsconfig.lib.json", + "packages/angular/tsconfig.spec.json" + ], + "createDefaultProgram": true + }, + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "plait", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "plait", + "style": "kebab-case" + } + ] + } + }, + { + "files": [ + "*.html" + ], + "rules": {} + } + ] +} diff --git a/packages/angular-board/CHANGELOG.md b/packages/angular-board/CHANGELOG.md new file mode 100644 index 000000000..4befe2122 --- /dev/null +++ b/packages/angular-board/CHANGELOG.md @@ -0,0 +1 @@ +# @plait/angular-board \ No newline at end of file diff --git a/packages/angular-board/README.md b/packages/angular-board/README.md new file mode 100644 index 000000000..4698fbbe8 --- /dev/null +++ b/packages/angular-board/README.md @@ -0,0 +1 @@ +## @plait/angular diff --git a/packages/angular-board/README.zh-CN.md b/packages/angular-board/README.zh-CN.md new file mode 100644 index 000000000..5baeb40a8 --- /dev/null +++ b/packages/angular-board/README.zh-CN.md @@ -0,0 +1,2 @@ + +## @plait/angular diff --git a/packages/angular-board/karma.conf.js b/packages/angular-board/karma.conf.js new file mode 100644 index 000000000..b30589ef8 --- /dev/null +++ b/packages/angular-board/karma.conf.js @@ -0,0 +1,50 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/plait'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + customLaunchers: { + ChromeHeadlessCI: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/packages/angular-board/ng-package.json b/packages/angular-board/ng-package.json new file mode 100644 index 000000000..da8c11b79 --- /dev/null +++ b/packages/angular-board/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/angular-board", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/packages/angular-board/package.json b/packages/angular-board/package.json new file mode 100644 index 000000000..094c5a4ed --- /dev/null +++ b/packages/angular-board/package.json @@ -0,0 +1,20 @@ +{ + "name": "@plait/angular-board", + "version": "0.60.0", + "peerDependencies": { + "@angular/common": "^17.2.4", + "@angular/core": "^17.2.4", + "is-hotkey": "^0.2.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "exports": { + "./styles/styles": { + "sass": "./styles/styles.scss" + }, + "./styles/*": { + "sass": "./styles/*" + } + } +} diff --git a/packages/core/src/board/board.component.interface.ts b/packages/angular-board/src/board/board.component.interface.ts similarity index 69% rename from packages/core/src/board/board.component.interface.ts rename to packages/angular-board/src/board/board.component.interface.ts index 21f60d8fb..fcae13882 100644 --- a/packages/core/src/board/board.component.interface.ts +++ b/packages/angular-board/src/board/board.component.interface.ts @@ -1,10 +1,10 @@ import { ChangeDetectorRef, EventEmitter, ViewContainerRef } from '@angular/core'; -import { PlaitBoardChangeEvent } from '../interfaces/board'; +import { OnChangeData } from '../plugins/angular-board'; export interface BoardComponentInterface { markForCheck: () => void; cdr: ChangeDetectorRef; nativeElement: HTMLElement; viewContainerRef: ViewContainerRef; - plaitChange: EventEmitter; + onChange: EventEmitter; } diff --git a/packages/text/src/utils/text-size.spec.ts b/packages/angular-board/src/board/board.component.spec.ts similarity index 96% rename from packages/text/src/utils/text-size.spec.ts rename to packages/angular-board/src/board/board.component.spec.ts index 06e056b02..6bf7da880 100644 --- a/packages/text/src/utils/text-size.spec.ts +++ b/packages/angular-board/src/board/board.component.spec.ts @@ -3,4 +3,4 @@ describe('mock test', () => { const a = '1'; expect(a).toEqual('1'); }); -}); +}); \ No newline at end of file diff --git a/packages/core/src/board/board.component.ts b/packages/angular-board/src/board/board.component.ts similarity index 88% rename from packages/core/src/board/board.component.ts rename to packages/angular-board/src/board/board.component.ts index ef01f921c..70a1e8988 100644 --- a/packages/core/src/board/board.component.ts +++ b/packages/angular-board/src/board/board.component.ts @@ -17,72 +17,69 @@ import { QueryList, SimpleChanges, ViewChild, - ViewContainerRef, - inject + ViewContainerRef } from '@angular/core'; import rough from 'roughjs/bin/rough'; import { RoughSVG } from 'roughjs/bin/svg'; import { fromEvent, Subject } from 'rxjs'; import { filter, takeUntil, tap } from 'rxjs/operators'; -import { PlaitBoard, PlaitBoardChangeEvent, PlaitBoardOptions } from '../interfaces/board'; -import { PlaitElement } from '../interfaces/element'; -import { PlaitPlugin } from '../interfaces/plugin'; -import { Viewport } from '../interfaces/viewport'; -import { createBoard } from '../plugins/create-board'; -import { withBoard } from '../plugins/with-board'; -import { withHistory } from '../plugins/with-history'; -import { withHandPointer } from '../plugins/with-hand'; -import { withSelection } from '../plugins/with-selection'; +import { BoardComponentInterface } from './board.component.interface'; import { + BOARD_TO_AFTER_CHANGE, + BOARD_TO_CONTEXT, + BOARD_TO_ELEMENT_HOST, + BOARD_TO_HOST, + BOARD_TO_MOVING_POINT, + BOARD_TO_MOVING_POINT_IN_BOARD, + BOARD_TO_ON_CHANGE, + BOARD_TO_ROUGH_SVG, + BoardTransforms, + HOST_CLASS_NAME, + IS_BOARD_ALIVE, IS_CHROME, IS_FIREFOX, IS_SAFARI, + ListRender, + PlaitBoard, + PlaitBoardContext, + PlaitBoardOptions, + PlaitChildrenContext, + PlaitElement, + PlaitPlugin, + PlaitTheme, + Viewport, WritableClipboardOperationType, + ZOOM_STEP, + createBoard, deleteFragment, getClipboardData, - getRectangleByElements, - getSelectedElements, - setClipboardData, - setFragment, - toHostPoint, - toViewBoxPoint -} from '../utils'; -import { - BOARD_TO_ON_CHANGE, - BOARD_TO_COMPONENT, - BOARD_TO_ELEMENT_HOST, - BOARD_TO_HOST, - BOARD_TO_ROUGH_SVG, - BOARD_TO_MOVING_POINT_IN_BOARD, - BOARD_TO_MOVING_POINT, - BOARD_TO_AFTER_CHANGE, - IS_BOARD_ALIVE -} from '../utils/weak-maps'; -import { BoardComponentInterface } from './board.component.interface'; -import { - initializeViewportOffset, + hasInputOrTextareaTarget, initializeViewBox, + initializeViewportContainer, + initializeViewportOffset, isFromViewportChange, + isPreventTouchMove, + setFragment, setIsFromViewportChange, - initializeViewportContainer, + toHostPoint, + toViewBoxPoint, + updateViewportByScrolling, updateViewportOffset, - updateViewportByScrolling -} from '../utils/viewport'; -import { withViewport } from '../plugins/with-viewport'; -import { withMoving } from '../plugins/with-moving'; -import { hasInputOrTextareaTarget } from '../utils/dom/common'; -import { withOptions } from '../plugins/with-options'; -import { PlaitIslandBaseComponent, hasOnBoardChange } from '../core/island/island-base.component'; -import { BoardTransforms } from '../transforms/board'; -import { PlaitTheme } from '../interfaces/theme'; -import { withHotkey } from '../plugins/with-hotkey'; -import { HOST_CLASS_NAME } from '../constants'; -import { PlaitContextService } from '../services/context.service'; -import { isPreventTouchMove } from '../utils/touch'; -import { ZOOM_STEP } from '../constants/zoom'; -import { withRelatedFragment } from '../plugins/with-related-fragment'; -import { ListRender } from '../core/list-render'; -import { PlaitChildrenContext } from '../interfaces'; + withBoard, + withHandPointer, + withHistory, + withHotkey, + withMoving, + withOptions, + withRelatedFragment, + withSelection, + withViewport +} from '@plait/core'; +import { PlaitIslandBaseComponent, hasOnBoardChange } from '../island/island-base.component'; +import { BOARD_TO_COMPONENT } from '../utils/weak-maps'; +import { withAngular } from '../plugins/with-angular'; +import { withImage, withText } from '@plait/common'; +import { OnChangeData } from '../plugins/angular-board'; const ElementLowerHostClass = 'element-lower-host'; const ElementHostClass = 'element-host'; @@ -103,7 +100,6 @@ const ElementActiveHostClass = 'element-active-host'; `, changeDetection: ChangeDetectionStrategy.OnPush, - providers: [PlaitContextService], standalone: true }) export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnChanges, AfterViewInit, AfterContentInit, OnDestroy { @@ -127,7 +123,7 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC @Input() plaitTheme?: PlaitTheme; - @Output() plaitChange: EventEmitter = new EventEmitter(); + @Output() onChange: EventEmitter = new EventEmitter(); @Output() plaitBoardInitialized: EventEmitter = new EventEmitter(); @@ -137,7 +133,7 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC @HostBinding('class') get hostClass() { - return `${HOST_CLASS_NAME} pointer-${this.board.pointer} theme-${this.board.theme.themeColorMode} ${this.getBrowserClassName()}`; + return `${HOST_CLASS_NAME} theme-${this.board.theme.themeColorMode} ${this.getBrowserClassName()}`; } getBrowserClassName() { @@ -182,8 +178,6 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC listRender!: ListRender; - contextService = inject(PlaitContextService); - constructor( public cdr: ChangeDetectorRef, public viewContainerRef: ViewContainerRef, @@ -201,7 +195,6 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC }); this.roughSVG = roughSVG; this.initializePlugins(); - this.ngZone.runOutsideAngular(() => { this.initializeHookListener(); this.viewportScrollListener(); @@ -232,7 +225,7 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC }); BOARD_TO_AFTER_CHANGE.set(this.board, () => { this.ngZone.run(() => { - const changeEvent: PlaitBoardChangeEvent = { + const data: OnChangeData = { children: this.board.children, operations: this.board.operations, viewport: this.board.viewport, @@ -240,10 +233,13 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC theme: this.board.theme }; this.updateIslands(); - this.plaitChange.emit(changeEvent); + this.onChange.emit(data); }); }); + const context = new PlaitBoardContext(); + BOARD_TO_CONTEXT.set(this.board, context); this.initializeListRender(); + this.elementRef.nativeElement.classList.add(`pointer-${this.board.pointer}`); this.hasInitialized = true; } @@ -279,7 +275,15 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC withHotkey( withHandPointer( withHistory( - withSelection(withMoving(withBoard(withViewport(withOptions(createBoard(this.plaitValue, this.plaitOptions)))))) + withSelection( + withMoving( + withBoard( + withViewport( + withOptions(withAngular(withImage(withText(createBoard(this.plaitValue, this.plaitOptions))))) + ) + ) + ) + ) ) ) ) @@ -450,7 +454,7 @@ export class PlaitBoardComponent implements BoardComponentInterface, OnInit, OnC private updateListRender() { this.listRender.update(this.board.children, this.initializeChildrenContext()); - this.contextService.nextStable(); + PlaitBoard.getBoardContext(this.board).nextStable(); } private initializeChildrenContext(): PlaitChildrenContext { diff --git a/packages/core/src/core/island/island-base.component.ts b/packages/angular-board/src/island/island-base.component.ts similarity index 84% rename from packages/core/src/core/island/island-base.component.ts rename to packages/angular-board/src/island/island-base.component.ts index 57c7f0649..0059dc868 100644 --- a/packages/core/src/core/island/island-base.component.ts +++ b/packages/angular-board/src/island/island-base.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectorRef, Directive, Input, OnDestroy, OnInit } from '@angular/core'; -import { PlaitBoard } from '../../interfaces'; +import { PlaitBoard } from '@plait/core'; import { Subscription } from 'rxjs'; +import { BOARD_TO_COMPONENT } from '../utils/weak-maps'; +import { BoardComponentInterface } from '../board/board.component.interface'; @Directive({ host: { @@ -36,8 +38,8 @@ export abstract class PlaitIslandPopoverBaseComponent implements OnInit, OnDestr initialize(board: PlaitBoard) { this.board = board; - const boardComponent = PlaitBoard.getComponent(board); - this.subscription = boardComponent.plaitChange.subscribe(() => { + const boardComponent = BOARD_TO_COMPONENT.get(board) as BoardComponentInterface; + this.subscription = boardComponent.onChange.subscribe(() => { if (hasOnBoardChange(this)) { this.onBoardChange(); } diff --git a/packages/angular-board/src/plugins/angular-board.ts b/packages/angular-board/src/plugins/angular-board.ts new file mode 100644 index 000000000..28d5a5590 --- /dev/null +++ b/packages/angular-board/src/plugins/angular-board.ts @@ -0,0 +1,19 @@ +import { PlaitElement, PlaitOperation, PlaitTheme, Viewport, Selection, ComponentType } from '@plait/core'; +import { RenderComponentRef } from '@plait/common'; +import { ComponentRef } from '@angular/core'; + +export interface AngularBoard { + renderComponent: HTMLElement }>( + type: ComponentType, + container: Element | DocumentFragment, + props: T + ) => { ref: RenderComponentRef; componentRef: ComponentRef }; +} + +export interface OnChangeData { + children: PlaitElement[]; + operations: PlaitOperation[]; + viewport: Viewport; + selection: Selection | null; + theme: PlaitTheme; +} diff --git a/packages/angular-board/src/plugins/with-angular.ts b/packages/angular-board/src/plugins/with-angular.ts new file mode 100644 index 000000000..ec80b2b26 --- /dev/null +++ b/packages/angular-board/src/plugins/with-angular.ts @@ -0,0 +1,60 @@ +import { ComponentType, PlaitBoard } from '@plait/core'; +import { AngularBoard } from './angular-board'; +import { PlaitTextBoard, TextComponentRef, TextProps } from '@plait/common'; +import { PlaitTextComponent } from '@plait/angular-text'; +import { AngularEditor } from 'slate-angular'; +import { BOARD_TO_COMPONENT } from '../utils/weak-maps'; +import { BoardComponentInterface } from '../board/board.component.interface'; + +export const withAngular = (board: PlaitBoard & PlaitTextBoard) => { + const newBoard = board as PlaitBoard & PlaitTextBoard & AngularBoard; + + newBoard.renderComponent = HTMLElement }>( + type: ComponentType, + container: Element | DocumentFragment, + props: T + ) => { + const boardComponent = BOARD_TO_COMPONENT.get(board) as BoardComponentInterface; + const componentRef = boardComponent.viewContainerRef.createComponent(type); + for (const key in props) { + const value = props[key as keyof T]; + (componentRef.instance as any)[key as keyof TextProps] = value as any; + } + container.appendChild(componentRef.instance.nativeElement()); + componentRef.changeDetectorRef.detectChanges(); + const ref: TextComponentRef = { + destroy: () => { + componentRef.destroy(); + }, + update: (props: Partial) => { + for (const key in props) { + const value = props[key as keyof TextProps]; + (componentRef.instance as any)[key] = value; + } + // solve image lose on move node + if (container.children.length === 0) { + container.append(componentRef.instance.nativeElement()); + } + componentRef.changeDetectorRef.detectChanges(); + } + }; + return { ref, componentRef }; + }; + + newBoard.renderText = (container: Element | DocumentFragment, props: TextProps) => { + const { ref, componentRef } = newBoard.renderComponent(PlaitTextComponent, container, props); + const { update } = ref; + ref.update = props => { + const beforeReadonly = componentRef.instance.readonly; + update(props); + if (beforeReadonly === true && props.readonly === false) { + AngularEditor.focus(componentRef.instance.editor); + } else if (beforeReadonly === false && props.readonly === true) { + AngularEditor.blur(componentRef.instance.editor); + } + }; + return ref; + }; + + return newBoard; +}; diff --git a/packages/angular-board/src/public-api.ts b/packages/angular-board/src/public-api.ts new file mode 100644 index 000000000..dd97d56e0 --- /dev/null +++ b/packages/angular-board/src/public-api.ts @@ -0,0 +1,8 @@ +/* + * Public API Surface of plait + */ + +export * from './board/board.component'; +export * from './board/board.component.interface'; +export * from './plugins/angular-board'; +export * from './island/island-base.component'; diff --git a/packages/core/src/styles/mixins.scss b/packages/angular-board/src/styles/mixins.scss similarity index 100% rename from packages/core/src/styles/mixins.scss rename to packages/angular-board/src/styles/mixins.scss diff --git a/packages/core/src/styles/styles.scss b/packages/angular-board/src/styles/styles.scss similarity index 98% rename from packages/core/src/styles/styles.scss rename to packages/angular-board/src/styles/styles.scss index fc5a10cad..17d9b3584 100644 --- a/packages/core/src/styles/styles.scss +++ b/packages/angular-board/src/styles/styles.scss @@ -30,7 +30,7 @@ } // https://stackoverflow.com/questions/51313873/svg-foreignobject-not-working-properly-on-safari - .plait-richtext-container { + .plait-text-container { // chrome show position is not correct, safari not working when don't assigned position property // can not assign absolute, because safari can not show correctly position position: initial; diff --git a/packages/text/src/test.ts b/packages/angular-board/src/test.ts similarity index 100% rename from packages/text/src/test.ts rename to packages/angular-board/src/test.ts diff --git a/packages/angular-board/src/utils/weak-maps.ts b/packages/angular-board/src/utils/weak-maps.ts new file mode 100644 index 000000000..d58ef1d7c --- /dev/null +++ b/packages/angular-board/src/utils/weak-maps.ts @@ -0,0 +1,4 @@ +import { PlaitBoard } from '@plait/core'; +import { BoardComponentInterface } from '../board/board.component.interface'; + +export const BOARD_TO_COMPONENT = new WeakMap(); diff --git a/packages/text/tsconfig.lib.json b/packages/angular-board/tsconfig.lib.json similarity index 100% rename from packages/text/tsconfig.lib.json rename to packages/angular-board/tsconfig.lib.json diff --git a/packages/text/tsconfig.lib.prod.json b/packages/angular-board/tsconfig.lib.prod.json similarity index 100% rename from packages/text/tsconfig.lib.prod.json rename to packages/angular-board/tsconfig.lib.prod.json diff --git a/packages/text/tsconfig.spec.json b/packages/angular-board/tsconfig.spec.json similarity index 100% rename from packages/text/tsconfig.spec.json rename to packages/angular-board/tsconfig.spec.json diff --git a/packages/text/.eslintrc.json b/packages/angular-text/.eslintrc.json similarity index 100% rename from packages/text/.eslintrc.json rename to packages/angular-text/.eslintrc.json diff --git a/packages/text/CHANGELOG.md b/packages/angular-text/CHANGELOG.md similarity index 99% rename from packages/text/CHANGELOG.md rename to packages/angular-text/CHANGELOG.md index 855fd4241..1e45199d9 100644 --- a/packages/text/CHANGELOG.md +++ b/packages/angular-text/CHANGELOG.md @@ -1,4 +1,4 @@ -# text +# angular-text ## 0.61.0 diff --git a/packages/text/README.md b/packages/angular-text/README.md similarity index 89% rename from packages/text/README.md rename to packages/angular-text/README.md index 5baf5c45d..25e4eae91 100644 --- a/packages/text/README.md +++ b/packages/angular-text/README.md @@ -1,4 +1,4 @@ -# @plait/text +# @plait/angular-text Text processing implementation, supports commonly used rich text formats, such as font size, color, bold, italics, etc. diff --git a/packages/text/README.zh-CN.md b/packages/angular-text/README.zh-CN.md similarity index 88% rename from packages/text/README.zh-CN.md rename to packages/angular-text/README.zh-CN.md index 9c4a2e16e..f6f69cb66 100644 --- a/packages/text/README.zh-CN.md +++ b/packages/angular-text/README.zh-CN.md @@ -1,4 +1,4 @@ -# @plait/text +# @plait/angular-text 文本处理实现,支持常用的富文本格式,如字体大小、颜色、粗体、斜体等。 diff --git a/packages/text/karma.conf.js b/packages/angular-text/karma.conf.js similarity index 100% rename from packages/text/karma.conf.js rename to packages/angular-text/karma.conf.js diff --git a/packages/text/ng-package.json b/packages/angular-text/ng-package.json similarity index 77% rename from packages/text/ng-package.json rename to packages/angular-text/ng-package.json index f487c72fe..97b39c9d2 100644 --- a/packages/text/ng-package.json +++ b/packages/angular-text/ng-package.json @@ -1,6 +1,6 @@ { "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", - "dest": "../../dist/text", + "dest": "../../dist/angular-text", "lib": { "entryFile": "src/public-api.ts" } diff --git a/packages/text/package.json b/packages/angular-text/package.json similarity index 91% rename from packages/text/package.json rename to packages/angular-text/package.json index 8181d30ec..46bcdc8e6 100644 --- a/packages/text/package.json +++ b/packages/angular-text/package.json @@ -1,5 +1,5 @@ { - "name": "@plait/text", + "name": "@plait/angular-text", "version": "0.61.0", "peerDependencies": { "@angular/common": "^17.2.4", diff --git a/packages/text/src/plugins/link/link.component.scss b/packages/angular-text/src/plugins/link/link.component.scss similarity index 100% rename from packages/text/src/plugins/link/link.component.scss rename to packages/angular-text/src/plugins/link/link.component.scss diff --git a/packages/text/src/plugins/link/link.component.ts b/packages/angular-text/src/plugins/link/link.component.ts similarity index 95% rename from packages/text/src/plugins/link/link.component.ts rename to packages/angular-text/src/plugins/link/link.component.ts index 8cf18697d..4f576e5e1 100644 --- a/packages/text/src/plugins/link/link.component.ts +++ b/packages/angular-text/src/plugins/link/link.component.ts @@ -1,6 +1,6 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { LinkElement } from '@plait/common'; import { BaseElementComponent } from 'slate-angular'; -import { LinkElement } from '../../custom-types'; @Component({ selector: 'a[plaitLink]', diff --git a/packages/angular-text/src/plugins/link/with-link-insert.ts b/packages/angular-text/src/plugins/link/with-link-insert.ts new file mode 100644 index 000000000..4d9ae96c5 --- /dev/null +++ b/packages/angular-text/src/plugins/link/with-link-insert.ts @@ -0,0 +1,18 @@ +import { Transforms } from 'slate'; +import { AngularEditor } from 'slate-angular'; +import { LinkEditor, getTextFromClipboard, isUrl, withLink } from '@plait/text-plugins'; + +export const withPasteLink = (editor: T): T => { + const { insertData } = editor; + editor.insertData = data => { + const text = getTextFromClipboard(data); + if (typeof text === 'string' && text && isUrl(text)) { + LinkEditor.wrapLink(editor, text, text); + Transforms.move(editor, { distance: 1, unit: 'offset' }); + } else { + insertData(data); + } + }; + + return withLink(editor); +}; diff --git a/packages/angular-text/src/plugins/mark-hotkey/with-mark-hotkey.ts b/packages/angular-text/src/plugins/mark-hotkey/with-mark-hotkey.ts new file mode 100644 index 000000000..a517ac69f --- /dev/null +++ b/packages/angular-text/src/plugins/mark-hotkey/with-mark-hotkey.ts @@ -0,0 +1,15 @@ +import { AngularEditor } from 'slate-angular'; +import { MarkEditor, markShortcuts, withMark } from '@plait/text-plugins'; + +export const withMarkHotkey = (editor: T): T => { + const e = editor; + + const { onKeydown } = e; + + e.onKeydown = (event: KeyboardEvent) => { + markShortcuts(editor, event); + onKeydown(event); + }; + + return withMark(e); +}; diff --git a/packages/text/src/plugins/paragraph/paragraph.component.ts b/packages/angular-text/src/plugins/paragraph/paragraph.component.ts similarity index 95% rename from packages/text/src/plugins/paragraph/paragraph.component.ts rename to packages/angular-text/src/plugins/paragraph/paragraph.component.ts index 38ac26264..8db34426e 100644 --- a/packages/text/src/plugins/paragraph/paragraph.component.ts +++ b/packages/angular-text/src/plugins/paragraph/paragraph.component.ts @@ -1,6 +1,6 @@ import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { ParagraphElement } from '@plait/common'; import { BaseElementComponent } from 'slate-angular'; -import { ParagraphElement } from '../../custom-types'; @Component({ selector: 'div[plaitTextParagraphElement]', diff --git a/packages/text/src/plugins/text.editor.ts b/packages/angular-text/src/plugins/text.editor.ts similarity index 100% rename from packages/text/src/plugins/text.editor.ts rename to packages/angular-text/src/plugins/text.editor.ts diff --git a/packages/text/src/plugins/with-inline.ts b/packages/angular-text/src/plugins/with-inline.ts similarity index 97% rename from packages/text/src/plugins/with-inline.ts rename to packages/angular-text/src/plugins/with-inline.ts index 061d78069..41a4c8bef 100644 --- a/packages/text/src/plugins/with-inline.ts +++ b/packages/angular-text/src/plugins/with-inline.ts @@ -16,7 +16,7 @@ export const withInline = (editor: T) => { e.onKeydown = (event: KeyboardEvent) => { const { selection } = editor; if (!selection || !selection.anchor || !selection.focus) { - onkeyDown(event); + onKeydown(event); return; } const isMoveBackward = hotkeys.isMoveBackward(event); @@ -51,7 +51,7 @@ export const withInline = (editor: T) => { return; } } - onkeyDown(event); + onKeydown(event); }; return e; diff --git a/packages/text/src/plugins/with-selection.ts b/packages/angular-text/src/plugins/with-selection.ts similarity index 100% rename from packages/text/src/plugins/with-selection.ts rename to packages/angular-text/src/plugins/with-selection.ts diff --git a/packages/text/src/plugins/with-single.ts b/packages/angular-text/src/plugins/with-single.ts similarity index 92% rename from packages/text/src/plugins/with-single.ts rename to packages/angular-text/src/plugins/with-single.ts index 891b1e2fb..840409e24 100644 --- a/packages/text/src/plugins/with-single.ts +++ b/packages/angular-text/src/plugins/with-single.ts @@ -1,5 +1,5 @@ +import { CLIPBOARD_FORMAT_KEY } from '@plait/text-plugins'; import { AngularEditor } from 'slate-angular'; -import { CLIPBOARD_FORMAT_KEY } from '../constant'; export const withSingleLine = (editor: T) => { const e = editor as T; diff --git a/packages/angular-text/src/public-api.ts b/packages/angular-text/src/public-api.ts new file mode 100644 index 000000000..75fe3befc --- /dev/null +++ b/packages/angular-text/src/public-api.ts @@ -0,0 +1,5 @@ +/* + * Public API Surface of richtext + */ +export * from './text/text.component'; +export * from './plugins/text.editor'; diff --git a/packages/text/src/styles/styles.scss b/packages/angular-text/src/styles/styles.scss similarity index 93% rename from packages/text/src/styles/styles.scss rename to packages/angular-text/src/styles/styles.scss index a3d0d065b..005850b13 100644 --- a/packages/text/src/styles/styles.scss +++ b/packages/angular-text/src/styles/styles.scss @@ -1,6 +1,6 @@ @forward '../plugins/link/link.component.scss'; -.plait-richtext-container { +.plait-text-container { font-size: 14px; min-height: 20px; line-height: 20px; @@ -8,6 +8,7 @@ white-space: pre-wrap; white-space: break-spaces; word-break: break-all; + font-family: PingFangSC-Regular, 'PingFang SC'; .slate-editable-container { outline: none; diff --git a/packages/text/src/styles/variables.scss b/packages/angular-text/src/styles/variables.scss similarity index 100% rename from packages/text/src/styles/variables.scss rename to packages/angular-text/src/styles/variables.scss diff --git a/packages/angular-text/src/test.ts b/packages/angular-text/src/test.ts new file mode 100644 index 000000000..61720dc87 --- /dev/null +++ b/packages/angular-text/src/test.ts @@ -0,0 +1,9 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: true } }); diff --git a/packages/text/src/text-node/text.component.ts b/packages/angular-text/src/text-node/text.component.ts similarity index 95% rename from packages/text/src/text-node/text.component.ts rename to packages/angular-text/src/text-node/text.component.ts index 5a78fd32d..2dabfa95c 100644 --- a/packages/text/src/text-node/text.component.ts +++ b/packages/angular-text/src/text-node/text.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, ElementRef, Renderer2 } from '@angular/core'; +import { CustomText } from '@plait/common'; +import { MarkTypes } from '@plait/text-plugins'; import { BaseTextComponent } from 'slate-angular'; -import { MarkTypes } from '../constant/mark'; -import { CustomText } from '../custom-types'; @Component({ selector: 'span[plaitText]', diff --git a/packages/text/src/richtext/richtext.component.html b/packages/angular-text/src/text/text.component.html similarity index 90% rename from packages/text/src/richtext/richtext.component.html rename to packages/angular-text/src/text/text.component.html index 810b83c60..3cf4d26f8 100644 --- a/packages/text/src/richtext/richtext.component.html +++ b/packages/angular-text/src/text/text.component.html @@ -10,4 +10,5 @@ [compositionUpdate]="compositionUpdate" [compositionEnd]="compositionEnd" [renderText]="renderText" + [ngClass]="{'editing': !readonly}" > diff --git a/packages/angular-text/src/text/text.component.spec.ts b/packages/angular-text/src/text/text.component.spec.ts new file mode 100644 index 000000000..6bf7da880 --- /dev/null +++ b/packages/angular-text/src/text/text.component.spec.ts @@ -0,0 +1,6 @@ +describe('mock test', () => { + it('test', () => { + const a = '1'; + expect(a).toEqual('1'); + }); +}); \ No newline at end of file diff --git a/packages/text/src/richtext/richtext.component.ts b/packages/angular-text/src/text/text.component.ts similarity index 57% rename from packages/text/src/richtext/richtext.component.ts rename to packages/angular-text/src/text/text.component.ts index 98f2c6e45..088c757a3 100644 --- a/packages/text/src/richtext/richtext.component.ts +++ b/packages/angular-text/src/text/text.component.ts @@ -3,75 +3,89 @@ import { ChangeDetectorRef, Component, ElementRef, - EventEmitter, HostBinding, Input, + OnChanges, OnInit, - Output, Renderer2, + SimpleChanges, ViewChild } from '@angular/core'; import { isKeyHotkey } from 'is-hotkey'; import { Editor, Element, Text, Transforms, createEditor } from 'slate'; import { SlateEditable, withAngular } from 'slate-angular'; import { withHistory } from 'slate-history'; -import { CLIPBOARD_FORMAT_KEY } from '../constant'; -import { MarkTypes } from '../constant/mark'; -import { LinkElement, TextPlugin } from '../custom-types'; import { PlaitLinkNodeComponent } from '../plugins/link/link.component'; -import { withLink } from '../plugins/link/with-link'; -import { withMark } from '../plugins/mark/with-marks'; +import { withMarkHotkey } from '../plugins/mark-hotkey/with-mark-hotkey'; import { ParagraphElementComponent } from '../plugins/paragraph/paragraph.component'; import { PlaitTextEditor } from '../plugins/text.editor'; import { withSelection } from '../plugins/with-selection'; import { withSingleLine } from '../plugins/with-single'; import { PlaitTextNodeComponent } from '../text-node/text.component'; import { FormsModule } from '@angular/forms'; +import { LinkElement, TextChangeData, TextPlugin } from '@plait/common'; +import { CLIPBOARD_FORMAT_KEY, MarkTypes } from '@plait/text-plugins'; +import { withPasteLink } from '../plugins/link/with-link-insert'; +import { CommonModule } from '@angular/common'; @Component({ - selector: 'plait-richtext', - templateUrl: './richtext.component.html', + selector: 'plait-text', + templateUrl: './text.component.html', standalone: true, - imports: [SlateEditable, FormsModule] + imports: [SlateEditable, FormsModule, CommonModule] }) -export class PlaitRichtextComponent implements OnInit, AfterViewInit { - @HostBinding('class') hostClass = 'plait-richtext-container'; +export class PlaitTextComponent implements OnInit, AfterViewInit, OnChanges { + @HostBinding('class') hostClass = 'plait-text-container'; children: Element[] = []; - @Input() textPlugins: TextPlugin[] = []; + @Input() textPlugins?: TextPlugin[]; - @Input() set value(value: Element) { - this.children = [value]; + @Input() set text(text: Element) { + this.children = [text]; this.cdr.markForCheck(); } - @Input() readonly = false; + @Input() readonly = true; @ViewChild('slateEditable') slateEditable!: SlateEditable; - @Output() - onChange: EventEmitter = new EventEmitter(); + @Input() + onChange!: (data: TextChangeData) => void; - @Output() - onComposition: EventEmitter = new EventEmitter(); + @Input() + afterInit?: (editor: Editor) => void; - editor = withSelection(withLink(withMark(withSingleLine(withHistory(withAngular(createEditor(), CLIPBOARD_FORMAT_KEY)))))); + @Input() + onComposition!: (event: CompositionEvent) => void; + + editor = withSelection(withPasteLink(withMarkHotkey(withSingleLine(withHistory(withAngular(createEditor(), CLIPBOARD_FORMAT_KEY)))))); + + nativeElement() { + return this.elementRef.nativeElement; + } constructor(public renderer2: Renderer2, private cdr: ChangeDetectorRef, public elementRef: ElementRef) {} valueChange() { - this.onChange.emit(this.editor); + this.onChange({ newText: this.editor.children[0] as Element, operations: this.editor.operations }); + } + + ngOnChanges(changes: SimpleChanges): void { } ngOnInit(): void { - this.textPlugins.forEach(plugin => { - plugin(this.editor); - }); + if (this.textPlugins) { + this.textPlugins.forEach(plugin => { + plugin(this.editor); + }); + } } - ngAfterViewInit(): void {} + ngAfterViewInit(): void { + this.afterInit && this.afterInit(this.editor); + } renderElement = (element: Element) => { const render = ((this.editor as unknown) as PlaitTextEditor)?.renderElement; @@ -96,15 +110,15 @@ export class PlaitRichtextComponent implements OnInit, AfterViewInit { }; compositionStart = (event: CompositionEvent) => { - this.onComposition.emit(event); + this.onComposition(event); }; compositionUpdate = (event: CompositionEvent) => { - this.onComposition.emit(event); + this.onComposition(event); }; compositionEnd = (event: CompositionEvent) => { - this.onComposition.emit(event); + this.onComposition(event); }; onKeydown = (event: KeyboardEvent) => { diff --git a/packages/angular-text/tsconfig.lib.json b/packages/angular-text/tsconfig.lib.json new file mode 100644 index 000000000..1407202dd --- /dev/null +++ b/packages/angular-text/tsconfig.lib.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/packages/angular-text/tsconfig.lib.prod.json b/packages/angular-text/tsconfig.lib.prod.json new file mode 100644 index 000000000..06de549e1 --- /dev/null +++ b/packages/angular-text/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/packages/angular-text/tsconfig.spec.json b/packages/angular-text/tsconfig.spec.json new file mode 100644 index 000000000..715dd0a5d --- /dev/null +++ b/packages/angular-text/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/packages/text/types.d.ts b/packages/angular-text/types.d.ts similarity index 65% rename from packages/text/types.d.ts rename to packages/angular-text/types.d.ts index 7d8dc2402..775274db8 100644 --- a/packages/text/types.d.ts +++ b/packages/angular-text/types.d.ts @@ -1,4 +1,4 @@ -import { CustomText, CustomElement } from './src/custom-types'; +import { CustomElement, CustomText } from "@plait/common"; declare module 'slate' { interface CustomTypes { diff --git a/packages/common/package.json b/packages/common/package.json index cffd7b373..f528a8241 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -2,8 +2,6 @@ "name": "@plait/common", "version": "0.61.0", "peerDependencies": { - "@angular/common": "^17.2.4", - "@angular/core": "^17.2.4", "is-hotkey": "^0.2.0" }, "dependencies": { diff --git a/packages/common/src/constants/default.ts b/packages/common/src/constants/default.ts index 8f6994bcb..756aeea3d 100644 --- a/packages/common/src/constants/default.ts +++ b/packages/common/src/constants/default.ts @@ -6,3 +6,4 @@ export const DEFAULT_ROUTE_MARGIN = 30; export const TRANSPARENT = 'transparent'; export const ROTATE_HANDLE_DISTANCE_TO_ELEMENT = 20; export const ROTATE_HANDLE_SIZE = 18; +export const DEFAULT_FONT_FAMILY = 'PingFangSC-Regular, "PingFang SC"'; diff --git a/packages/common/src/core/element-flavour.ts b/packages/common/src/core/element-flavour.ts index 045acd5a8..97269666b 100644 --- a/packages/common/src/core/element-flavour.ts +++ b/packages/common/src/core/element-flavour.ts @@ -1,7 +1,7 @@ import { ElementFlavour, PlaitBoard, PlaitElement } from '@plait/core'; -import { TextManage } from '@plait/text'; import { ELEMENT_TO_TEXT_MANAGES } from '../utils/text'; import { PlaitCommonElementRef } from './element-ref'; +import { TextManage } from '../text/text-manage'; export class CommonElementFlavour< T extends PlaitElement = PlaitElement, diff --git a/packages/common/src/core/group.component.ts b/packages/common/src/core/group.component.ts index 5cd8839b7..e0a523f61 100644 --- a/packages/common/src/core/group.component.ts +++ b/packages/common/src/core/group.component.ts @@ -1,7 +1,6 @@ import { OnContextChanged, PlaitBoard, - PlaitContextService, PlaitGroup, PlaitPluginElementContext, getElementsInGroup, @@ -42,7 +41,7 @@ export class GroupComponent extends CommonElementFlavour initialize(): void { super.initialize(); this.initializeGenerator(); - const contextService = PlaitBoard.getViewContainerRef(this.board).injector.get(PlaitContextService); + const contextService = PlaitBoard.getBoardContext(this.board); this.onStableSubscription = contextService.onStable().subscribe(() => { const elementsInGroup = getElementsInGroup(this.board, this.element, false, true); const isPartialSelectGroup = diff --git a/packages/common/src/core/image-base.component.ts b/packages/common/src/core/image-base.component.ts deleted file mode 100644 index 010928af0..000000000 --- a/packages/common/src/core/image-base.component.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ChangeDetectorRef, Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; -import { ACTIVE_STROKE_WIDTH, PlaitBoard, PlaitElement, RectangleClient, getSelectedElements, isSelectionMoving } from '@plait/core'; -import { ActiveGenerator } from '../generators'; -import { CommonImageItem, canResize, getElementOfFocusedImage } from '../utils'; - -@Directive({ - host: { - class: 'plait-image-container' - } -}) -export abstract class ImageBaseComponent implements OnInit, OnDestroy { - _imageItem!: CommonImageItem; - - _isFocus!: boolean; - - initialized = false; - - activeGenerator!: ActiveGenerator; - - @Input() - element!: PlaitElement; - - @Input() - set imageItem(value: CommonImageItem) { - this.afterImageItemChange(this._imageItem, value); - this._imageItem = value; - this.drawFocus(); - } - - get imageItem() { - return this._imageItem; - } - - @Input() - board!: PlaitBoard; - - @Input() - set isFocus(value: boolean) { - this._isFocus = value; - this.drawFocus(); - } - - get isFocus() { - return this._isFocus; - } - - get nativeElement() { - return this.elementRef.nativeElement; - } - - abstract afterImageItemChange(previous: CommonImageItem, current: CommonImageItem): void; - - @Input() getRectangle!: () => RectangleClient; - - @Input() hasResizeHandle!: () => boolean; - - constructor(protected elementRef: ElementRef, public cdr: ChangeDetectorRef) {} - - ngOnInit(): void { - this.activeGenerator = new ActiveGenerator(this.board, { - getStrokeWidth: () => { - const selectedElements = getSelectedElements(this.board); - if (!(selectedElements.length === 1 && !isSelectionMoving(this.board))) { - return ACTIVE_STROKE_WIDTH; - } else { - return ACTIVE_STROKE_WIDTH; - } - }, - getStrokeOpacity: () => { - const selectedElements = getSelectedElements(this.board); - if ((selectedElements.length === 1 && !isSelectionMoving(this.board)) || !selectedElements.length) { - return 1; - } else { - return 0.5; - } - }, - getRectangle: () => { - return this.getRectangle(); - }, - hasResizeHandle: () => { - const isSelectedImageElement = canResize(this.board, this.element); - const isSelectedImage = !!getElementOfFocusedImage(this.board); - return isSelectedImage || isSelectedImageElement; - } - }); - this.initialized = true; - } - - drawFocus() { - if (this.initialized) { - const activeG = PlaitBoard.getElementActiveHost(this.board); - this.activeGenerator.processDrawing(this.element as PlaitElement, activeG, { selected: this._isFocus }); - } - } - - ngOnDestroy(): void { - if (this.activeGenerator) { - this.activeGenerator.destroy(); - } - } -} diff --git a/packages/common/src/core/index.ts b/packages/common/src/core/index.ts new file mode 100644 index 000000000..1671611f1 --- /dev/null +++ b/packages/common/src/core/index.ts @@ -0,0 +1,4 @@ +export * from './element-flavour'; +export * from './element-ref'; +export * from './group.component'; +export * from './render-component'; diff --git a/packages/common/src/core/render-component.ts b/packages/common/src/core/render-component.ts new file mode 100644 index 000000000..64bc6dff0 --- /dev/null +++ b/packages/common/src/core/render-component.ts @@ -0,0 +1,4 @@ +export interface RenderComponentRef { + destroy: () => void; + update: (props: Partial) => void; +} diff --git a/packages/common/src/generators/generator.ts b/packages/common/src/generators/generator.ts index 665b0f0a8..fecd3bdfa 100644 --- a/packages/common/src/generators/generator.ts +++ b/packages/common/src/generators/generator.ts @@ -7,7 +7,6 @@ import { getSelectionAngle, setAngleForG } from '@plait/core'; -import { PlaitCommonElementRef } from '../core/element-ref'; export interface GeneratorExtraData {} diff --git a/packages/common/src/generators/image.generator.ts b/packages/common/src/generators/image.generator.ts deleted file mode 100644 index 4320c4946..000000000 --- a/packages/common/src/generators/image.generator.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - PlaitBoard, - PlaitElement, - PlaitOptionsBoard, - RectangleClient, - createForeignObject, - createG, - setAngleForG, - updateForeignObject -} from '@plait/core'; -import { Generator, GeneratorOptions } from './generator'; -import { ComponentRef, ViewContainerRef } from '@angular/core'; -import { ImageBaseComponent } from '../core/image-base.component'; -import { CommonImageItem, WithCommonPluginOptions } from '../utils'; -import { WithCommonPluginKey } from '../constants'; -export interface ShapeData { - viewContainerRef: ViewContainerRef; -} - -export interface ImageGeneratorOptions { - getRectangle: (element: T) => RectangleClient; - getImageItem: (element: T) => CommonImageItem; -} - -export class ImageGenerator extends Generator< - T, - ViewContainerRef, - ImageGeneratorOptions & GeneratorOptions -> { - static key = 'image-generator'; - - foreignObject!: SVGForeignObjectElement; - - componentRef!: ComponentRef; - - constructor(public board: PlaitBoard, public options: ImageGeneratorOptions) { - super(board, options); - } - - canDraw(element: T, data: ViewContainerRef): boolean { - return !!this.options.getImageItem(element); - } - - draw(element: T, viewContainerRef: ViewContainerRef): SVGGElement { - const g = createG(); - const foreignRectangle = this.options.getRectangle(element); - this.foreignObject = createForeignObject(foreignRectangle.x, foreignRectangle.y, foreignRectangle.width, foreignRectangle.height); - g.append(this.foreignObject); - const componentType = (this.board as PlaitOptionsBoard).getPluginOptions(WithCommonPluginKey) - .imageComponentType; - if (!componentType) { - throw new Error('Not implement ImageBaseComponent error.'); - } - this.componentRef = viewContainerRef.createComponent(componentType); - this.componentRef.instance.board = this.board; - - this.componentRef.instance.imageItem = this.options.getImageItem(element); - this.componentRef.instance.element = element; - this.componentRef.instance.getRectangle = () => { - return this.options.getRectangle(element); - }; - this.componentRef.instance.cdr.markForCheck(); - this.foreignObject.append(this.componentRef!.instance.nativeElement); - return g; - } - - updateImage(nodeG: SVGGElement, previous: T, current: T) { - if (previous !== current && this.componentRef) { - this.componentRef.instance.imageItem = this.options.getImageItem(current); - this.componentRef.instance.element = current; - this.componentRef!.instance.getRectangle = () => { - return this.options.getRectangle(current); - }; - } - const currentForeignObject = this.options.getRectangle(current); - updateForeignObject( - this.g!, - currentForeignObject.width, - currentForeignObject.height, - currentForeignObject.x, - currentForeignObject.y - ); - if (currentForeignObject && current.angle) { - setAngleForG(this.g!, RectangleClient.getCenterPoint(currentForeignObject), current.angle); - } - // solve image lose on move node - if (this.foreignObject.children.length === 0) { - this.foreignObject.append(this.componentRef!.instance.nativeElement); - } - this.componentRef?.instance.cdr.markForCheck(); - } - - destroy(): void { - super.destroy(); - this.componentRef?.destroy(); - } -} diff --git a/packages/common/src/generators/index.ts b/packages/common/src/generators/index.ts index b955041fd..55fdc63d6 100644 --- a/packages/common/src/generators/index.ts +++ b/packages/common/src/generators/index.ts @@ -1,3 +1,3 @@ export * from './generator'; export * from './active.generator'; -export * from './image.generator'; +export * from '../image/image.generator'; diff --git a/packages/common/src/image/image-base.component.ts b/packages/common/src/image/image-base.component.ts new file mode 100644 index 000000000..0da630282 --- /dev/null +++ b/packages/common/src/image/image-base.component.ts @@ -0,0 +1,44 @@ +import { PlaitBoard, PlaitElement } from '@plait/core'; +import { CommonImageItem } from '../utils'; + +export abstract class ImageBaseComponent { + _imageItem!: CommonImageItem; + + _isFocus!: boolean; + + initialized = false; + + element!: PlaitElement; + + set imageItem(value: CommonImageItem) { + this._imageItem = value; + if (this.initialized) { + this.afterImageItemChange(this._imageItem, value); + } + } + + get imageItem() { + return this._imageItem; + } + + board!: PlaitBoard; + + set isFocus(value: boolean) { + this._isFocus = value; + } + + get isFocus() { + return this._isFocus; + } + + abstract afterImageItemChange(previous: CommonImageItem, current: CommonImageItem): void; + + abstract nativeElement(): HTMLElement; + + initialize(): void { + this.initialized = true; + } + + destroy(): void { + } +} diff --git a/packages/common/src/image/image.generator.ts b/packages/common/src/image/image.generator.ts new file mode 100644 index 000000000..e09f776d2 --- /dev/null +++ b/packages/common/src/image/image.generator.ts @@ -0,0 +1,135 @@ +import { + ACTIVE_STROKE_WIDTH, + PlaitBoard, + PlaitElement, + RectangleClient, + createForeignObject, + createG, + getSelectedElements, + isSelectionMoving, + setAngleForG, + updateForeignObject +} from '@plait/core'; +import { Generator, GeneratorExtraData, GeneratorOptions } from '../generators/generator'; +import { CommonImageItem, canResize, getElementOfFocusedImage } from '../utils'; +import { ActiveGenerator } from '../generators/active.generator'; +import { PlaitImageBoard, ImageComponentRef, ImageProps } from './with-image'; + +export interface ImageGeneratorOptions { + getRectangle: (element: T) => RectangleClient; + getImageItem: (element: T) => CommonImageItem; +} + +export class ImageGenerator extends Generator< + T, + GeneratorExtraData, + ImageGeneratorOptions & GeneratorOptions +> { + static key = 'image-generator'; + + foreignObject!: SVGForeignObjectElement; + + imageComponentRef!: ImageComponentRef; + + activeGenerator!: ActiveGenerator; + + isFocus = false; + + element!: T; + + constructor(public board: PlaitBoard, public options: ImageGeneratorOptions) { + super(board, options); + } + + canDraw(element: T): boolean { + return !!this.options.getImageItem(element); + } + + draw(element: T): SVGGElement { + this.element = element; + const g = createG(); + const foreignRectangle = this.options.getRectangle(element); + this.foreignObject = createForeignObject(foreignRectangle.x, foreignRectangle.y, foreignRectangle.width, foreignRectangle.height); + g.append(this.foreignObject); + const props: ImageProps = { + board: this.board, + imageItem: this.options.getImageItem(element), + element, + getRectangle: () => { + return this.options.getRectangle(element); + } + }; + this.imageComponentRef = ((this.board as unknown) as PlaitImageBoard).renderImage(this.foreignObject, props); + + this.activeGenerator = new ActiveGenerator(this.board, { + getStrokeWidth: () => { + const selectedElements = getSelectedElements(this.board); + if (!(selectedElements.length === 1 && !isSelectionMoving(this.board))) { + return ACTIVE_STROKE_WIDTH; + } else { + return ACTIVE_STROKE_WIDTH; + } + }, + getStrokeOpacity: () => { + const selectedElements = getSelectedElements(this.board); + if ((selectedElements.length === 1 && !isSelectionMoving(this.board)) || !selectedElements.length) { + return 1; + } else { + return 0.5; + } + }, + getRectangle: () => { + return this.options.getRectangle(this.element); + }, + hasResizeHandle: () => { + const isSelectedImageElement = canResize(this.board, this.element); + const isSelectedImage = !!getElementOfFocusedImage(this.board); + return isSelectedImage || isSelectedImageElement; + } + }); + return g; + } + + updateImage(nodeG: SVGGElement, previous: T, current: T) { + this.element = current; + if (previous !== current && this.imageComponentRef) { + const props = { + imageItem: this.options.getImageItem(current), + element: current, + getRectangle: () => { + return this.options.getRectangle(current); + } + }; + this.imageComponentRef.update(props); + } + const currentForeignObject = this.options.getRectangle(current); + updateForeignObject( + this.g!, + currentForeignObject.width, + currentForeignObject.height, + currentForeignObject.x, + currentForeignObject.y + ); + if (currentForeignObject && current.angle) { + setAngleForG(this.g!, RectangleClient.getCenterPoint(currentForeignObject), current.angle); + } + const activeG = PlaitBoard.getElementActiveHost(this.board); + this.activeGenerator.processDrawing(current, activeG, { selected: this.isFocus }); + } + + setFocus(element: PlaitElement, isFocus: boolean) { + this.isFocus = isFocus; + const activeG = PlaitBoard.getElementActiveHost(this.board); + this.activeGenerator.processDrawing(element, activeG, { selected: isFocus }); + const props: Partial = { + isFocus + }; + this.imageComponentRef.update(props); + } + + destroy(): void { + super.destroy(); + this.imageComponentRef?.destroy(); + this.activeGenerator?.destroy(); + } +} diff --git a/packages/common/src/image/index.ts b/packages/common/src/image/index.ts new file mode 100644 index 000000000..d2e791fa4 --- /dev/null +++ b/packages/common/src/image/index.ts @@ -0,0 +1,3 @@ +export * from './image-base.component'; +export * from './image.generator'; +export * from './with-image'; diff --git a/packages/common/src/image/with-image.ts b/packages/common/src/image/with-image.ts new file mode 100644 index 000000000..35f3cbde5 --- /dev/null +++ b/packages/common/src/image/with-image.ts @@ -0,0 +1,26 @@ +import { PlaitBoard, PlaitElement, RectangleClient } from '@plait/core'; +import { RenderComponentRef } from '../core/render-component'; +import { CommonImageItem } from '../utils/image'; + +export interface PlaitImageBoard { + renderImage: (container: Element | DocumentFragment, props: ImageProps) => ImageComponentRef; +} + +export const withImage = (board: T) => { + const newBoard = board as T & PlaitImageBoard; + + newBoard.renderImage = (container: Element | DocumentFragment, props: ImageProps) => { + throw new Error('No implementation for renderImage method.'); + }; + return newBoard; +}; + +export type ImageComponentRef = RenderComponentRef; + +export interface ImageProps { + board: PlaitBoard; + imageItem: CommonImageItem; + element: PlaitElement; + isFocus?: boolean; + getRectangle: () => RectangleClient; +} diff --git a/packages/common/src/plugins/index.ts b/packages/common/src/plugins/index.ts index cf55be133..742509fb1 100644 --- a/packages/common/src/plugins/index.ts +++ b/packages/common/src/plugins/index.ts @@ -1,3 +1,2 @@ export * from './with-resize'; -export * from './text-options'; -export * from './with-group'; \ No newline at end of file +export * from './with-group'; diff --git a/packages/common/src/plugins/text-options.ts b/packages/common/src/plugins/text-options.ts deleted file mode 100644 index ab91e37b8..000000000 --- a/packages/common/src/plugins/text-options.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WithPluginOptions } from '@plait/core'; -import { TextPlugin } from '@plait/text'; - -export interface WithTextOptions extends WithPluginOptions { - textPlugins?: TextPlugin[]; -} diff --git a/packages/common/src/public-api.ts b/packages/common/src/public-api.ts index 4d8537e62..8bd399387 100644 --- a/packages/common/src/public-api.ts +++ b/packages/common/src/public-api.ts @@ -8,8 +8,9 @@ export * from './transforms'; export * from './shapes'; export * from './plugins'; export * from './utils'; -export * from './core/element-flavour'; -export * from './core/image-base.component'; -export * from './core/element-ref'; +export * from './image/image-base.component'; +export * from './core'; export * from './algorithms'; +export * from './text'; +export * from './image'; export * from './types'; diff --git a/packages/common/src/text/index.ts b/packages/common/src/text/index.ts new file mode 100644 index 000000000..7bad044e7 --- /dev/null +++ b/packages/common/src/text/index.ts @@ -0,0 +1,4 @@ +export * from './text-manage'; +export * from './with-text'; +export * from './text-measure'; +export * from './types'; diff --git a/packages/common/src/text/text-manage.ts b/packages/common/src/text/text-manage.ts new file mode 100644 index 000000000..937677bfb --- /dev/null +++ b/packages/common/src/text/text-manage.ts @@ -0,0 +1,190 @@ +import { + IS_TEXT_EDITABLE, + MERGING, + PlaitBoard, + Point, + RectangleClient, + createForeignObject, + createG, + setAngleForG, + toHostPoint, + toViewBoxPoint, + updateForeignObject, + updateForeignObjectWidth +} from '@plait/core'; +import { fromEvent, timer } from 'rxjs'; +import { Editor, Element, NodeEntry, Range, Text, Node, Transforms, Operation } from 'slate'; +import { PlaitTextBoard, TextPlugin } from './with-text'; +import { measureElement } from './text-measure'; +import { TextChangeData, TextComponentRef, TextProps } from './with-text'; + +export interface TextManageChangeData { + newText?: Element; + operations?: Operation[]; + width: number; + height: number; +} + +export class TextManage { + isEditing = false; + + editor!: Editor; + + g!: SVGGElement; + + foreignObject!: SVGForeignObjectElement; + + textComponentRef!: TextComponentRef; + + constructor( + private board: PlaitBoard, + private options: { + getRectangle: () => RectangleClient; + onChange?: (data: TextManageChangeData) => void; + getRenderRectangle?: () => RectangleClient; + getMaxWidth?: () => number; + textPlugins?: TextPlugin[]; + } + ) { + if (!this.options.getMaxWidth) { + this.options.getMaxWidth = () => 999; + } + } + + draw(text: Element) { + const _rectangle = this.options.getRectangle(); + this.g = createG(); + this.foreignObject = createForeignObject(_rectangle.x, _rectangle.y, _rectangle.width, _rectangle.height); + this.g.append(this.foreignObject); + this.g.classList.add('text'); + const props: TextProps = { + board: this.board, + text, + textPlugins: this.options.textPlugins, + onChange: (data: TextChangeData) => { + if (data.operations.some(op => !Operation.isSelectionOperation(op))) { + const { width, height } = this.getSize(); + this.options.onChange && this.options.onChange({ ...data, width, height }); + MERGING.set(this.board, true); + } + }, + afterInit: (editor: Editor) => { + this.editor = editor; + }, + onComposition: (event: CompositionEvent) => { + const fakeRoot = buildCompositionData(this.editor, event.data); + if (fakeRoot) { + const sizeData = this.getSize(fakeRoot.children[0]); + this.options.onChange && this.options.onChange(sizeData); + MERGING.set(this.board, true); + } + } + }; + this.textComponentRef = ((this.board as unknown) as PlaitTextBoard).renderText(this.foreignObject, props); + } + + updateRectangleWidth(width: number) { + updateForeignObjectWidth(this.g, width); + } + + updateAngle(centerPoint: Point, angle: number = 0) { + setAngleForG(this.g, centerPoint, angle); + } + + updateRectangle(rectangle?: RectangleClient) { + const { x, y, width, height } = rectangle || this.options.getRectangle(); + updateForeignObject(this.g, width, height, x, y); + } + + updateText(newText: Element) { + const props = { + text: newText + }; + this.textComponentRef.update(props); + } + + edit(callback?: () => void) { + this.isEditing = true; + IS_TEXT_EDITABLE.set(this.board, true); + const props: Partial = { + readonly: false + }; + this.textComponentRef.update(props); + Transforms.select(this.editor, [0]); + const mousedown$ = fromEvent(document, 'mousedown').subscribe((event: MouseEvent) => { + const point = toViewBoxPoint(this.board, toHostPoint(this.board, event.x, event.y)); + const textRec = this.options.getRenderRectangle ? this.options.getRenderRectangle() : this.options.getRectangle(); + const clickInText = RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), textRec); + const isAttached = (event.target as HTMLElement).closest('.plait-board-attached'); + if (!clickInText && !isAttached) { + // handle composition input state, like: Chinese IME Composition Input + timer(0).subscribe(() => { + exitCallback(); + }); + } + }); + const keydown$ = fromEvent(document, 'keydown').subscribe((event: KeyboardEvent) => { + if (event.isComposing) { + return; + } + if (event.key === 'Escape' || (event.key === 'Enter' && !event.shiftKey) || event.key === 'Tab') { + event.preventDefault(); + event.stopPropagation(); + exitCallback(); + return; + } + }); + const exitCallback = () => { + this.updateRectangle(); + mousedown$.unsubscribe(); + keydown$.unsubscribe(); + IS_TEXT_EDITABLE.set(this.board, false); + MERGING.set(this.board, false); + callback && callback(); + const props = { + readonly: true + }; + this.textComponentRef.update(props); + this.isEditing = false; + }; + return exitCallback; + } + + getSize = (element?: Element) => { + const computedStyle = window.getComputedStyle(this.foreignObject.children[0]); + const fontFamily = computedStyle.fontFamily; + const fontSize = parseFloat(computedStyle.fontSize); + const target = element || (this.editor.children[0] as Element); + return measureElement( + target, + { + fontSize: fontSize, + fontFamily + }, + this.options.getMaxWidth!() + ); + }; + + getText = () => { + return this.editor.children[0]; + }; + + destroy() { + this.g?.remove(); + this.textComponentRef?.destroy(); + } +} + +export const buildCompositionData = (editor: Editor, data: string) => { + if (editor.selection && Range.isCollapsed(editor.selection)) { + const [textNode, textPath] = Editor.node(editor, editor.selection) as NodeEntry; + const offset = editor.selection.anchor.offset; + const clonedElement = JSON.parse(JSON.stringify(editor.children[0])); + const root = { children: [clonedElement] }; + const newTextString = textNode.text.slice(0, offset + 1) + data + textNode.text.slice(offset + 1); + const clonedTextNode = Node.get(root, textPath) as Text; + clonedTextNode.text = newTextString; + return root; + } + return null; +}; diff --git a/packages/common/src/text/text-measure.ts b/packages/common/src/text/text-measure.ts new file mode 100644 index 000000000..6258f33b8 --- /dev/null +++ b/packages/common/src/text/text-measure.ts @@ -0,0 +1,74 @@ +import { Node } from 'slate'; +import { CustomText, ParagraphElement } from './types'; +import { getLineHeightByFontSize } from '../utils/text'; + +export function measureElement( + element: ParagraphElement, + options: { + fontSize: number; + fontFamily: string; + }, + containerMaxWidth: number = 10000 +) { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + const textEntries = Node.texts(element); + const lines: CustomText[][] = [[]]; + for (const textEntry of textEntries) { + const [text] = textEntry; + const textString = Node.string(text); + const textArray = textString.split('\n'); + textArray.forEach((segmentTextString: string, index: number) => { + const segmentText = { ...text, text: segmentTextString }; + if (index === 0) { + const currentLine = lines[lines.length - 1]; + currentLine.push(segmentText); + } else { + const newLine: CustomText[] = []; + newLine.push(segmentText); + lines.push(newLine); + } + }); + } + let width = 0; + let height = 0; + lines.forEach((lineTexts: CustomText[], index: number) => { + let lineWidth = 0; + let maxLineHeight = getLineHeightByFontSize(options.fontSize); + lineTexts.forEach((text: CustomText, index: number) => { + const font = getFont(text, { fontFamily: options.fontFamily, fontSize: options.fontSize }); + ctx.font = font; + const textMetrics = ctx.measureText(text.text); + lineWidth += textMetrics.width; + const isLast = index === lineTexts.length - 1; + // skip when text is empty and is not last text of line + if (text['font-size'] && (isLast || text.text !== '')) { + const lineHeight = getLineHeightByFontSize(parseFloat(text['font-size'])); + if (lineHeight > maxLineHeight) { + maxLineHeight = lineHeight; + } + } + }); + if (lineWidth <= containerMaxWidth) { + if (lineWidth > width) { + width = lineWidth; + } + height += maxLineHeight; + } else { + width = containerMaxWidth; + const lineWrapNumber = Math.ceil(lineWidth / containerMaxWidth); + height += maxLineHeight * lineWrapNumber; + } + }); + return { width, height }; +} + +const getFont = ( + text: CustomText, + options: { + fontSize: number; + fontFamily: string; + } +) => { + return `${text.italic ? 'italic ' : ''} ${text.bold ? 'bold ' : ''} ${text['font-size'] || options.fontSize}px ${options.fontFamily} `; +}; diff --git a/packages/common/src/text/types.ts b/packages/common/src/text/types.ts new file mode 100644 index 000000000..a5064706a --- /dev/null +++ b/packages/common/src/text/types.ts @@ -0,0 +1,29 @@ +import { BaseElement, Editor } from 'slate'; + +export enum Alignment { + left = 'left', + center = 'center', + right = 'right' +} + +export type CustomText = { + bold?: boolean; + italic?: boolean; + strike?: boolean; + code?: boolean; + text: string; + underlined?: boolean; + color?: string; + [`font-size`]?: string; +}; + +export interface LinkElement extends BaseElement { + type: 'link'; + url: string; +} + +export interface ParagraphElement extends BaseElement { + align?: Alignment; +} + +export type CustomElement = ParagraphElement | LinkElement; diff --git a/packages/common/src/text/with-text.ts b/packages/common/src/text/with-text.ts new file mode 100644 index 000000000..610e8ff59 --- /dev/null +++ b/packages/common/src/text/with-text.ts @@ -0,0 +1,36 @@ +import { PlaitBoard, WithPluginOptions } from '@plait/core'; +import { Editor, Operation, Element as SlateElement } from 'slate'; +import { RenderComponentRef } from '../core/render-component'; + +export interface PlaitTextBoard { + renderText: (container: Element | DocumentFragment, props: TextProps) => TextComponentRef; +} + +export const withText = (board: T) => { + const newBoard = board as T & PlaitTextBoard; + + newBoard.renderText = (container: Element | DocumentFragment, props: TextProps) => { + throw new Error('No implementation for renderText method.'); + }; + return newBoard; +}; + +export type TextComponentRef = RenderComponentRef; + +export interface TextProps { + board: PlaitBoard; + text: SlateElement; + textPlugins?: TextPlugin[]; + readonly?: boolean; + onChange?: (data: TextChangeData) => void; + afterInit?: (data: Editor) => void; + onComposition?: (data: CompositionEvent) => void; +} + +export type TextChangeData = { newText: SlateElement; operations: Operation[] }; + +export interface WithTextPluginOptions extends WithPluginOptions { + textPlugins?: TextPlugin[]; +} + +export type TextPlugin = (editor: Editor) => Editor; diff --git a/packages/common/src/transforms/index.ts b/packages/common/src/transforms/index.ts index 9f14398ac..397acad55 100644 --- a/packages/common/src/transforms/index.ts +++ b/packages/common/src/transforms/index.ts @@ -1,3 +1,2 @@ export * from './property'; export * from './align'; -export * from './text'; \ No newline at end of file diff --git a/packages/common/src/utils/image.ts b/packages/common/src/utils/image.ts index 72c3774b4..80fbdf309 100644 --- a/packages/common/src/utils/image.ts +++ b/packages/common/src/utils/image.ts @@ -1,5 +1,5 @@ -import { ComponentType, PlaitBoard, PlaitContextService, PlaitElement } from '@plait/core'; -import { ImageBaseComponent } from '../core/image-base.component'; +import { ComponentType, PlaitBoard, PlaitElement } from '@plait/core'; +import { ImageBaseComponent } from '../image/image-base.component'; export interface CommonImageItem { url: string; @@ -7,10 +7,6 @@ export interface CommonImageItem { height: number; } -export interface WithCommonPluginOptions { - imageComponentType?: ComponentType; -} - export const selectImage = ( board: PlaitBoard, defaultImageWidth: number, @@ -42,7 +38,7 @@ export const buildImage = async ( let imageItem = null; const url = URL.createObjectURL(imageFile); - const context = PlaitBoard.getComponent(board).viewContainerRef.injector.get(PlaitContextService); + const context = PlaitBoard.getBoardContext(board); context.setUploadingFile({ url, file: imageFile }); imageItem = { diff --git a/packages/common/src/utils/text.ts b/packages/common/src/utils/text.ts index 0dfc3c2b6..3eacf39bd 100644 --- a/packages/common/src/utils/text.ts +++ b/packages/common/src/utils/text.ts @@ -1,6 +1,7 @@ import { PlaitBoard, PlaitElement, getSelectedElements } from '@plait/core'; -import { CustomText, PlaitMarkEditor, TextManage } from '@plait/text'; -import { Editor, Node } from 'slate'; +import { Editor, Node, Element } from 'slate'; +import { TextManage } from '../text/text-manage'; +import { Alignment, CustomText, ParagraphElement } from '../text/types'; export const getTextManages = (element: PlaitElement) => { return ELEMENT_TO_TEXT_MANAGES.get(element) || []; @@ -16,7 +17,7 @@ export const getFirstTextManage = (element: PlaitElement) => { export const getTextEditorsByElement = (element: PlaitElement) => { return getTextManages(element).map(manage => { - return manage.componentRef.instance.editor; + return manage.editor; }); }; @@ -40,21 +41,6 @@ export const findFirstTextEditor = (board: PlaitBoard) => { return firstEditor; }; -export const getTextMarksByElement = (element: PlaitElement) => { - const editors = getTextEditorsByElement(element); - const editor = editors[0]; - if (!editor) { - return {}; - } - if (editor.children.length === 0) { - const textManage = getTextManages(element)[0]; - const currentMarks: Omit = PlaitMarkEditor.getMarksByElement(textManage.componentRef.instance.children[0]); - return currentMarks; - } - const currentMarks: Omit = PlaitMarkEditor.getMarks(editor); - return currentMarks; -}; - export const getElementsText = (elements: PlaitElement[]) => { return elements .map(item => { @@ -86,10 +72,10 @@ export const getTextEditors = (board: PlaitBoard, elements?: PlaitElement[]) => }); const editingTextManage = textManages.find(textManage => textManage.isEditing); if (editingTextManage) { - return [editingTextManage.componentRef.instance.editor]; + return [editingTextManage.editor]; } return textManages.map(item => { - return item.componentRef.instance.editor; + return item.editor; }); } return undefined; @@ -103,9 +89,28 @@ export const getEditingTextEditor = (board: PlaitBoard, elements?: PlaitElement[ }); const editingTextManage = textManages.find(textManage => textManage.isEditing); if (editingTextManage) { - return editingTextManage.componentRef.instance.editor; + return editingTextManage.editor; } return undefined; }; +export const buildText = (text: string | Element, align?: Alignment, properties?: Partial) => { + properties = properties || {}; + const plaitText = typeof text === 'string' ? { children: [{ text, ...properties }] } : text; + if (align) { + (plaitText as ParagraphElement).align = align; + } + return plaitText; +}; + +export const getLineHeightByFontSize = (fontSize: number) => { + if (fontSize === 14) { + return 20; + } + if (fontSize === 18) { + return 25; + } + return fontSize * 1.5; +}; + export const ELEMENT_TO_TEXT_MANAGES: WeakMap = new WeakMap(); diff --git a/packages/core/package.json b/packages/core/package.json index fab7e2872..42cc203e4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,19 +2,14 @@ "name": "@plait/core", "version": "0.61.0", "peerDependencies": { - "@angular/common": "^17.2.4", - "@angular/core": "^17.2.4", - "is-hotkey": "^0.2.0" + "immer": "^10.0.3", + "is-hotkey": "^0.2.0", + "rxjs": "~7.8.0", + "roughjs": "^4.5.2", + "slate": "^0.101.5" }, "dependencies": { "tslib": "^2.3.0" }, - "exports": { - "./styles/styles": { - "sass": "./styles/styles.scss" - }, - "./styles/*": { - "sass": "./styles/*" - } - } -} + "exports": {} +} \ No newline at end of file diff --git a/packages/core/src/constants/index.ts b/packages/core/src/constants/index.ts index 6bc80888d..141adc930 100644 --- a/packages/core/src/constants/index.ts +++ b/packages/core/src/constants/index.ts @@ -1,6 +1,7 @@ export * from './keycodes'; export * from './cursor'; export * from './selection'; +export * from './zoom'; export const HOST_CLASS_NAME = 'plait-board-container'; diff --git a/packages/core/src/services/context.service.ts b/packages/core/src/context.ts similarity index 81% rename from packages/core/src/services/context.service.ts rename to packages/core/src/context.ts index f1e7ddf57..ee814b5dd 100644 --- a/packages/core/src/services/context.service.ts +++ b/packages/core/src/context.ts @@ -1,9 +1,7 @@ -import { Injectable } from '@angular/core'; -import { ImageEntry } from '../interfaces'; import { Subject } from 'rxjs'; +import { ImageEntry } from './interfaces/element'; -@Injectable() -export class PlaitContextService { +export class PlaitBoardContext { private _stable = new Subject(); private uploadingFiles: ImageEntry[] = []; diff --git a/packages/core/src/core/list-render.ts b/packages/core/src/core/list-render.ts index ba5b9d6ce..b4bc0f006 100644 --- a/packages/core/src/core/list-render.ts +++ b/packages/core/src/core/list-render.ts @@ -1,4 +1,3 @@ -import { IterableChangeRecord, IterableDiffer, IterableDiffers } from '@angular/core'; import { Ancestor, ComponentType, @@ -11,12 +10,14 @@ import { import { NODE_TO_INDEX, NODE_TO_PARENT } from '../utils/weak-maps'; import { addSelectedElement, isSelectedElement, removeSelectedElement } from '../utils/selected-element'; import { ElementFlavour } from './element/element-flavour'; +import { DefaultIterableDiffer } from '../differs/default_iterable_differ'; +import { IterableChangeRecord, IterableDiffer } from '../differs/iterable_differs'; export class ListRender { private children: PlaitElement[] = []; private instances: ElementFlavour[] = []; private contexts: PlaitPluginElementContext[] = []; - private differ: IterableDiffer | null = null; + private differ: IterableDiffer | null = null; public initialized = false; constructor(private board: PlaitBoard) {} @@ -29,12 +30,11 @@ export class ListRender { NODE_TO_PARENT.set(descendant, childrenContext.parent); const context = getContext(this.board, descendant, index, childrenContext.parent); const componentType = getComponentType(this.board, context); - const instance = createPluginComponent(this.board, componentType, context, childrenContext); + const instance = createPluginComponent(this.board, componentType, context, childrenContext); this.instances.push(instance); this.contexts.push(context); }); - const newDiffers = PlaitBoard.getViewContainerRef(this.board).injector.get(IterableDiffers); - this.differ = newDiffers.find(children).create(trackBy); + this.differ = new DefaultIterableDiffer(trackBy); this.differ.diff(children); } @@ -126,8 +126,6 @@ const createPluginComponent = ( context: PlaitPluginElementContext, childrenContext: PlaitChildrenContext ) => { - // const componentRef = PlaitBoard.getViewContainerRef(board).createComponent(componentType, { injector: PlaitBoard.getViewContainerRef(board).injector }); - // const instance = componentRef.instance; const instance = new componentType(); instance.context = context; instance.initialize(); diff --git a/packages/core/src/differs/default_iterable_differ.ts b/packages/core/src/differs/default_iterable_differ.ts new file mode 100644 index 000000000..b1ac9f7c2 --- /dev/null +++ b/packages/core/src/differs/default_iterable_differ.ts @@ -0,0 +1,671 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {isListLikeIterable, iterateListLike} from '../utils/iterable'; + +import {IterableChangeRecord, IterableChanges, IterableDiffer, CustomIterable, TrackByFunction} from './iterable_differs'; + + +const trackByIdentity = (index: number, item: any) => item; + +export type Writable = { + -readonly[K in keyof T]: T[K]; +}; + +export class DefaultIterableDiffer implements IterableDiffer, IterableChanges { + public readonly length: number = 0; + public readonly collection!: V[]|Iterable|null; + // Keeps track of the used records at any point in time (during & across `_check()` calls) + private _linkedRecords: _DuplicateMap|null = null; + // Keeps track of the removed records at any point in time during `_check()` calls. + private _unlinkedRecords: _DuplicateMap|null = null; + private _previousItHead: IterableChangeRecord_|null = null; + private _itHead: IterableChangeRecord_|null = null; + private _itTail: IterableChangeRecord_|null = null; + private _additionsHead: IterableChangeRecord_|null = null; + private _additionsTail: IterableChangeRecord_|null = null; + private _movesHead: IterableChangeRecord_|null = null; + private _movesTail: IterableChangeRecord_|null = null; + private _removalsHead: IterableChangeRecord_|null = null; + private _removalsTail: IterableChangeRecord_|null = null; + // Keeps track of records where custom track by is the same, but item identity has changed + private _identityChangesHead: IterableChangeRecord_|null = null; + private _identityChangesTail: IterableChangeRecord_|null = null; + private _trackByFn: TrackByFunction; + + constructor(trackByFn?: TrackByFunction) { + this._trackByFn = trackByFn || trackByIdentity; + } + + forEachItem(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._itHead; record !== null; record = record._next) { + fn(record); + } + } + + forEachOperation( + fn: (item: IterableChangeRecord, previousIndex: number|null, currentIndex: number|null) => + void) { + let nextIt = this._itHead; + let nextRemove = this._removalsHead; + let addRemoveOffset = 0; + let moveOffsets: number[]|null = null; + while (nextIt || nextRemove) { + // Figure out which is the next record to process + // Order: remove, add, move + const record: IterableChangeRecord = !nextRemove || + nextIt && + nextIt.currentIndex! < + getPreviousIndex(nextRemove, addRemoveOffset, moveOffsets) ? + nextIt! : + nextRemove; + const adjPreviousIndex = getPreviousIndex(record, addRemoveOffset, moveOffsets); + const currentIndex = record.currentIndex; + + // consume the item, and adjust the addRemoveOffset and update moveDistance if necessary + if (record === nextRemove) { + addRemoveOffset--; + nextRemove = nextRemove._nextRemoved; + } else { + nextIt = nextIt!._next; + if (record.previousIndex == null) { + addRemoveOffset++; + } else { + // INVARIANT: currentIndex < previousIndex + if (!moveOffsets) moveOffsets = []; + const localMovePreviousIndex = adjPreviousIndex - addRemoveOffset; + const localCurrentIndex = currentIndex! - addRemoveOffset; + if (localMovePreviousIndex != localCurrentIndex) { + for (let i = 0; i < localMovePreviousIndex; i++) { + const offset = i < moveOffsets.length ? moveOffsets[i] : (moveOffsets[i] = 0); + const index = offset + i; + if (localCurrentIndex <= index && index < localMovePreviousIndex) { + moveOffsets[i] = offset + 1; + } + } + const previousIndex = record.previousIndex; + moveOffsets[previousIndex] = localCurrentIndex - localMovePreviousIndex; + } + } + } + + if (adjPreviousIndex !== currentIndex) { + fn(record, adjPreviousIndex, currentIndex); + } + } + } + + forEachPreviousItem(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._previousItHead; record !== null; record = record._nextPrevious) { + fn(record); + } + } + + forEachAddedItem(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._additionsHead; record !== null; record = record._nextAdded) { + fn(record); + } + } + + forEachMovedItem(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._movesHead; record !== null; record = record._nextMoved) { + fn(record); + } + } + + forEachRemovedItem(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._removalsHead; record !== null; record = record._nextRemoved) { + fn(record); + } + } + + forEachIdentityChange(fn: (record: IterableChangeRecord_) => void) { + let record: IterableChangeRecord_|null; + for (record = this._identityChangesHead; record !== null; record = record._nextIdentityChange) { + fn(record); + } + } + + diff(collection: CustomIterable|null|undefined): DefaultIterableDiffer|null { + if (collection == null) collection = []; + if (!isListLikeIterable(collection)) { + throw new Error('Exception: Error trying to diff. Only arrays and iterables are allowed'); + } + + if (this.check(collection)) { + return this; + } else { + return null; + } + } + + onDestroy() {} + + check(collection: CustomIterable): boolean { + this._reset(); + + let record: IterableChangeRecord_|null = this._itHead; + let mayBeDirty: boolean = false; + let index: number; + let item: V; + let itemTrackBy: any; + if (Array.isArray(collection)) { + (this as Writable).length = collection.length; + + for (let index = 0; index < this.length; index++) { + item = collection[index]; + itemTrackBy = this._trackByFn(index, item); + if (record === null || !Object.is(record.trackById, itemTrackBy)) { + record = this._mismatch(record, item, itemTrackBy, index); + mayBeDirty = true; + } else { + if (mayBeDirty) { + record = this._verifyReinsertion(record, item, itemTrackBy, index); + } + if (!Object.is(record.item, item)) this._addIdentityChange(record, item); + } + + record = record._next; + } + } else { + index = 0; + iterateListLike(collection, (item: V) => { + itemTrackBy = this._trackByFn(index, item); + if (record === null || !Object.is(record.trackById, itemTrackBy)) { + record = this._mismatch(record, item, itemTrackBy, index); + mayBeDirty = true; + } else { + if (mayBeDirty) { + record = this._verifyReinsertion(record, item, itemTrackBy, index); + } + if (!Object.is(record.item, item)) this._addIdentityChange(record, item); + } + record = record._next; + index++; + }); + (this as Writable).length = index; + } + + this._truncate(record); + (this as Writable).collection = collection; + return this.isDirty; + } + + /* CollectionChanges is considered dirty if it has any additions, moves, removals, or identity + * changes. + */ + get isDirty(): boolean { + return this._additionsHead !== null || this._movesHead !== null || + this._removalsHead !== null || this._identityChangesHead !== null; + } + + /** + * Reset the state of the change objects to show no changes. This means set previousKey to + * currentKey, and clear all of the queues (additions, moves, removals). + * Set the previousIndexes of moved and added items to their currentIndexes + * Reset the list of additions, moves and removals + * + * @internal + */ + _reset() { + if (this.isDirty) { + let record: IterableChangeRecord_|null; + + for (record = this._previousItHead = this._itHead; record !== null; record = record._next) { + record._nextPrevious = record._next; + } + + for (record = this._additionsHead; record !== null; record = record._nextAdded) { + record.previousIndex = record.currentIndex; + } + this._additionsHead = this._additionsTail = null; + + for (record = this._movesHead; record !== null; record = record._nextMoved) { + record.previousIndex = record.currentIndex; + } + this._movesHead = this._movesTail = null; + this._removalsHead = this._removalsTail = null; + this._identityChangesHead = this._identityChangesTail = null; + } + } + + /** + * This is the core function which handles differences between collections. + * + * - `record` is the record which we saw at this position last time. If null then it is a new + * item. + * - `item` is the current item in the collection + * - `index` is the position of the item in the collection + * + * @internal + */ + _mismatch(record: IterableChangeRecord_|null, item: V, itemTrackBy: any, index: number): + IterableChangeRecord_ { + // The previous record after which we will append the current one. + let previousRecord: IterableChangeRecord_|null; + + if (record === null) { + previousRecord = this._itTail; + } else { + previousRecord = record._prev; + // Remove the record from the collection since we know it does not match the item. + this._remove(record); + } + + // See if we have evicted the item, which used to be at some anterior position of _itHead list. + record = this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null); + if (record !== null) { + // It is an item which we have evicted earlier: reinsert it back into the list. + // But first we need to check if identity changed, so we can update in view if necessary. + if (!Object.is(record.item, item)) this._addIdentityChange(record, item); + + this._reinsertAfter(record, previousRecord, index); + } else { + // Attempt to see if the item is at some posterior position of _itHead list. + record = this._linkedRecords === null ? null : this._linkedRecords.get(itemTrackBy, index); + if (record !== null) { + // We have the item in _itHead at/after `index` position. We need to move it forward in the + // collection. + // But first we need to check if identity changed, so we can update in view if necessary. + if (!Object.is(record.item, item)) this._addIdentityChange(record, item); + + this._moveAfter(record, previousRecord, index); + } else { + // It is a new item: add it. + record = + this._addAfter(new IterableChangeRecord_(item, itemTrackBy), previousRecord, index); + } + } + return record; + } + + /** + * This check is only needed if an array contains duplicates. (Short circuit of nothing dirty) + * + * Use case: `[a, a]` => `[b, a, a]` + * + * If we did not have this check then the insertion of `b` would: + * 1) evict first `a` + * 2) insert `b` at `0` index. + * 3) leave `a` at index `1` as is. <-- this is wrong! + * 3) reinsert `a` at index 2. <-- this is wrong! + * + * The correct behavior is: + * 1) evict first `a` + * 2) insert `b` at `0` index. + * 3) reinsert `a` at index 1. + * 3) move `a` at from `1` to `2`. + * + * + * Double check that we have not evicted a duplicate item. We need to check if the item type may + * have already been removed: + * The insertion of b will evict the first 'a'. If we don't reinsert it now it will be reinserted + * at the end. Which will show up as the two 'a's switching position. This is incorrect, since a + * better way to think of it is as insert of 'b' rather then switch 'a' with 'b' and then add 'a' + * at the end. + * + * @internal + */ + _verifyReinsertion(record: IterableChangeRecord_, item: V, itemTrackBy: any, index: number): + IterableChangeRecord_ { + let reinsertRecord: IterableChangeRecord_|null = + this._unlinkedRecords === null ? null : this._unlinkedRecords.get(itemTrackBy, null); + if (reinsertRecord !== null) { + record = this._reinsertAfter(reinsertRecord, record._prev!, index); + } else if (record.currentIndex != index) { + record.currentIndex = index; + this._addToMoves(record, index); + } + return record; + } + + /** + * Get rid of any excess {@link IterableChangeRecord_}s from the previous collection + * + * - `record` The first excess {@link IterableChangeRecord_}. + * + * @internal + */ + _truncate(record: IterableChangeRecord_|null) { + // Anything after that needs to be removed; + while (record !== null) { + const nextRecord: IterableChangeRecord_|null = record._next; + this._addToRemovals(this._unlink(record)); + record = nextRecord; + } + if (this._unlinkedRecords !== null) { + this._unlinkedRecords.clear(); + } + + if (this._additionsTail !== null) { + this._additionsTail._nextAdded = null; + } + if (this._movesTail !== null) { + this._movesTail._nextMoved = null; + } + if (this._itTail !== null) { + this._itTail._next = null; + } + if (this._removalsTail !== null) { + this._removalsTail._nextRemoved = null; + } + if (this._identityChangesTail !== null) { + this._identityChangesTail._nextIdentityChange = null; + } + } + + /** @internal */ + _reinsertAfter( + record: IterableChangeRecord_, prevRecord: IterableChangeRecord_|null, + index: number): IterableChangeRecord_ { + if (this._unlinkedRecords !== null) { + this._unlinkedRecords.remove(record); + } + const prev = record._prevRemoved; + const next = record._nextRemoved; + + if (prev === null) { + this._removalsHead = next; + } else { + prev._nextRemoved = next; + } + if (next === null) { + this._removalsTail = prev; + } else { + next._prevRemoved = prev; + } + + this._insertAfter(record, prevRecord, index); + this._addToMoves(record, index); + return record; + } + + /** @internal */ + _moveAfter( + record: IterableChangeRecord_, prevRecord: IterableChangeRecord_|null, + index: number): IterableChangeRecord_ { + this._unlink(record); + this._insertAfter(record, prevRecord, index); + this._addToMoves(record, index); + return record; + } + + /** @internal */ + _addAfter( + record: IterableChangeRecord_, prevRecord: IterableChangeRecord_|null, + index: number): IterableChangeRecord_ { + this._insertAfter(record, prevRecord, index); + + if (this._additionsTail === null) { + // assert(this._additionsHead === null); + this._additionsTail = this._additionsHead = record; + } else { + // assert(_additionsTail._nextAdded === null); + // assert(record._nextAdded === null); + this._additionsTail = this._additionsTail._nextAdded = record; + } + return record; + } + + /** @internal */ + _insertAfter( + record: IterableChangeRecord_, prevRecord: IterableChangeRecord_|null, + index: number): IterableChangeRecord_ { + + const next: IterableChangeRecord_|null = + prevRecord === null ? this._itHead : prevRecord._next; + record._next = next; + record._prev = prevRecord; + if (next === null) { + this._itTail = record; + } else { + next._prev = record; + } + if (prevRecord === null) { + this._itHead = record; + } else { + prevRecord._next = record; + } + + if (this._linkedRecords === null) { + this._linkedRecords = new _DuplicateMap(); + } + this._linkedRecords.put(record); + + record.currentIndex = index; + return record; + } + + /** @internal */ + _remove(record: IterableChangeRecord_): IterableChangeRecord_ { + return this._addToRemovals(this._unlink(record)); + } + + /** @internal */ + _unlink(record: IterableChangeRecord_): IterableChangeRecord_ { + if (this._linkedRecords !== null) { + this._linkedRecords.remove(record); + } + + const prev = record._prev; + const next = record._next; + + if (prev === null) { + this._itHead = next; + } else { + prev._next = next; + } + if (next === null) { + this._itTail = prev; + } else { + next._prev = prev; + } + + return record; + } + + /** @internal */ + _addToMoves(record: IterableChangeRecord_, toIndex: number): IterableChangeRecord_ { + if (record.previousIndex === toIndex) { + return record; + } + + if (this._movesTail === null) { + this._movesTail = this._movesHead = record; + } else { + this._movesTail = this._movesTail._nextMoved = record; + } + + return record; + } + + private _addToRemovals(record: IterableChangeRecord_): IterableChangeRecord_ { + if (this._unlinkedRecords === null) { + this._unlinkedRecords = new _DuplicateMap(); + } + this._unlinkedRecords.put(record); + record.currentIndex = null; + record._nextRemoved = null; + + if (this._removalsTail === null) { + this._removalsTail = this._removalsHead = record; + record._prevRemoved = null; + } else { + record._prevRemoved = this._removalsTail; + this._removalsTail = this._removalsTail._nextRemoved = record; + } + return record; + } + + /** @internal */ + _addIdentityChange(record: IterableChangeRecord_, item: V) { + record.item = item; + if (this._identityChangesTail === null) { + this._identityChangesTail = this._identityChangesHead = record; + } else { + this._identityChangesTail = this._identityChangesTail._nextIdentityChange = record; + } + return record; + } +} + +export class IterableChangeRecord_ implements IterableChangeRecord { + currentIndex: number|null = null; + previousIndex: number|null = null; + + /** @internal */ + _nextPrevious: IterableChangeRecord_|null = null; + /** @internal */ + _prev: IterableChangeRecord_|null = null; + /** @internal */ + _next: IterableChangeRecord_|null = null; + /** @internal */ + _prevDup: IterableChangeRecord_|null = null; + /** @internal */ + _nextDup: IterableChangeRecord_|null = null; + /** @internal */ + _prevRemoved: IterableChangeRecord_|null = null; + /** @internal */ + _nextRemoved: IterableChangeRecord_|null = null; + /** @internal */ + _nextAdded: IterableChangeRecord_|null = null; + /** @internal */ + _nextMoved: IterableChangeRecord_|null = null; + /** @internal */ + _nextIdentityChange: IterableChangeRecord_|null = null; + + + constructor(public item: V, public trackById: any) {} +} + +// A linked list of IterableChangeRecords with the same IterableChangeRecord_.item +class _DuplicateItemRecordList { + /** @internal */ + _head: IterableChangeRecord_|null = null; + /** @internal */ + _tail: IterableChangeRecord_|null = null; + + /** + * Append the record to the list of duplicates. + * + * Note: by design all records in the list of duplicates hold the same value in record.item. + */ + add(record: IterableChangeRecord_): void { + if (this._head === null) { + this._head = this._tail = record; + record._nextDup = null; + record._prevDup = null; + } else { + this._tail!._nextDup = record; + record._prevDup = this._tail; + record._nextDup = null; + this._tail = record; + } + } + + // Returns a IterableChangeRecord_ having IterableChangeRecord_.trackById == trackById and + // IterableChangeRecord_.currentIndex >= atOrAfterIndex + get(trackById: any, atOrAfterIndex: number|null): IterableChangeRecord_|null { + let record: IterableChangeRecord_|null; + for (record = this._head; record !== null; record = record._nextDup) { + if ((atOrAfterIndex === null || atOrAfterIndex <= record.currentIndex!) && + Object.is(record.trackById, trackById)) { + return record; + } + } + return null; + } + + /** + * Remove one {@link IterableChangeRecord_} from the list of duplicates. + * + * Returns whether the list of duplicates is empty. + */ + remove(record: IterableChangeRecord_): boolean { + + const prev: IterableChangeRecord_|null = record._prevDup; + const next: IterableChangeRecord_|null = record._nextDup; + if (prev === null) { + this._head = next; + } else { + prev._nextDup = next; + } + if (next === null) { + this._tail = prev; + } else { + next._prevDup = prev; + } + return this._head === null; + } +} + +class _DuplicateMap { + map = new Map>(); + + put(record: IterableChangeRecord_) { + const key = record.trackById; + + let duplicates = this.map.get(key); + if (!duplicates) { + duplicates = new _DuplicateItemRecordList(); + this.map.set(key, duplicates); + } + duplicates.add(record); + } + + /** + * Retrieve the `value` using key. Because the IterableChangeRecord_ value may be one which we + * have already iterated over, we use the `atOrAfterIndex` to pretend it is not there. + * + * Use case: `[a, b, c, a, a]` if we are at index `3` which is the second `a` then asking if we + * have any more `a`s needs to return the second `a`. + */ + get(trackById: any, atOrAfterIndex: number|null): IterableChangeRecord_|null { + const key = trackById; + const recordList = this.map.get(key); + return recordList ? recordList.get(trackById, atOrAfterIndex) : null; + } + + /** + * Removes a {@link IterableChangeRecord_} from the list of duplicates. + * + * The list of duplicates also is removed from the map if it gets empty. + */ + remove(record: IterableChangeRecord_): IterableChangeRecord_ { + const key = record.trackById; + const recordList: _DuplicateItemRecordList = this.map.get(key)!; + // Remove the list of duplicates when it gets empty + if (recordList.remove(record)) { + this.map.delete(key); + } + return record; + } + + get isEmpty(): boolean { + return this.map.size === 0; + } + + clear() { + this.map.clear(); + } +} + +function getPreviousIndex(item: any, addRemoveOffset: number, moveOffsets: number[]|null): number { + const previousIndex = item.previousIndex; + if (previousIndex === null) return previousIndex; + let moveOffset = 0; + if (moveOffsets && previousIndex < moveOffsets.length) { + moveOffset = moveOffsets[previousIndex]; + } + return previousIndex + addRemoveOffset + moveOffset; +} diff --git a/packages/core/src/differs/iterable_differs.ts b/packages/core/src/differs/iterable_differs.ts new file mode 100644 index 000000000..f5ad810b2 --- /dev/null +++ b/packages/core/src/differs/iterable_differs.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * A type describing supported iterable types. + * + * @publicApi + */ +export type CustomIterable = Array|Iterable; + +/** + * A strategy for tracking changes over time to an iterable. Used by {@link NgForOf} to + * respond to changes in an iterable by effecting equivalent changes in the DOM. + * + * @publicApi + */ +export interface IterableDiffer { + /** + * Compute a difference between the previous state and the new `object` state. + * + * @param object containing the new value. + * @returns an object describing the difference. The return value is only valid until the next + * `diff()` invocation. + */ + diff(object: CustomIterable|undefined|null): IterableChanges|null; +} + +/** + * An object describing the changes in the `Iterable` collection since last time + * `IterableDiffer#diff()` was invoked. + * + * @publicApi + */ +export interface IterableChanges { + /** + * Iterate over all changes. `IterableChangeRecord` will contain information about changes + * to each item. + */ + forEachItem(fn: (record: IterableChangeRecord) => void): void; + + /** + * Iterate over a set of operations which when applied to the original `Iterable` will produce the + * new `Iterable`. + * + * NOTE: These are not necessarily the actual operations which were applied to the original + * `Iterable`, rather these are a set of computed operations which may not be the same as the + * ones applied. + * + * @param record A change which needs to be applied + * @param previousIndex The `IterableChangeRecord#previousIndex` of the `record` refers to the + * original `Iterable` location, where as `previousIndex` refers to the transient location + * of the item, after applying the operations up to this point. + * @param currentIndex The `IterableChangeRecord#currentIndex` of the `record` refers to the + * original `Iterable` location, where as `currentIndex` refers to the transient location + * of the item, after applying the operations up to this point. + */ + forEachOperation( + fn: + (record: IterableChangeRecord, previousIndex: number|null, + currentIndex: number|null) => void): void; + + /** + * Iterate over changes in the order of original `Iterable` showing where the original items + * have moved. + */ + forEachPreviousItem(fn: (record: IterableChangeRecord) => void): void; + + /** Iterate over all added items. */ + forEachAddedItem(fn: (record: IterableChangeRecord) => void): void; + + /** Iterate over all moved items. */ + forEachMovedItem(fn: (record: IterableChangeRecord) => void): void; + + /** Iterate over all removed items. */ + forEachRemovedItem(fn: (record: IterableChangeRecord) => void): void; + + /** + * Iterate over all items which had their identity (as computed by the `TrackByFunction`) + * changed. + */ + forEachIdentityChange(fn: (record: IterableChangeRecord) => void): void; +} + +/** + * Record representing the item change information. + * + * @publicApi + */ +export interface IterableChangeRecord { + /** Current index of the item in `Iterable` or null if removed. */ + readonly currentIndex: number|null; + + /** Previous index of the item in `Iterable` or null if added. */ + readonly previousIndex: number|null; + + /** The item. */ + readonly item: V; + + /** Track by identity as computed by the `TrackByFunction`. */ + readonly trackById: any; +} + +/** + * A function optionally passed into the `NgForOf` directive to customize how `NgForOf` uniquely + * identifies items in an iterable. + * + * `NgForOf` needs to uniquely identify items in the iterable to correctly perform DOM updates + * when items in the iterable are reordered, new items are added, or existing items are removed. + * + * + * In all of these scenarios it is usually desirable to only update the DOM elements associated + * with the items affected by the change. This behavior is important to: + * + * - preserve any DOM-specific UI state (like cursor position, focus, text selection) when the + * iterable is modified + * - enable animation of item addition, removal, and iterable reordering + * - preserve the value of the ` diff --git a/src/app/editor/editor.component.ts b/src/app/editor/editor.component.ts index c70695a3e..6cfd361cc 100644 --- a/src/app/editor/editor.component.ts +++ b/src/app/editor/editor.component.ts @@ -2,7 +2,6 @@ import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular import { BoardTransforms, PlaitBoard, - PlaitBoardChangeEvent, PlaitBoardOptions, PlaitElement, PlaitTheme, @@ -19,7 +18,8 @@ import { Transforms, duplicateElements, setFragment, - WritableClipboardOperationType + WritableClipboardOperationType, + PlaitPlugin } from '@plait/core'; import { mockDrawData, mockTableData, mockMindData, mockRotateData, mockGroupData, mockSwimlaneData } from './mock-data'; import { withMind, PlaitMindBoard, PlaitMind } from '@plait/mind'; @@ -30,7 +30,6 @@ import { AppSettingPanelComponent } from '../components/setting-panel/setting-pa import { AppMainToolbarComponent } from '../components/main-toolbar/main-toolbar.component'; import { AppZoomToolbarComponent } from '../components/zoom-toolbar/zoom-toolbar.component'; import { FormsModule } from '@angular/forms'; -import { PlaitBoardComponent } from '../../../packages/core/src/board/board.component'; import { ActivatedRoute, Params } from '@angular/router'; import { mockLineData, withLineRoute } from '../plugins/with-line-route'; import { withCommonPlugin } from '../plugins/with-common'; @@ -38,6 +37,7 @@ import { AppMenuComponent } from '../components/menu/menu.component'; import { NgIf } from '@angular/common'; import { mockTurningPointData } from './mock-turning-point-data'; import { withGroup } from '@plait/common'; +import { OnChangeData, PlaitBoardComponent } from '@plait/angular-board'; const LOCAL_STORAGE_KEY = 'plait-board-data'; @@ -56,7 +56,7 @@ const LOCAL_STORAGE_KEY = 'plait-board-data'; ] }) export class BasicEditorComponent implements OnInit { - plugins = [withCommonPlugin, withMind, withMindExtend, withDraw, withGroup]; + plugins: PlaitPlugin[] = [withCommonPlugin, withMind, withMindExtend, withDraw, withGroup]; value: (PlaitElement | PlaitGeometry | PlaitMind)[] = []; @@ -151,7 +151,7 @@ export class BasicEditorComponent implements OnInit { }); } - change(event: PlaitBoardChangeEvent) { + change(event: OnChangeData) { this.setLocalData(JSON.stringify(event)); this.selectedElements = getSelectedElements(this.board); this.showRemoveGroup = canRemoveGroup(this.board); diff --git a/src/app/editor/emoji/emoji.component.ts b/src/app/editor/emoji/emoji.component.ts index f6a49b3ed..3cc174018 100644 --- a/src/app/editor/emoji/emoji.component.ts +++ b/src/app/editor/emoji/emoji.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, OnInit, inject } from '@angular/core'; import { MindEmojiBaseComponent } from '@plait/mind'; @Component({ @@ -8,12 +8,14 @@ import { MindEmojiBaseComponent } from '@plait/mind'; changeDetection: ChangeDetectionStrategy.OnPush }) export class MindEmojiComponent extends MindEmojiBaseComponent implements OnInit { - constructor(protected elementRef: ElementRef) { - super(elementRef); + elementRef = inject(ElementRef) + + nativeElement() { + return this.elementRef.nativeElement; } ngOnInit(): void { - super.ngOnInit(); - this.nativeElement.innerHTML = this.emojiItem.name; + super.initialize(); + this.nativeElement().innerHTML = this.emojiItem.name; } } diff --git a/src/app/editor/image/image.component.ts b/src/app/editor/image/image.component.ts index da4236048..2d518f63d 100644 --- a/src/app/editor/image/image.component.ts +++ b/src/app/editor/image/image.component.ts @@ -9,10 +9,22 @@ import { ImageBaseComponent } from '@plait/common'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: true }) -export class PlaitImageComponent extends ImageBaseComponent { - constructor(protected elementRef: ElementRef, cdr: ChangeDetectorRef) { - super(elementRef, cdr); +export class PlaitImageComponent extends ImageBaseComponent implements OnInit { + constructor(protected elementRef: ElementRef, public cdr: ChangeDetectorRef) { + super(); } - afterImageItemChange() {} + nativeElement() { + return this.elementRef.nativeElement; + } + + ngOnInit(): void { + super.initialize(); + } + + afterImageItemChange() { + if (this.initialized) { + this.cdr.detectChanges(); + } + } } diff --git a/src/app/flow/custom-node.component.ts b/src/app/flow/custom-node.component.ts index f3434f286..9aa942860 100644 --- a/src/app/flow/custom-node.component.ts +++ b/src/app/flow/custom-node.component.ts @@ -1,12 +1,12 @@ +import { TextManage } from '@plait/common'; import { PlaitBoard, Point, drawCircle, normalizePoint } from '@plait/core'; import { FlowNode, FlowNodeComponent, NodeActiveGenerator, NodeGenerator } from '@plait/flow'; -import { TextManage } from '@plait/text'; export class CustomFlowNodeComponent extends FlowNodeComponent { initializeGenerator() { this.nodeGenerator = new CustomNodeGenerator(this.board); this.nodeActiveGenerator = new CustomNodeActiveGenerator(this.board); - this.textManage = new TextManage(this.board, PlaitBoard.getViewContainerRef(this.board), { + this.textManage = new TextManage(this.board, { getRectangle: () => { const { x, y } = normalizePoint(this.element.points![0]); const width = this.element.width; diff --git a/src/app/flow/flow.component.html b/src/app/flow/flow.component.html index 02eeac927..9fad816b8 100644 --- a/src/app/flow/flow.component.html +++ b/src/app/flow/flow.component.html @@ -22,6 +22,6 @@

{{ menu.name }}

[plaitViewport]="viewport" [plaitOptions]="options" (plaitBoardInitialized)="plaitBoardInitialized($event)" - (plaitChange)="change($event)" + (onChange)="change($event)" > diff --git a/src/app/flow/flow.component.ts b/src/app/flow/flow.component.ts index c76f87bf7..40f9354ad 100644 --- a/src/app/flow/flow.component.ts +++ b/src/app/flow/flow.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, Injector, HostBinding, ChangeDetectorRef } from '@angular/core'; -import { BoardTransforms, PlaitBoardChangeEvent, PlaitBoardOptions, PlaitElement, Viewport } from '@plait/core'; +import { BoardTransforms, PlaitBoardOptions, PlaitElement, Viewport } from '@plait/core'; import { withFlow } from '@plait/flow'; import { withCommon } from './plugins/with-common'; import { withDraw } from './plugins/with-draw'; import { CustomBoard } from './interfaces/board'; -import { PlaitBoardComponent } from '../../../packages/core/src/board/board.component'; import { NgClass, NgFor } from '@angular/common'; import { mockBasicEdges, mockMarkEdges, mockIconEdges, mockShapeEdges } from './flow-edge-data'; import { mockBasicNodes, mockCustomNodes, mockCustomHandles, mockUndeletableNodes } from './flow-node-data'; import { mockFlowData } from './flow-data'; +import { OnChangeData, PlaitBoardComponent } from '@plait/angular-board'; const LOCAL_DATA_KEY = 'plait-board-flow-change-data'; @@ -88,7 +88,7 @@ export class BasicFlowComponent implements OnInit { this.value = mockFlowData; } - change(event: PlaitBoardChangeEvent) { + change(event: OnChangeData) { this.setLocalData(JSON.stringify(event)); } diff --git a/src/app/flow/icon.component.ts b/src/app/flow/icon.component.ts index 72cd607d8..40931be9f 100644 --- a/src/app/flow/icon.component.ts +++ b/src/app/flow/icon.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, OnInit, inject } from '@angular/core'; import { FlowEdgeLabelIconBaseComponent } from '@plait/flow'; @Component({ @@ -10,11 +10,13 @@ import { FlowEdgeLabelIconBaseComponent } from '@plait/flow'; changeDetection: ChangeDetectionStrategy.OnPush }) export class IconComponent extends FlowEdgeLabelIconBaseComponent implements OnInit { - constructor(protected elementRef: ElementRef) { - super(elementRef); + elementRef = inject(ElementRef); + + nativeElement(): HTMLElement { + return this.elementRef.nativeElement; } ngOnInit(): void { - super.ngOnInit(); + super.initialize(); } } diff --git a/src/app/flow/plugins/with-draw.ts b/src/app/flow/plugins/with-draw.ts index 0700e5d46..d2c285e3f 100644 --- a/src/app/flow/plugins/with-draw.ts +++ b/src/app/flow/plugins/with-draw.ts @@ -1,11 +1,12 @@ import { PlaitBoard, PlaitOptionsBoard, PlaitPlugin, PlaitPluginElementContext } from '@plait/core'; -import { FlowElement, FlowNode, PlaitFlowBoard, FlowPluginOptions, FlowPluginKey } from '@plait/flow'; +import { FlowElement, FlowNode, FlowPluginOptions, FlowPluginKey, PlaitFlowLabelIconBoard, LabelIconProps } from '@plait/flow'; import { CustomFlowNodeComponent } from '../custom-node.component'; import { IconComponent } from '../icon.component'; import { WorkflowType } from '../flow-data'; +import { AngularBoard } from '@plait/angular-board'; export const withDraw: PlaitPlugin = (board: PlaitBoard) => { - const newBoard = board as PlaitBoard & PlaitFlowBoard; + const newBoard = board as PlaitBoard & PlaitFlowLabelIconBoard & AngularBoard; const { drawElement } = board; @@ -18,8 +19,9 @@ export const withDraw: PlaitPlugin = (board: PlaitBoard) => { return drawElement(context); }; - newBoard.drawLabelIcon = () => { - return IconComponent; + newBoard.renderLabelIcon = (container: Element | DocumentFragment, props: LabelIconProps) => { + const { ref } = newBoard.renderComponent(IconComponent, container, props); + return ref; }; (board as PlaitOptionsBoard).setPluginOptions(FlowPluginKey.flowOptions, { diff --git a/src/app/plugins/with-common.ts b/src/app/plugins/with-common.ts index ec1262c88..85c0fee72 100644 --- a/src/app/plugins/with-common.ts +++ b/src/app/plugins/with-common.ts @@ -1,14 +1,15 @@ -import { PlaitMindBoard } from '@plait/mind'; -import { PlaitBoard, PlaitOptionsBoard } from '@plait/core'; -import { WithCommonPluginKey, WithCommonPluginOptions } from '@plait/common'; +import { PlaitBoard } from '@plait/core'; +import { ImageProps, PlaitImageBoard } from '@plait/common'; +import { AngularBoard } from '@plait/angular-board'; import { PlaitImageComponent } from '../editor/image/image.component'; export const withCommonPlugin = (board: PlaitBoard) => { - const newBoard = board as PlaitBoard & PlaitMindBoard; + const newBoard = board as PlaitBoard & PlaitImageBoard & AngularBoard; - (board as PlaitOptionsBoard).setPluginOptions(WithCommonPluginKey, { - imageComponentType: PlaitImageComponent - }); + newBoard.renderImage = (container: Element | DocumentFragment, props: ImageProps) => { + const { ref } = newBoard.renderComponent(PlaitImageComponent, container, props); + return ref; + }; - return newBoard; + return board; }; diff --git a/src/app/plugins/with-mind-extend.ts b/src/app/plugins/with-mind-extend.ts index 5c94ff2d5..85a20a445 100644 --- a/src/app/plugins/with-mind-extend.ts +++ b/src/app/plugins/with-mind-extend.ts @@ -1,16 +1,21 @@ -import { PlaitMindBoard, WithMindOptions, WithMindPluginKey } from '@plait/mind'; +import { EmojiProps, PlaitMindBoard, PlaitMindEmojiBoard, WithMindOptions, WithMindPluginKey } from '@plait/mind'; import { PlaitBoard, PlaitOptionsBoard } from '@plait/core'; +import { AngularBoard } from '@plait/angular-board'; import { MindEmojiComponent } from '../editor/emoji/emoji.component'; export const withMindExtend = (board: PlaitBoard) => { - const newBoard = board as PlaitBoard & PlaitMindBoard; + const newBoard = board as PlaitBoard & PlaitMindBoard & PlaitMindEmojiBoard & AngularBoard; (board as PlaitOptionsBoard).setPluginOptions(WithMindPluginKey, { isMultiple: true, emojiPadding: 0, - spaceBetweenEmojis: 4, - emojiComponentType: MindEmojiComponent + spaceBetweenEmojis: 4 }); + newBoard.renderEmoji = (container: Element | DocumentFragment, props: EmojiProps) => { + const { ref } = newBoard.renderComponent(MindEmojiComponent, container, props); + return ref; + }; + return newBoard; }; diff --git a/src/app/richtext/richtext.component.html b/src/app/richtext/richtext.component.html index 81ee99bc4..ca3261553 100644 --- a/src/app/richtext/richtext.component.html +++ b/src/app/richtext/richtext.component.html @@ -1,4 +1,4 @@

Richtext 示例:

- +
\ No newline at end of file diff --git a/src/app/richtext/richtext.component.scss b/src/app/richtext/richtext.component.scss index ff184b5db..213379a87 100644 --- a/src/app/richtext/richtext.component.scss +++ b/src/app/richtext/richtext.component.scss @@ -5,7 +5,7 @@ margin: 0 auto; margin-top: 50px; - .plait-richtext-container { + .plait-text-container { padding: 8px; border: 1px solid gray; border-radius: 3px; diff --git a/src/app/richtext/richtext.component.ts b/src/app/richtext/richtext.component.ts index 925736fec..86bc48ccc 100644 --- a/src/app/richtext/richtext.component.ts +++ b/src/app/richtext/richtext.component.ts @@ -1,18 +1,20 @@ import { Component } from '@angular/core'; import { Editor } from 'slate'; -import { PlaitRichtextComponent } from '../../../packages/text/src/richtext/richtext.component'; +import { PlaitTextComponent } from '../../../packages/angular-text/src/text/text.component'; +import { TextChangeData } from '@plait/common'; @Component({ selector: 'app-basic-richtext', templateUrl: './richtext.component.html', standalone: true, - imports: [PlaitRichtextComponent] + imports: [PlaitTextComponent] }) export class BasicRichtextComponent { value = { children: [{ text: '富文本' }] }; - onChange(event: Editor) { - console.log(event); - } + + onChangeHandle = (data: TextChangeData) => { + console.log(data); + }; } diff --git a/tsconfig.app.json b/tsconfig.app.json index 680877a9f..b3e0ae783 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -17,8 +17,14 @@ "@plait/core": [ "packages/core/src/public-api", ], - "@plait/text": [ - "packages/text/src/public-api", + "@plait/angular-board": [ + "packages/angular-board/src/public-api", + ], + "@plait/angular-text": [ + "packages/angular-text/src/public-api", + ], + "@plait/text-plugins": [ + "packages/text-plugins/src/public-api", ], "@plait/draw": [ "packages/draw/src/public-api", diff --git a/tsconfig.json b/tsconfig.json index cffaf5ef0..4ac7a7ae4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,14 +16,20 @@ "@plait/core": [ "dist/core" ], - "@plait/text": [ - "dist/text" + "@plait/angular-board": [ + "dist/angular-board" + ], + "@plait/angular-text": [ + "dist/angular-text" ], "@plait/draw": [ "dist/draw" ], "@plait/common": [ "dist/common" + ], + "@plait/text-plugins": [ + "dist/text-plugins" ] }, "outDir": "./dist/out-tsc",