Compare commits
14 Commits
v0.0.1
...
content-su
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bb8f9c697 | |||
| 74f11aefbc | |||
| 28b91cd9eb | |||
| d879af2881 | |||
| cacbc016ec | |||
| a8c2eaa2fd | |||
| 9eaca089e5 | |||
| 8ac6e00b68 | |||
| 81c5550311 | |||
| 1c1b9748c6 | |||
| 6c73b0fa61 | |||
| e4e6bc5af4 | |||
| 028ce21c4c | |||
| 915c56351b |
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,3 +1,12 @@
|
|||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
node_modules
|
node_modules
|
||||||
*.log
|
**/node_modules
|
||||||
storybook-static
|
**/.turbo
|
||||||
|
coverage
|
||||||
|
.DS_Store
|
||||||
|
**/storybook-static
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { StorybookConfig } from "@storybook/react-vite";
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ["../stories/**/*.stories.@(ts|tsx)"],
|
|
||||||
addons: ["@storybook/addon-essentials"],
|
|
||||||
framework: {
|
|
||||||
name: "@storybook/react-vite",
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: "tag",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
:root {
|
|
||||||
--Default-BgLight: #ffffff;
|
|
||||||
--Default-BgDarken: #f2f2f2;
|
|
||||||
--Default-StrokeDividers: #e0e0e0;
|
|
||||||
--Default-White: #ffffff;
|
|
||||||
--Text-Primary: #1d1d1d;
|
|
||||||
--Controls-Primary: #1d1d1d;
|
|
||||||
--Controls-Plashes: #f5f5f5;
|
|
||||||
--Shadow-Z100: 0 10px 24px rgba(20, 20, 20, 0.14);
|
|
||||||
--Accent-Primary: #5152ba;
|
|
||||||
--Opacity-BlackOpacity45: rgba(0, 0, 0, 0.45);
|
|
||||||
--corner-S: 8px;
|
|
||||||
--corner-M: 12px;
|
|
||||||
--Corner-XL: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body,
|
|
||||||
#storybook-root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: #f6f8fb;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import type { Preview } from "@storybook/react";
|
|
||||||
|
|
||||||
import "antd/dist/reset.css";
|
|
||||||
import "../src/react/video-player/tach-video-js.css";
|
|
||||||
import "./preview.css";
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
parameters: {
|
|
||||||
layout: "padded",
|
|
||||||
controls: {
|
|
||||||
expanded: true,
|
|
||||||
sort: "requiredFirst",
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
storySort: {
|
|
||||||
order: ["React", "Angular"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
8
.yarnrc.yml
Normal file
8
.yarnrc.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
enableGlobalCache: false
|
||||||
|
|
||||||
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
packageExtensions:
|
||||||
|
ng-zorro-antd@*:
|
||||||
|
peerDependencies:
|
||||||
|
rxjs: ">=7.0.0"
|
||||||
215
README.md
215
README.md
@@ -1,193 +1,60 @@
|
|||||||
# @tach/video-player
|
# hublib-web
|
||||||
|
|
||||||
Single package with three entrypoints:
|
Monorepo for shared UI packages distributed with Yarn workspaces.
|
||||||
|
|
||||||
- `@tach/video-player/core`
|
External projects install these packages directly from Git tags. Package `dist/` artifacts must be committed.
|
||||||
- `@tach/video-player/react`
|
|
||||||
- `@tach/video-player/angular`
|
|
||||||
|
|
||||||
This folder is prepared to be moved into a dedicated repository as-is.
|
## Structure
|
||||||
|
|
||||||
## What Is Included
|
- `packages/tach-typography` - typography tokens and adapters for React/Angular.
|
||||||
|
- `packages/video-player` - cross-framework video player runtime and adapters.
|
||||||
|
- `packages/content-suggestions` - content text/title with mention/tag/link parsing for React/Angular.
|
||||||
|
|
||||||
- Build pipeline to `dist/`:
|
## Package READMEs
|
||||||
- `npm run build`
|
|
||||||
- `npm run typecheck`
|
|
||||||
- Storybook playground for framework entrypoints:
|
|
||||||
- `npm run storybook`
|
|
||||||
- `npm run build-storybook`
|
|
||||||
- `dist`-first flow for git-based installation:
|
|
||||||
- `dist/` is committed to the repository.
|
|
||||||
- consumers do not depend on `prepare` at install time.
|
|
||||||
- Version bump scripts:
|
|
||||||
- `npm run release:patch`
|
|
||||||
- `npm run release:minor`
|
|
||||||
- `npm run release:major`
|
|
||||||
- Optional framework peers:
|
|
||||||
- React peers are optional if only `core/angular` is used.
|
|
||||||
- Angular peers are optional if only `core/react` is used.
|
|
||||||
|
|
||||||
## Repo Transfer Checklist
|
- [tach-typography](./packages/tach-typography/README.md)
|
||||||
|
- [video-player](./packages/video-player/README.md)
|
||||||
|
- [content-suggestions](./packages/content-suggestions/README.md)
|
||||||
|
|
||||||
1. Create a new repository (for example `tach/video-player`).
|
## Development
|
||||||
2. Copy everything from this folder to the new repo root.
|
|
||||||
3. In the new repo run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
corepack enable
|
||||||
npm run build
|
yarn set version 4.12.0
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
yarn test
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Commit and push (including `dist/`):
|
## Add a new package
|
||||||
|
|
||||||
|
1. Create a folder inside `packages/<name>`.
|
||||||
|
2. Add `package.json` with a unique package name.
|
||||||
|
3. Link internal dependencies with `workspace:*` (or `workspace:^`).
|
||||||
|
4. Run `yarn install` and then `yarn build`.
|
||||||
|
|
||||||
|
## Installation from Git (SSH)
|
||||||
|
|
||||||
|
This repository is consumed by tag-based Git dependencies.
|
||||||
|
|
||||||
|
- Full install guide: [docs/git-installation.md](./docs/git-installation.md)
|
||||||
|
- Release workflow: [docs/release-policy.md](./docs/release-policy.md)
|
||||||
|
|
||||||
|
Command template:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
yarn add "@hublib-web/<package>@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/<package>&tag=<package>-vX.Y.Z"
|
||||||
git commit -m "feat: initial @tach/video-player package"
|
|
||||||
git push origin main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Create a release tag:
|
## Releases
|
||||||
|
|
||||||
```bash
|
Releases are done with Git tags (no Changesets, no npm publish).
|
||||||
git tag v0.1.0
|
|
||||||
git push origin v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Build Output
|
Short flow:
|
||||||
|
|
||||||
`npm run build` compiles TypeScript into `dist/` and copies required assets (`css/scss/svg/...`).
|
1. Bump version in `packages/<package>/package.json`.
|
||||||
`dist/` must be committed before creating a new version tag.
|
2. Build package and commit updated `dist/` artifacts.
|
||||||
|
3. Create an annotated package tag: `<package>-vX.Y.Z`.
|
||||||
|
4. Push commit and tags to origin.
|
||||||
|
|
||||||
Entrypoints are exported from `dist`:
|
Detailed policy and examples: [docs/release-policy.md](./docs/release-policy.md).
|
||||||
|
|
||||||
- `@tach/video-player/core`
|
|
||||||
- `@tach/video-player/react`
|
|
||||||
- `@tach/video-player/angular`
|
|
||||||
|
|
||||||
## Storybook
|
|
||||||
|
|
||||||
Local Storybook exposes two pages for development and prop testing:
|
|
||||||
|
|
||||||
- `React/VideoPlayer` - React component entrypoint with controls for player props.
|
|
||||||
- `Angular/VideoPlayerAdapter` - adapter entrypoint with controls for runtime options (`attach/update`).
|
|
||||||
|
|
||||||
Run Storybook:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run storybook
|
|
||||||
```
|
|
||||||
|
|
||||||
Build static Storybook:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build-storybook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation From Git (Without npm Registry)
|
|
||||||
|
|
||||||
### npm
|
|
||||||
|
|
||||||
By tag:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i git+ssh://git@github.com/<org>/video-player.git#v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
By commit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i git+ssh://git@github.com/<org>/video-player.git#<commit-sha>
|
|
||||||
```
|
|
||||||
|
|
||||||
By semver tag range:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i github:<org>/video-player#semver:^0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### pnpm
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm add git+ssh://git@github.com/<org>/video-player.git#v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### yarn
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn add git+ssh://git@github.com/<org>/video-player.git#v0.1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Versioning Workflow
|
|
||||||
|
|
||||||
1. Build and bump version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run release:patch
|
|
||||||
# or release:minor / release:major
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Commit generated `dist` changes and `package.json` version bump:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "chore(release): vX.Y.Z"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Push commit and tags:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git push origin main --follow-tags
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Consumers update git tag/version in their `package.json`.
|
|
||||||
|
|
||||||
## Why Optional Peers
|
|
||||||
|
|
||||||
Consumers install only the framework they need:
|
|
||||||
|
|
||||||
- React app: install `react` and `react-dom`, use `@tach/video-player/react`.
|
|
||||||
- Angular app: install `@angular/*`, use `@tach/video-player/angular`.
|
|
||||||
- Shared utilities only: use `@tach/video-player/core`.
|
|
||||||
|
|
||||||
## Entrypoints
|
|
||||||
|
|
||||||
### Core
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import {
|
|
||||||
isHlsSource,
|
|
||||||
selectPlaybackEngine,
|
|
||||||
VideoPlayerRuntime,
|
|
||||||
setVideoPlayerTokenProvider,
|
|
||||||
} from "@tach/video-player/core";
|
|
||||||
|
|
||||||
setVideoPlayerTokenProvider(async () => {
|
|
||||||
// Provide host-app token retrieval here.
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const runtime = new VideoPlayerRuntime();
|
|
||||||
await runtime.init({
|
|
||||||
container: document.getElementById("player")!,
|
|
||||||
source: { src: "https://example.com/video.m3u8" },
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### React
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import VideoPlayer from "@tach/video-player/react";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Angular
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { AngularVideoPlayerAdapter } from "@tach/video-player/angular";
|
|
||||||
|
|
||||||
const adapter = new AngularVideoPlayerAdapter();
|
|
||||||
await adapter.attach(containerElement, {
|
|
||||||
source: { src: "https://example.com/video.m3u8" },
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
`AngularVideoPlayerAdapter` is intentionally framework-light: it wraps `VideoPlayerRuntime` (`attach/update/destroy/on`) and does not depend on React code.
|
|
||||||
|
|||||||
11
dist/core/format-time.js
vendored
11
dist/core/format-time.js
vendored
@@ -1,11 +0,0 @@
|
|||||||
export const formatTime = (seconds) => {
|
|
||||||
const pad = (num) => String(num).padStart(2, "0");
|
|
||||||
const hrs = Math.floor(seconds / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
if (seconds < 3600) {
|
|
||||||
return `${pad(mins)}:${pad(secs)}`;
|
|
||||||
}
|
|
||||||
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=format-time.js.map
|
|
||||||
1
dist/core/format-time.js.map
vendored
1
dist/core/format-time.js.map
vendored
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"format-time.js","sourceRoot":"","sources":["../../src/core/format-time.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE;IAC7C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IAEtC,IAAI,OAAO,GAAG,IAAI,EAAE,CAAC;QACpB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IACpC,CAAC;IAED,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AAChD,CAAC,CAAC"}
|
|
||||||
1
dist/core/player-runtime.js.map
vendored
1
dist/core/player-runtime.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Player from "video.js/dist/types/player";
|
|
||||||
interface VideoQRScannerPluginProps {
|
|
||||||
player: Player | null;
|
|
||||||
enabled?: boolean;
|
|
||||||
/**
|
|
||||||
* Интервал между запусками сканирования в миллисекундах.
|
|
||||||
* По умолчанию 200 мс.
|
|
||||||
*/
|
|
||||||
scanInterval?: number;
|
|
||||||
/**
|
|
||||||
* Масштаб для сканирования (0 < scanningScale <= 1).
|
|
||||||
* Значение меньше 1 уменьшает разрешение для ускорения обработки.
|
|
||||||
* По умолчанию 1.
|
|
||||||
*/
|
|
||||||
scanningScale?: number;
|
|
||||||
/**
|
|
||||||
* Количество неудачных попыток обнаружения QR-кода до снятия маски.
|
|
||||||
* По умолчанию 2.a
|
|
||||||
*/
|
|
||||||
maxFailedAttempts?: number;
|
|
||||||
}
|
|
||||||
declare const VideoQRScannerPlugin: React.FC<VideoQRScannerPluginProps>;
|
|
||||||
export default VideoQRScannerPlugin;
|
|
||||||
//# sourceMappingURL=index.d.ts.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/qr-scanner/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAIhD,UAAU,yBAAyB;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqR7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import { Popover } from "antd";
|
|
||||||
import QrScanner from "qr-scanner";
|
|
||||||
import { CloseOutlined } from "@ant-design/icons";
|
|
||||||
const VideoQRScannerPlugin = ({ player, enabled = true, scanInterval = 200, scanningScale = 1, maxFailedAttempts = 2, }) => {
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
const [detectedQr, setDetectedQr] = useState(null);
|
|
||||||
const animationFrameRef = useRef(null);
|
|
||||||
const ignoredQRCodes = useRef(new Set());
|
|
||||||
const [playerEl, setPlayerEl] = useState(null);
|
|
||||||
const lastScanTimeRef = useRef(0);
|
|
||||||
const isScanningRef = useRef(false);
|
|
||||||
// Счётчик неудачных попыток обнаружения QR, если код уже отображается
|
|
||||||
const failedAttemptsRef = useRef(0);
|
|
||||||
// Для получения актуального значения detectedQr внутри асинхронного колбэка
|
|
||||||
const detectedQrRef = useRef(null);
|
|
||||||
useEffect(() => {
|
|
||||||
detectedQrRef.current = detectedQr;
|
|
||||||
}, [detectedQr]);
|
|
||||||
// Изначально устанавливаем контейнер для портала как элемент плеера
|
|
||||||
useEffect(() => {
|
|
||||||
if (player) {
|
|
||||||
setPlayerEl(player.el());
|
|
||||||
}
|
|
||||||
}, [player]);
|
|
||||||
// Обновление контейнера при переходе в полноэкранный режим
|
|
||||||
useEffect(() => {
|
|
||||||
const handleFullscreenChange = () => {
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
setPlayerEl(document.fullscreenElement);
|
|
||||||
}
|
|
||||||
else if (player) {
|
|
||||||
setPlayerEl(player.el());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
||||||
};
|
|
||||||
}, [player]);
|
|
||||||
const scanFrame = useCallback(async () => {
|
|
||||||
if (!player || player.paused() || !canvasRef.current || !enabled)
|
|
||||||
return;
|
|
||||||
const now = performance.now();
|
|
||||||
if (now - lastScanTimeRef.current < scanInterval) {
|
|
||||||
animationFrameRef.current = requestAnimationFrame(scanFrame);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastScanTimeRef.current = now;
|
|
||||||
// Предотвращаем параллельное выполнение сканирования
|
|
||||||
if (isScanningRef.current) {
|
|
||||||
animationFrameRef.current = requestAnimationFrame(scanFrame);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isScanningRef.current = true;
|
|
||||||
const videoEl = player.tech(true).el();
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) {
|
|
||||||
isScanningRef.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Получаем оригинальные размеры видео
|
|
||||||
const originalVideoWidth = videoEl.videoWidth;
|
|
||||||
const originalVideoHeight = videoEl.videoHeight;
|
|
||||||
// Устанавливаем canvas с пониженным разрешением для сканирования
|
|
||||||
canvas.width = originalVideoWidth * scanningScale;
|
|
||||||
canvas.height = originalVideoHeight * scanningScale;
|
|
||||||
// Рисуем видео в canvas с пониженным разрешением
|
|
||||||
ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
|
|
||||||
try {
|
|
||||||
const result = await QrScanner.scanImage(canvas, {
|
|
||||||
returnDetailedScanResult: true,
|
|
||||||
});
|
|
||||||
if (result &&
|
|
||||||
result.data &&
|
|
||||||
result.cornerPoints?.length === 4 &&
|
|
||||||
!ignoredQRCodes.current.has(result.data)) {
|
|
||||||
// Сброс неудачных попыток при успешном обнаружении
|
|
||||||
failedAttemptsRef.current = 0;
|
|
||||||
// Преобразуем координаты из масштабированного canvas в координаты оригинального видео
|
|
||||||
const points = result.cornerPoints.map(p => ({
|
|
||||||
x: p.x / scanningScale,
|
|
||||||
y: p.y / scanningScale,
|
|
||||||
}));
|
|
||||||
const minX = Math.min(...points.map(p => p.x));
|
|
||||||
const minY = Math.min(...points.map(p => p.y));
|
|
||||||
const maxX = Math.max(...points.map(p => p.x));
|
|
||||||
const maxY = Math.max(...points.map(p => p.y));
|
|
||||||
// Получаем размеры отображаемого видео
|
|
||||||
const rect = videoEl.getBoundingClientRect();
|
|
||||||
const displayScale = Math.min(rect.width / originalVideoWidth, rect.height / originalVideoHeight);
|
|
||||||
const offsetX = (rect.width - originalVideoWidth * displayScale) / 2;
|
|
||||||
const offsetY = (rect.height - originalVideoHeight * displayScale) / 2;
|
|
||||||
const qrWidth = (maxX - minX) * displayScale;
|
|
||||||
const qrHeight = (maxY - minY) * displayScale;
|
|
||||||
const padding = 8;
|
|
||||||
const x = minX * displayScale + offsetX - padding;
|
|
||||||
const y = minY * displayScale + offsetY - padding;
|
|
||||||
setDetectedQr({
|
|
||||||
data: result.data,
|
|
||||||
position: new DOMRect(x, y, qrWidth + padding * 2, qrHeight + padding * 2),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Если код не найден, и он уже отображался, даем maxFailedAttempts попыток
|
|
||||||
if (detectedQrRef.current) {
|
|
||||||
failedAttemptsRef.current += 1;
|
|
||||||
if (failedAttemptsRef.current >= maxFailedAttempts) {
|
|
||||||
setDetectedQr(null);
|
|
||||||
failedAttemptsRef.current = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
// В случае ошибки аналогичная логика: если код уже отображался — даем maxFailedAttempts попыток
|
|
||||||
if (detectedQrRef.current) {
|
|
||||||
failedAttemptsRef.current += 1;
|
|
||||||
if (failedAttemptsRef.current >= maxFailedAttempts) {
|
|
||||||
setDetectedQr(null);
|
|
||||||
failedAttemptsRef.current = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
isScanningRef.current = false;
|
|
||||||
}
|
|
||||||
animationFrameRef.current = requestAnimationFrame(scanFrame);
|
|
||||||
}, [player, enabled, scanInterval, scanningScale, maxFailedAttempts]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!player || !enabled)
|
|
||||||
return;
|
|
||||||
const startScanning = () => {
|
|
||||||
if (animationFrameRef.current)
|
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
|
||||||
animationFrameRef.current = requestAnimationFrame(scanFrame);
|
|
||||||
};
|
|
||||||
const stopScanning = () => {
|
|
||||||
if (animationFrameRef.current) {
|
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
|
||||||
animationFrameRef.current = null;
|
|
||||||
}
|
|
||||||
setDetectedQr(null);
|
|
||||||
};
|
|
||||||
player.on("play", startScanning);
|
|
||||||
player.on("pause", stopScanning);
|
|
||||||
player.on("ended", stopScanning);
|
|
||||||
window.addEventListener("resize", startScanning);
|
|
||||||
return () => {
|
|
||||||
stopScanning();
|
|
||||||
player.off("play", startScanning);
|
|
||||||
player.off("pause", stopScanning);
|
|
||||||
player.off("ended", stopScanning);
|
|
||||||
window.removeEventListener("resize", startScanning);
|
|
||||||
};
|
|
||||||
}, [player, enabled, scanFrame]);
|
|
||||||
const ignoreQRCode = () => {
|
|
||||||
if (detectedQr) {
|
|
||||||
ignoredQRCodes.current.add(detectedQr.data);
|
|
||||||
setDetectedQr(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (_jsxs(_Fragment, { children: [_jsx("canvas", { ref: canvasRef, style: { display: "none" } }), detectedQr &&
|
|
||||||
playerEl &&
|
|
||||||
createPortal(_jsx(Popover, { getPopupContainer: () => playerEl, content: _jsx("a", { href: detectedQr.data, target: "_blank", rel: "noreferrer", referrerPolicy: "no-referrer", style: { color: "#1677ff" }, children: detectedQr.data }), placement: "top", children: _jsxs("div", { style: {
|
|
||||||
position: "absolute",
|
|
||||||
left: detectedQr.position.x,
|
|
||||||
top: detectedQr.position.y,
|
|
||||||
width: detectedQr.position.width,
|
|
||||||
height: detectedQr.position.height,
|
|
||||||
pointerEvents: "auto",
|
|
||||||
cursor: "pointer",
|
|
||||||
zIndex: 10,
|
|
||||||
}, children: [["top-left", "top-right", "bottom-left", "bottom-right"].map(corner => (_jsx("div", { style: {
|
|
||||||
position: "absolute",
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
border: "3px solid var(--Accent-Primary)",
|
|
||||||
borderRadius: 4,
|
|
||||||
...(corner.includes("top") ? { top: 0 } : { bottom: 0 }),
|
|
||||||
...(corner.includes("left") ? { left: 0 } : { right: 0 }),
|
|
||||||
borderTop: corner.includes("bottom")
|
|
||||||
? "none"
|
|
||||||
: "3px solid var(--Accent-Primary)",
|
|
||||||
borderBottom: corner.includes("top")
|
|
||||||
? "none"
|
|
||||||
: "3px solid var(--Accent-Primary)",
|
|
||||||
borderLeft: corner.includes("right")
|
|
||||||
? "none"
|
|
||||||
: "3px solid var(--Accent-Primary)",
|
|
||||||
borderRight: corner.includes("left")
|
|
||||||
? "none"
|
|
||||||
: "3px solid var(--Accent-Primary)",
|
|
||||||
} }, corner))), _jsx(CloseOutlined, { style: {
|
|
||||||
color: "#ff4d4f",
|
|
||||||
position: "absolute",
|
|
||||||
top: "-8px",
|
|
||||||
right: "-8px",
|
|
||||||
background: "white",
|
|
||||||
borderRadius: "50%",
|
|
||||||
padding: "2px",
|
|
||||||
cursor: "pointer",
|
|
||||||
}, onClick: ignoreQRCode })] }) }), playerEl)] }));
|
|
||||||
};
|
|
||||||
export default VideoQRScannerPlugin;
|
|
||||||
//# sourceMappingURL=index.js.map
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/react/video-player/components/video-js/plugins/settings/index.ts"],"names":[],"mappings":"AACA,OAAO,SAAS,MAAM,+BAA+B,CAAC;AAEtD,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAYhD,OAAO,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,UAAU,qBAAqB;CAE9B;AAGD,UAAU,oBAAqB,SAAQ,aAAa;IACnD,UAAU,EAAE,SAAS,CAAC;CACtB;AAED,QAAA,MAAM,UAAU,EAAmC,OAAO,MAAM,CAAC;AAKjE,cAAM,cAAe,SAAQ,UAAU;IACtC,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,QAAQ,CAA6B;IAC7C,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,cAAc,CAAa;IAGnC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,gBAAgB,CAA6B;gBAEzC,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,qBAAqB;IAMxE;;OAEG;IACH,OAAO,CAAC,UAAU;IAKlB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA8D5B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAInB;;;;;;OAMG;YACW,OAAO;CA8CrB;AAID,eAAe,cAAc,CAAC"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/react/video-player/components/video-js/plugins/settings/selectors/text-track-selector/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAS3D,UAAU,kBAAmB,SAAQ,yBAAyB;CAAG;AAEjE,QAAA,MAAM,UAAU,GACf,QAAQ,aAAa,EACrB,UAAS,kBAA6B;;;;;;;;;;;;;;;CAgDtC,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../../../../src/react/video-player/components/video-js/plugins/settings/selectors/text-track-selector/index.ts"],"names":[],"mappings":"AAGA,MAAM,QAAQ,GAAG;IAChB,KAAK,EAAE,UAAU;IACjB,QAAQ,EAAE,KAAK;IACf,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,eAAe;CAC1B,CAAC;AAIF,MAAM,UAAU,GAAG,CAClB,MAAqB,EACrB,UAA8B,QAAQ,EACrC,EAAE;IACH,MAAM,SAAS,GAAG,GAAG,EAAE;QACtB,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC;QAC/C,MAAM,UAAU,GAAG,EAAE,CAAC;QAEtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAC7C,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,SAAS;YACV,CAAC;YAED,MAAM,SAAS,GAAG;gBACjB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK;gBAC1B,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK;gBAC1B,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS;gBAC1C,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;aACtC,CAAC;YAEF,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,UAAU,CAAC;IACnB,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,KAAgB,EAAE,EAAE;QACrC,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC;QAE/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YAC7C,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACrC,SAAS;YACV,CAAC;YAED,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,KAAK;gBAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,SAAS,CAAC;;gBACnE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,UAAU,CAAC;QACtC,CAAC;IACF,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,GAAG,EAAE;QACrB,OAAO;YACN,GAAG,OAAO;YACV,OAAO,EAAE,SAAS,EAAE,CAAC,MAAM,GAAG,CAAC;SAC/B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACN,SAAS;QACT,QAAQ;KACR,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import Player from "video.js/dist/types/player";
|
|
||||||
import { SkipButtonsOptions } from "./plugins/skip-buttons";
|
|
||||||
export interface IVideoJSSource {
|
|
||||||
src: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
export type PreloadType = "auto" | "metadata" | "none" | "visibility";
|
|
||||||
export interface IVideoJSOptions {
|
|
||||||
autoplay: boolean;
|
|
||||||
controls: boolean;
|
|
||||||
responsive: boolean;
|
|
||||||
aspectRatio?: string;
|
|
||||||
preload: PreloadType;
|
|
||||||
fluid: boolean;
|
|
||||||
muted: boolean;
|
|
||||||
sources: IVideoJSSource[];
|
|
||||||
poster?: string;
|
|
||||||
preferHQ?: boolean;
|
|
||||||
/** Включить детальное логирование */
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
export interface IVideoJSProps {
|
|
||||||
options: IVideoJSOptions;
|
|
||||||
onReady?: (player: VideoJsPlayer) => void;
|
|
||||||
className?: string;
|
|
||||||
classNames?: string[];
|
|
||||||
initialTime?: number;
|
|
||||||
full?: boolean;
|
|
||||||
withRewind?: boolean;
|
|
||||||
}
|
|
||||||
export interface VideoJsLiveTracker {
|
|
||||||
isLive_: boolean;
|
|
||||||
atLiveEdge?: () => boolean;
|
|
||||||
startTracking: () => void;
|
|
||||||
trigger: (event: string) => void;
|
|
||||||
}
|
|
||||||
export type VideoJsPlayer = Player & {
|
|
||||||
liveTracker?: VideoJsLiveTracker;
|
|
||||||
settingsMenu?: () => void;
|
|
||||||
mobileUi?: () => void;
|
|
||||||
bigPlayPauseButton?: () => void;
|
|
||||||
skipButtons?: (options: SkipButtonsOptions) => void;
|
|
||||||
subscribeToSegmentChange: (callback: (segment: any) => void) => void;
|
|
||||||
subscribeToDuration: (callback: (duration: number) => void) => void;
|
|
||||||
subscribeToPlayStart: (callback: () => void) => void;
|
|
||||||
subscribeToPlayStarted: (callback: () => void) => void;
|
|
||||||
subscribeToManifestLoaded: (callback: () => void) => void;
|
|
||||||
mediaduration: () => number | undefined;
|
|
||||||
textTracks: () => TextTrack[];
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=types.d.ts.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/video-js/types.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAEhD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,YAAY,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qCAAqC;IACrC,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AAGD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACpD,wBAAwB,EAAE,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,GAAG,KAAK,IAAI,KAAK,IAAI,CAAC;IACrE,mBAAmB,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;IACpE,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACrD,sBAAsB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IACvD,yBAAyB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;IAC1D,aAAa,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACxC,UAAU,EAAE,MAAM,SAAS,EAAE,CAAC;CAC9B,CAAC"}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export const formatTime = (seconds) => {
|
|
||||||
const pad = (num) => String(num).padStart(2, "0");
|
|
||||||
const hrs = Math.floor(seconds / 3600);
|
|
||||||
const mins = Math.floor((seconds % 3600) / 60);
|
|
||||||
const secs = Math.floor(seconds % 60);
|
|
||||||
if (seconds < 3600) {
|
|
||||||
return `${pad(mins)}:${pad(secs)}`;
|
|
||||||
}
|
|
||||||
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=utils.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../../../src/react/video-player/components/video-js/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE;IAC7C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACtC,IAAI,OAAO,GAAG,IAAI,EAAE,CAAC;QACpB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AAChD,CAAC,CAAC"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-errors/index.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAMrD,OAAO,UAAU,MAAM,eAAe,CAAC;AACvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAO/C,MAAM,UAAU,GAAG,CAAC,EACnB,UAAU,GAAG,KAAK,EAClB,QAAQ,EACR,GAAG,KAAK,EACqB,EAAE,EAAE;IAEjC,OAAO,CACN,eACC,SAAS,EAAE,MAAM,CAAC,SAAS,EAC3B,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,aAEhD,UAAU,IAAI,KAAC,UAAU,IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,GAAI,EACtD,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,IACtC,CACN,CAAC;AACH,CAAC,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-mouse-events/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAIjC,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAEhD,OAAO,EAAE,aAAa,EAAiB,MAAM,mBAAmB,CAAC;AAGjE,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC3D,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC1C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,GAAI,sEAO7B,qBAAqB,4CAkEvB,CAAC"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/react/video-player/components/with-observation/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAKjC,OAAO,MAAM,MAAM,4BAA4B,CAAC;AAGhD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,MAAM,WAAW,qBAAsB,SAAQ,qBAAqB;IACnE,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzC,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB;AAED,eAAO,MAAM,eAAe,GAAI,iDAM7B,qBAAqB,4CAsDvB,CAAC"}
|
|
||||||
43
docs/git-installation.md
Normal file
43
docs/git-installation.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Git installation (SSH + tags)
|
||||||
|
|
||||||
|
This repository is consumed directly from Git tags, not from npm.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- You have SSH access to the repository (`git@github.com:ORG/REPO.git`).
|
||||||
|
- Release tags already exist in remote.
|
||||||
|
- `dist/` is committed in the tagged commit.
|
||||||
|
|
||||||
|
## Tag naming
|
||||||
|
|
||||||
|
Use package-specific tags:
|
||||||
|
|
||||||
|
- `tach-typography-vX.Y.Z`
|
||||||
|
- `video-player-vX.Y.Z`
|
||||||
|
- `content-suggestions-vX.Y.Z`
|
||||||
|
|
||||||
|
## Yarn template
|
||||||
|
|
||||||
|
Always wrap the dependency string in quotes because of `&tag=`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add "@hublib-web/<package>@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/<package>&tag=<package>-vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add "@hublib-web/tach-typography@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/tach-typography&tag=tach-typography-v0.1.0"
|
||||||
|
yarn add "@hublib-web/video-player@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/video-player&tag=video-player-v0.1.0"
|
||||||
|
yarn add "@hublib-web/content-suggestions@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/content-suggestions&tag=content-suggestions-v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update package version in a consumer app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn up "@hublib-web/video-player@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/video-player&tag=video-player-v0.2.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- Release policy: [release-policy.md](./release-policy.md)
|
||||||
54
docs/release-policy.md
Normal file
54
docs/release-policy.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Release policy
|
||||||
|
|
||||||
|
This monorepo is released through Git tags. We do not use Changesets or npm publish.
|
||||||
|
|
||||||
|
## Principles
|
||||||
|
|
||||||
|
- Every release points to a commit where the package `dist/` is already built and committed.
|
||||||
|
- Tags are package-specific, so each package can be released independently.
|
||||||
|
- Consumers install by tag using Git SSH URLs.
|
||||||
|
|
||||||
|
## Tag format
|
||||||
|
|
||||||
|
- `tach-typography-vX.Y.Z`
|
||||||
|
- `video-player-vX.Y.Z`
|
||||||
|
- `content-suggestions-vX.Y.Z`
|
||||||
|
|
||||||
|
`X.Y.Z` must match the version in the package's `package.json`.
|
||||||
|
|
||||||
|
## Release checklist (single package)
|
||||||
|
|
||||||
|
1. Update package version in `packages/<package>/package.json`.
|
||||||
|
2. Build package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn workspace @hublib-web/<package> build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Verify that `dist/` and `package.json` contain the release changes.
|
||||||
|
4. Commit release files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/<package>/package.json packages/<package>/dist
|
||||||
|
git commit -m "release(<package>): vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create annotated tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a <package>-vX.Y.Z -m "@hublib-web/<package> vX.Y.Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Push commit and tags:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main --follow-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-package release
|
||||||
|
|
||||||
|
If one commit releases several packages, create one tag per package on the same commit.
|
||||||
|
|
||||||
|
## Consumer installation
|
||||||
|
|
||||||
|
Use the package-specific tag in dependency spec. See [git-installation.md](./git-installation.md).
|
||||||
21
eslint.config.mjs
Normal file
21
eslint.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ["**/dist/**", "**/node_modules/**"],
|
||||||
|
},
|
||||||
|
];
|
||||||
5994
package-lock.json
generated
5994
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@@ -1,88 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "@tach/video-player",
|
"name": "@hublib-web/root",
|
||||||
"version": "0.0.1",
|
"private": true,
|
||||||
"type": "module",
|
"packageManager": "yarn@4.12.0",
|
||||||
"main": "./dist/react/index.js",
|
"workspaces": [
|
||||||
"types": "./dist/react/index.d.ts",
|
"packages/*"
|
||||||
"sideEffects": true,
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"README.md"
|
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ./scripts/build.mjs",
|
"build": "yarn workspaces foreach -A -p --topological-dev run build",
|
||||||
"typecheck": "tsc -p ./tsconfig.build.json --noEmit",
|
"typecheck": "yarn workspaces foreach -A -p --topological-dev run typecheck",
|
||||||
"storybook": "storybook dev -p 6006",
|
"test": "yarn workspaces foreach -A -p --topological-dev run test",
|
||||||
"build-storybook": "storybook build",
|
"clean": "yarn workspaces foreach -A -p --topological-dev run clean",
|
||||||
"prepack": "npm run build",
|
"lint": "yarn workspaces foreach -A -p --topological-dev run lint"
|
||||||
"release:patch": "npm run build && npm version patch",
|
|
||||||
"release:minor": "npm run build && npm version minor",
|
|
||||||
"release:major": "npm run build && npm version major"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
"./package.json": "./package.json",
|
|
||||||
"./core": {
|
|
||||||
"types": "./dist/core/index.d.ts",
|
|
||||||
"import": "./dist/core/index.js",
|
|
||||||
"default": "./dist/core/index.js"
|
|
||||||
},
|
|
||||||
"./react": {
|
|
||||||
"types": "./dist/react/index.d.ts",
|
|
||||||
"import": "./dist/react/index.js",
|
|
||||||
"default": "./dist/react/index.js"
|
|
||||||
},
|
|
||||||
"./angular": {
|
|
||||||
"types": "./dist/angular/index.d.ts",
|
|
||||||
"import": "./dist/angular/index.js",
|
|
||||||
"default": "./dist/angular/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@ant-design/icons": "5.3.7",
|
|
||||||
"antd": "5.14.2",
|
|
||||||
"classnames": "2.5.1",
|
|
||||||
"hls.js": "1.6.14",
|
|
||||||
"lodash": "4.17.21",
|
|
||||||
"qr-scanner": "1.4.2",
|
|
||||||
"react-device-detect": "2.2.3",
|
|
||||||
"video.js": "8.23.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
||||||
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
||||||
"@angular/forms": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
|
||||||
"react": "^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@angular/common": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@angular/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@angular/forms": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.37.0",
|
||||||
"@storybook/addon-essentials": "8.6.14",
|
"@storybook/addon-essentials": "8.6.14",
|
||||||
"@storybook/react": "8.6.14",
|
|
||||||
"@storybook/react-vite": "8.6.14",
|
"@storybook/react-vite": "8.6.14",
|
||||||
"@types/node": "22.12.0",
|
"@types/node": "^24.6.1",
|
||||||
"@types/react": "19.0.2",
|
"eslint": "^9.37.0",
|
||||||
"@types/react-dom": "19.0.2",
|
"prettier": "^3.6.2",
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"sass": "1.83.4",
|
|
||||||
"storybook": "8.6.14",
|
"storybook": "8.6.14",
|
||||||
"typescript": "5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "5.4.14"
|
"typescript-eslint": "^8.57.0",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
packages/content-suggestions/.storybook/main.ts
Normal file
18
packages/content-suggestions/.storybook/main.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { StorybookConfig } from "@storybook/react-vite";
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||||
|
addons: ["@storybook/addon-essentials"],
|
||||||
|
framework: {
|
||||||
|
name: "@storybook/react-vite",
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: "tag",
|
||||||
|
},
|
||||||
|
core: {
|
||||||
|
disableTelemetry: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
19
packages/content-suggestions/.storybook/preview.ts
Normal file
19
packages/content-suggestions/.storybook/preview.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Preview } from "@storybook/react";
|
||||||
|
|
||||||
|
import "antd/dist/reset.css";
|
||||||
|
import "@hublib-web/tach-typography/styles.css";
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
layout: "padded",
|
||||||
|
controls: {
|
||||||
|
expanded: true,
|
||||||
|
sort: "requiredFirst",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
argTypesRegex: "^on[A-Z].*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
138
packages/content-suggestions/README.md
Normal file
138
packages/content-suggestions/README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# @hublib-web/content-suggestions
|
||||||
|
|
||||||
|
Cross-framework content text/title renderer with support for mentions, tags and links.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Shared parser in `core` (`findAllEntities`, `findMentions`, `findTags`, `findLinks`).
|
||||||
|
- React UI components in `react`:
|
||||||
|
- `ContentText`
|
||||||
|
- `ContentTextWithSuggestions`
|
||||||
|
- `ContentTitleWithSuggestions`
|
||||||
|
- Angular UI components in `angular` (based on `@hublib-web/tach-typography/angular`):
|
||||||
|
- `ContentTextComponent`
|
||||||
|
- `ContentTextWithSuggestionsComponent`
|
||||||
|
- `ContentTitleWithSuggestionsComponent`
|
||||||
|
- Depends on `@hublib-web/tach-typography` for visual consistency.
|
||||||
|
- Business logic (API requests for mentions/tags) stays in consumer application.
|
||||||
|
|
||||||
|
## Install from Git (SSH tag)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add "@hublib-web/content-suggestions@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/content-suggestions&tag=content-suggestions-v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
`@hublib-web/tach-typography@0.3.0` is a peer dependency.
|
||||||
|
|
||||||
|
## Install inside this monorepo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @hublib-web/content-suggestions
|
||||||
|
```
|
||||||
|
|
||||||
|
## Release this package
|
||||||
|
|
||||||
|
1. Bump `version` in `packages/content-suggestions/package.json`.
|
||||||
|
2. Build package artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn workspace @hublib-web/content-suggestions build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Commit release files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add packages/content-suggestions/package.json packages/content-suggestions/dist
|
||||||
|
git commit -m "release(content-suggestions): v0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Create and push tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a content-suggestions-v0.1.0 -m "@hublib-web/content-suggestions v0.1.0"
|
||||||
|
git push origin main --follow-tags
|
||||||
|
```
|
||||||
|
|
||||||
|
Detailed docs:
|
||||||
|
|
||||||
|
- [Release policy](../../docs/release-policy.md)
|
||||||
|
- [Git installation](../../docs/git-installation.md)
|
||||||
|
|
||||||
|
## React usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ContentTextWithSuggestions } from "@hublib-web/content-suggestions/react";
|
||||||
|
|
||||||
|
<ContentTextWithSuggestions text={text} />;
|
||||||
|
```
|
||||||
|
|
||||||
|
With app-specific mention business logic:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ContentTextWithSuggestions
|
||||||
|
text={text}
|
||||||
|
renderMention={(entity) => <MyMention entity={entity} />}
|
||||||
|
renderTag={(entity) => <MyTagLink entity={entity} />}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, tags are rendered as plain styled text (not links).
|
||||||
|
|
||||||
|
## Angular usage
|
||||||
|
|
||||||
|
Standalone component usage:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Component } from "@angular/core";
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "@hublib-web/content-suggestions/angular";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextWithSuggestionsComponent],
|
||||||
|
template: `
|
||||||
|
<content-text-with-suggestions
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="{ count: 180, expandable: true }"
|
||||||
|
[onView]="onView"
|
||||||
|
/>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class FeedPostComponent {
|
||||||
|
text = "@[John Doe](d290f1ee-6c54-4b01-90e6-d701748f0851) hello #tag";
|
||||||
|
|
||||||
|
onView = () => {
|
||||||
|
// analytics
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Title variant with defaults (`weight: "bold"`, `ellipsis: { rows: 2 }`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ContentTitleWithSuggestionsComponent } from "@hublib-web/content-suggestions/angular";
|
||||||
|
|
||||||
|
// template
|
||||||
|
// <content-title-with-suggestions [text]="title" />
|
||||||
|
```
|
||||||
|
|
||||||
|
Tokenization helper:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createAngularContentTokens } from "@hublib-web/content-suggestions/angular";
|
||||||
|
|
||||||
|
const tokens = createAngularContentTokens(text);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storybook (dev/design system)
|
||||||
|
|
||||||
|
Run from repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn workspace @hublib-web/content-suggestions storybook
|
||||||
|
```
|
||||||
|
|
||||||
|
Build static Storybook:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn workspace @hublib-web/content-suggestions storybook:build
|
||||||
|
```
|
||||||
21
packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.d.ts
vendored
Normal file
21
packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { AngularContentEllipsisConfig, AngularLinkRenderer, AngularMentionRenderer, AngularTagRenderer } from "./types";
|
||||||
|
import { EventEmitter } from "@angular/core";
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export declare class ContentTextWithSuggestionsComponent {
|
||||||
|
className: string | undefined;
|
||||||
|
weight: TypographyWeight;
|
||||||
|
text: string | null | undefined;
|
||||||
|
ellipsis: AngularContentEllipsisConfig;
|
||||||
|
blur: boolean;
|
||||||
|
style: Record<string, string | number> | null | undefined;
|
||||||
|
onView: (() => void) | undefined;
|
||||||
|
renderMention: AngularMentionRenderer | undefined;
|
||||||
|
renderTag: AngularTagRenderer | undefined;
|
||||||
|
renderLink: AngularLinkRenderer | undefined;
|
||||||
|
readonly viewed: EventEmitter<void>;
|
||||||
|
readonly expandedChange: EventEmitter<boolean>;
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<ContentTextWithSuggestionsComponent, never>;
|
||||||
|
static ɵcmp: i0.ɵɵComponentDeclaration<ContentTextWithSuggestionsComponent, "content-text-with-suggestions", never, { "className": { "alias": "className"; "required": false; }; "weight": { "alias": "weight"; "required": false; }; "text": { "alias": "text"; "required": false; }; "ellipsis": { "alias": "ellipsis"; "required": false; }; "blur": { "alias": "blur"; "required": false; }; "style": { "alias": "style"; "required": false; }; "onView": { "alias": "onView"; "required": false; }; "renderMention": { "alias": "renderMention"; "required": false; }; "renderTag": { "alias": "renderTag"; "required": false; }; "renderLink": { "alias": "renderLink"; "required": false; }; }, { "viewed": "viewed"; "expandedChange": "expandedChange"; }, never, never, true, never>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=content-text-with-suggestions.component.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"content-text-with-suggestions.component.d.ts","sourceRoot":"","sources":["../../../src/angular/content-text-with-suggestions.component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAsC,YAAY,EAAiB,MAAM,eAAe,CAAC;AAEhG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;;AAIzE,qBAsBa,mCAAmC;IACrC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,gBAAgB,CAAY;IACpC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,4BAA4B,CAAS;IAC/C,IAAI,UAAS;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAClD,SAAS,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE3C,QAAQ,CAAC,MAAM,qBAA4B;IAC3C,QAAQ,CAAC,cAAc,wBAA+B;yCAbrD,mCAAmC;2CAAnC,mCAAmC;CAc/C"}
|
||||||
84
packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js
vendored
Normal file
84
packages/content-suggestions/dist/angular/angular/content-text-with-suggestions.component.js
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { ContentTextComponent } from "./content-text.component";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export class ContentTextWithSuggestionsComponent {
|
||||||
|
className;
|
||||||
|
weight = "normal";
|
||||||
|
text;
|
||||||
|
ellipsis = false;
|
||||||
|
blur = false;
|
||||||
|
style;
|
||||||
|
onView;
|
||||||
|
renderMention;
|
||||||
|
renderTag;
|
||||||
|
renderLink;
|
||||||
|
viewed = new EventEmitter();
|
||||||
|
expandedChange = new EventEmitter();
|
||||||
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextWithSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||||
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: ContentTextWithSuggestionsComponent, isStandalone: true, selector: "content-text-with-suggestions", inputs: { className: "className", weight: "weight", text: "text", ellipsis: "ellipsis", blur: "blur", style: "style", onView: "onView", renderMention: "renderMention", renderTag: "renderTag", renderLink: "renderLink" }, outputs: { viewed: "viewed", expandedChange: "expandedChange" }, ngImport: i0, template: `
|
||||||
|
<content-text
|
||||||
|
[className]="className"
|
||||||
|
[weight]="weight"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="ellipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text>
|
||||||
|
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextComponent, selector: "content-text", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||||
|
}
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextWithSuggestionsComponent, decorators: [{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
selector: "content-text-with-suggestions",
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextComponent],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<content-text
|
||||||
|
[className]="className"
|
||||||
|
[weight]="weight"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="ellipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text>
|
||||||
|
`,
|
||||||
|
}]
|
||||||
|
}], propDecorators: { className: [{
|
||||||
|
type: Input
|
||||||
|
}], weight: [{
|
||||||
|
type: Input
|
||||||
|
}], text: [{
|
||||||
|
type: Input
|
||||||
|
}], ellipsis: [{
|
||||||
|
type: Input
|
||||||
|
}], blur: [{
|
||||||
|
type: Input
|
||||||
|
}], style: [{
|
||||||
|
type: Input
|
||||||
|
}], onView: [{
|
||||||
|
type: Input
|
||||||
|
}], renderMention: [{
|
||||||
|
type: Input
|
||||||
|
}], renderTag: [{
|
||||||
|
type: Input
|
||||||
|
}], renderLink: [{
|
||||||
|
type: Input
|
||||||
|
}], viewed: [{
|
||||||
|
type: Output
|
||||||
|
}], expandedChange: [{
|
||||||
|
type: Output
|
||||||
|
}] } });
|
||||||
|
//# sourceMappingURL=content-text-with-suggestions.component.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"content-text-with-suggestions.component.js","sourceRoot":"","sources":["../../../src/angular/content-text-with-suggestions.component.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAIhG,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;;AAwBhE,MAAM,OAAO,mCAAmC;IACrC,SAAS,CAAqB;IAC9B,MAAM,GAAqB,QAAQ,CAAC;IACpC,IAAI,CAA4B;IAChC,QAAQ,GAAiC,KAAK,CAAC;IAC/C,IAAI,GAAG,KAAK,CAAC;IACb,KAAK,CAAqD;IAC1D,MAAM,CAA2B;IACjC,aAAa,CAAqC;IAClD,SAAS,CAAiC;IAC1C,UAAU,CAAkC;IAElC,MAAM,GAAG,IAAI,YAAY,EAAQ,CAAC;IAClC,cAAc,GAAG,IAAI,YAAY,EAAW,CAAC;wGAbrD,mCAAmC;4FAAnC,mCAAmC,sXAjBpC;;;;;;;;;;;;;;;GAeT,4DAjBS,oBAAoB;;4FAmBnB,mCAAmC;kBAtB/C,SAAS;mBAAC;oBACT,QAAQ,EAAE,+BAA+B;oBACzC,UAAU,EAAE,IAAI;oBAChB,OAAO,EAAE,CAAC,oBAAoB,CAAC;oBAC/B,eAAe,EAAE,uBAAuB,CAAC,MAAM;oBAC/C,QAAQ,EAAE;;;;;;;;;;;;;;;GAeT;iBACF;;sBAEE,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBAEL,MAAM;;sBACN,MAAM"}
|
||||||
92
packages/content-suggestions/dist/angular/angular/content-text.component.d.ts
vendored
Normal file
92
packages/content-suggestions/dist/angular/angular/content-text.component.d.ts
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import type { LinkEntity, MentionEntity, TagEntity } from "../core";
|
||||||
|
import type { AngularContentEllipsisConfig, AngularLinkRenderer, AngularRenderResult, AngularTagRenderer, AngularMentionRenderer } from "./types";
|
||||||
|
import { AfterViewInit, ChangeDetectorRef, EventEmitter, OnChanges, OnDestroy, SimpleChanges } from "@angular/core";
|
||||||
|
import { type TachTypographyHostProps } from "@hublib-web/tach-typography/angular";
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
type TextPart = {
|
||||||
|
kind: "text";
|
||||||
|
key: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
type MentionPart = {
|
||||||
|
kind: "mention";
|
||||||
|
key: string;
|
||||||
|
entity: MentionEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
type TagPart = {
|
||||||
|
kind: "tag";
|
||||||
|
key: string;
|
||||||
|
entity: TagEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
type LinkPart = {
|
||||||
|
kind: "link";
|
||||||
|
key: string;
|
||||||
|
entity: LinkEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
type ContentPart = TextPart | MentionPart | TagPart | LinkPart;
|
||||||
|
export declare class ContentTextComponent implements OnChanges, AfterViewInit, OnDestroy {
|
||||||
|
private readonly cdr;
|
||||||
|
className: string | undefined;
|
||||||
|
weight: TypographyWeight;
|
||||||
|
text: string | null | undefined;
|
||||||
|
ellipsis: AngularContentEllipsisConfig;
|
||||||
|
blur: boolean;
|
||||||
|
style: Record<string, string | number> | null | undefined;
|
||||||
|
onView: (() => void) | undefined;
|
||||||
|
renderMention: AngularMentionRenderer | undefined;
|
||||||
|
renderTag: AngularTagRenderer | undefined;
|
||||||
|
renderLink: AngularLinkRenderer | undefined;
|
||||||
|
readonly viewed: EventEmitter<void>;
|
||||||
|
readonly expandedChange: EventEmitter<boolean>;
|
||||||
|
private readonly containerRef;
|
||||||
|
visibleParts: ContentPart[];
|
||||||
|
mergedStyle: Record<string, string | number>;
|
||||||
|
rowsEllipsis: {
|
||||||
|
rows: number;
|
||||||
|
} | undefined;
|
||||||
|
showTrailingEllipsis: boolean;
|
||||||
|
showInlineReadMore: boolean;
|
||||||
|
showBlockReadMore: boolean;
|
||||||
|
readMoreLabel: string;
|
||||||
|
paragraphHostProps: TachTypographyHostProps;
|
||||||
|
private expanded;
|
||||||
|
private mergedExpanded;
|
||||||
|
private isExpandedControlled;
|
||||||
|
private isInsideView;
|
||||||
|
private viewedFired;
|
||||||
|
private pendingRowsOverflowCheck;
|
||||||
|
private intersectionObserver;
|
||||||
|
constructor(cdr: ChangeDetectorRef);
|
||||||
|
ngOnChanges(_changes: SimpleChanges): void;
|
||||||
|
ngAfterViewInit(): void;
|
||||||
|
ngOnDestroy(): void;
|
||||||
|
trackByKey(_index: number, part: ContentPart): string;
|
||||||
|
buildDefaultLinkHostProps(url: string): TachTypographyHostProps;
|
||||||
|
handleExpand(event: MouseEvent): void;
|
||||||
|
private recomputeContent;
|
||||||
|
private buildParts;
|
||||||
|
private resolveCustomRender;
|
||||||
|
private resolveEllipsisConfig;
|
||||||
|
private isCountConfig;
|
||||||
|
private isRowsConfig;
|
||||||
|
private getMergedExpanded;
|
||||||
|
private resolveEllipsisSymbol;
|
||||||
|
private notifyExpand;
|
||||||
|
private computeEntitySafeCutoff;
|
||||||
|
private initObserver;
|
||||||
|
private emitViewIfNeeded;
|
||||||
|
private scheduleRowsOverflowCheck;
|
||||||
|
private refreshRowsOverflow;
|
||||||
|
private getParagraphElement;
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<ContentTextComponent, never>;
|
||||||
|
static ɵcmp: i0.ɵɵComponentDeclaration<ContentTextComponent, "content-text", never, { "className": { "alias": "className"; "required": false; }; "weight": { "alias": "weight"; "required": false; }; "text": { "alias": "text"; "required": false; }; "ellipsis": { "alias": "ellipsis"; "required": false; }; "blur": { "alias": "blur"; "required": false; }; "style": { "alias": "style"; "required": false; }; "onView": { "alias": "onView"; "required": false; }; "renderMention": { "alias": "renderMention"; "required": false; }; "renderTag": { "alias": "renderTag"; "required": false; }; "renderLink": { "alias": "renderLink"; "required": false; }; }, { "viewed": "viewed"; "expandedChange": "expandedChange"; }, never, never, true, never>;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=content-text.component.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/content-text.component.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/content-text.component.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"content-text.component.d.ts","sourceRoot":"","sources":["../../../src/angular/content-text.component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,KAAK,EACV,4BAA4B,EAE5B,mBAAmB,EACnB,mBAAmB,EAEnB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,aAAa,EAEb,iBAAiB,EAGjB,YAAY,EAEZ,SAAS,EACT,SAAS,EAET,aAAa,EAEd,MAAM,eAAe,CAAC;AAEvB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;;AA4BzE,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,mBAAmB,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,KAAK,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/D,qBAkGa,oBAAqB,YAAW,SAAS,EAAE,aAAa,EAAE,SAAS;IAoC5E,OAAO,CAAC,QAAQ,CAAC,GAAG;IAnCb,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,gBAAgB,CAAY;IACpC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,4BAA4B,CAAS;IAC/C,IAAI,UAAS;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAClD,SAAS,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE3C,QAAQ,CAAC,MAAM,qBAA4B;IAC3C,QAAQ,CAAC,cAAc,wBAA+B;IAGhE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA2B;IAExD,YAAY,EAAE,WAAW,EAAE,CAAM;IACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAgC;IAC5E,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAC3C,oBAAoB,UAAS;IAC7B,kBAAkB,UAAS;IAC3B,iBAAiB,UAAS;IAC1B,aAAa,SAAkB;IAC/B,kBAAkB,EAAE,uBAAuB,CAAgC;IAE3E,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,oBAAoB,CAAS;IACrC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,wBAAwB,CAAS;IACzC,OAAO,CAAC,oBAAoB,CAAqC;gBAG9C,GAAG,EAAE,iBAAiB;IAGzC,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAI1C,eAAe,IAAI,IAAI;IAKvB,WAAW,IAAI,IAAI;IAOnB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM;IAIrD,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,uBAAuB;IAQ/D,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAYrC,OAAO,CAAC,gBAAgB;IA4DxB,OAAO,CAAC,UAAU;IAwElB,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,qBAAqB;IAW7B,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,YAAY;IA8BpB,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,mBAAmB;yCArXhB,oBAAoB;2CAApB,oBAAoB;CA6XhC"}
|
||||||
536
packages/content-suggestions/dist/angular/angular/content-text.component.js
vendored
Normal file
536
packages/content-suggestions/dist/angular/angular/content-text.component.js
vendored
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
import { NgFor, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault } from "@angular/common";
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewChild, } from "@angular/core";
|
||||||
|
import { TachTypographyComponent, } from "@hublib-web/tach-typography/angular";
|
||||||
|
import { findAllEntities } from "../core";
|
||||||
|
import { ContentSuggestionsRenderResultDirective } from "./render-result.directive";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
const READ_MORE_TEXT = "Читать полностью";
|
||||||
|
const BODY_DATA_ATTR = "data-content-suggestions-body";
|
||||||
|
const BASE_SELECTABLE_STYLE = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
WebkitTouchCallout: "default",
|
||||||
|
WebkitUserSelect: "text",
|
||||||
|
KhtmlUserSelect: "text",
|
||||||
|
MozUserSelect: "text",
|
||||||
|
msUserSelect: "text",
|
||||||
|
userSelect: "text",
|
||||||
|
};
|
||||||
|
const BLUR_STYLE = {
|
||||||
|
filter: "blur(3px)",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
KhtmlUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "none",
|
||||||
|
};
|
||||||
|
export class ContentTextComponent {
|
||||||
|
cdr;
|
||||||
|
className;
|
||||||
|
weight = "normal";
|
||||||
|
text;
|
||||||
|
ellipsis = false;
|
||||||
|
blur = false;
|
||||||
|
style;
|
||||||
|
onView;
|
||||||
|
renderMention;
|
||||||
|
renderTag;
|
||||||
|
renderLink;
|
||||||
|
viewed = new EventEmitter();
|
||||||
|
expandedChange = new EventEmitter();
|
||||||
|
containerRef;
|
||||||
|
visibleParts = [];
|
||||||
|
mergedStyle = { ...BASE_SELECTABLE_STYLE };
|
||||||
|
rowsEllipsis;
|
||||||
|
showTrailingEllipsis = false;
|
||||||
|
showInlineReadMore = false;
|
||||||
|
showBlockReadMore = false;
|
||||||
|
readMoreLabel = READ_MORE_TEXT;
|
||||||
|
paragraphHostProps = { [BODY_DATA_ATTR]: "true" };
|
||||||
|
expanded = false;
|
||||||
|
mergedExpanded = false;
|
||||||
|
isExpandedControlled = false;
|
||||||
|
isInsideView = false;
|
||||||
|
viewedFired = false;
|
||||||
|
pendingRowsOverflowCheck = false;
|
||||||
|
intersectionObserver = null;
|
||||||
|
constructor(cdr) {
|
||||||
|
this.cdr = cdr;
|
||||||
|
}
|
||||||
|
ngOnChanges(_changes) {
|
||||||
|
this.recomputeContent();
|
||||||
|
}
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.initObserver();
|
||||||
|
this.scheduleRowsOverflowCheck();
|
||||||
|
}
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.intersectionObserver) {
|
||||||
|
this.intersectionObserver.disconnect();
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackByKey(_index, part) {
|
||||||
|
return part.key;
|
||||||
|
}
|
||||||
|
buildDefaultLinkHostProps(url) {
|
||||||
|
return {
|
||||||
|
href: url,
|
||||||
|
target: "_blank",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
handleExpand(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!this.isExpandedControlled) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
this.notifyExpand(event, true);
|
||||||
|
this.recomputeContent();
|
||||||
|
}
|
||||||
|
recomputeContent() {
|
||||||
|
const content = this.text ?? "";
|
||||||
|
const entities = findAllEntities(content);
|
||||||
|
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||||
|
this.isExpandedControlled = Boolean(ellipsisConfig && typeof ellipsisConfig.expanded === "boolean");
|
||||||
|
if (!this.isExpandedControlled && this.isCountConfig(ellipsisConfig)) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
if (content.length <= count && !this.expanded) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.mergedExpanded = this.getMergedExpanded(ellipsisConfig, content);
|
||||||
|
this.readMoreLabel = this.resolveEllipsisSymbol(ellipsisConfig?.symbol, this.mergedExpanded);
|
||||||
|
this.mergedStyle = {
|
||||||
|
...BASE_SELECTABLE_STYLE,
|
||||||
|
...(this.blur ? BLUR_STYLE : {}),
|
||||||
|
...(this.style ?? {}),
|
||||||
|
};
|
||||||
|
this.visibleParts = this.buildParts(content, entities, null);
|
||||||
|
this.rowsEllipsis = undefined;
|
||||||
|
this.showTrailingEllipsis = false;
|
||||||
|
this.showInlineReadMore = false;
|
||||||
|
this.showBlockReadMore = false;
|
||||||
|
this.pendingRowsOverflowCheck = false;
|
||||||
|
if (this.isCountConfig(ellipsisConfig)) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
const shouldTruncate = !this.mergedExpanded && count > 0 && content.length > count;
|
||||||
|
if (shouldTruncate) {
|
||||||
|
const cutoff = this.computeEntitySafeCutoff(count, entities);
|
||||||
|
this.visibleParts = this.buildParts(content, entities, cutoff);
|
||||||
|
this.showTrailingEllipsis = true;
|
||||||
|
this.showInlineReadMore = Boolean(ellipsisConfig.expandable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.isRowsConfig(ellipsisConfig) && !this.mergedExpanded) {
|
||||||
|
this.rowsEllipsis = {
|
||||||
|
rows: ellipsisConfig.rows,
|
||||||
|
};
|
||||||
|
if (ellipsisConfig.expandable) {
|
||||||
|
this.pendingRowsOverflowCheck = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
this.scheduleRowsOverflowCheck();
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
buildParts(content, entities, upto) {
|
||||||
|
const parts = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
for (const [index, entity] of entities.entries()) {
|
||||||
|
if (upto !== null && entity.start >= upto) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
|
||||||
|
if (entity.start > lastIndex && lastIndex < textEnd) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${textEnd}`,
|
||||||
|
text: content.slice(lastIndex, textEnd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (upto === null || entity.end <= upto) {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
parts.push({
|
||||||
|
kind: "mention",
|
||||||
|
key: `mention-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderMention, entity, index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (entity.type === "tag") {
|
||||||
|
parts.push({
|
||||||
|
kind: "tag",
|
||||||
|
key: `tag-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderTag, entity, index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
parts.push({
|
||||||
|
kind: "link",
|
||||||
|
key: `link-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderLink, entity, index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = entity.end;
|
||||||
|
}
|
||||||
|
if (upto === null) {
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${content.length}`,
|
||||||
|
text: content.slice(lastIndex),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (lastIndex < upto) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${upto}`,
|
||||||
|
text: content.slice(lastIndex, upto),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
resolveCustomRender(rendererFn, entity, index) {
|
||||||
|
const rendered = rendererFn?.(entity, index);
|
||||||
|
return rendered ?? null;
|
||||||
|
}
|
||||||
|
resolveEllipsisConfig() {
|
||||||
|
if (!this.ellipsis || typeof this.ellipsis !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.ellipsis;
|
||||||
|
}
|
||||||
|
isCountConfig(config) {
|
||||||
|
return Boolean(config && "count" in config);
|
||||||
|
}
|
||||||
|
isRowsConfig(config) {
|
||||||
|
return Boolean(config && "rows" in config);
|
||||||
|
}
|
||||||
|
getMergedExpanded(config, text) {
|
||||||
|
const controlledExpanded = config && typeof config.expanded === "boolean"
|
||||||
|
? config.expanded
|
||||||
|
: undefined;
|
||||||
|
if (controlledExpanded !== undefined) {
|
||||||
|
return controlledExpanded;
|
||||||
|
}
|
||||||
|
if (this.isCountConfig(config)) {
|
||||||
|
const count = config.count ?? 0;
|
||||||
|
if (count <= 0 || text.length <= count) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.expanded;
|
||||||
|
}
|
||||||
|
resolveEllipsisSymbol(symbol, expanded) {
|
||||||
|
if (typeof symbol === "function") {
|
||||||
|
return symbol(expanded);
|
||||||
|
}
|
||||||
|
return symbol ?? READ_MORE_TEXT;
|
||||||
|
}
|
||||||
|
notifyExpand(event, expanded) {
|
||||||
|
const config = this.resolveEllipsisConfig();
|
||||||
|
const onExpand = config?.onExpand;
|
||||||
|
if (onExpand) {
|
||||||
|
if (onExpand.length >= 2) {
|
||||||
|
onExpand(event, { expanded });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onExpand(expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.expandedChange.emit(expanded);
|
||||||
|
}
|
||||||
|
computeEntitySafeCutoff(count, entities) {
|
||||||
|
let cutoff = count;
|
||||||
|
let extended = true;
|
||||||
|
while (extended) {
|
||||||
|
extended = false;
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start < cutoff && entity.end > cutoff) {
|
||||||
|
cutoff = entity.end;
|
||||||
|
extended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
initObserver() {
|
||||||
|
const host = this.containerRef?.nativeElement;
|
||||||
|
if (!host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof IntersectionObserver === "undefined") {
|
||||||
|
this.isInsideView = true;
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.intersectionObserver = new IntersectionObserver(entries => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.isIntersecting && !this.isInsideView) {
|
||||||
|
this.isInsideView = true;
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
}
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
this.intersectionObserver.observe(host);
|
||||||
|
}
|
||||||
|
emitViewIfNeeded() {
|
||||||
|
if (!this.mergedExpanded || this.viewedFired || !this.isInsideView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.viewedFired = true;
|
||||||
|
this.onView?.();
|
||||||
|
this.viewed.emit();
|
||||||
|
}
|
||||||
|
scheduleRowsOverflowCheck() {
|
||||||
|
if (!this.pendingRowsOverflowCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queueMicrotask(() => {
|
||||||
|
this.refreshRowsOverflow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
refreshRowsOverflow() {
|
||||||
|
const paragraph = this.getParagraphElement();
|
||||||
|
if (!paragraph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasOverflow = paragraph.scrollHeight - paragraph.clientHeight > 1;
|
||||||
|
if (this.showBlockReadMore !== hasOverflow) {
|
||||||
|
this.showBlockReadMore = hasOverflow;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getParagraphElement() {
|
||||||
|
const host = this.containerRef?.nativeElement;
|
||||||
|
if (!host) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return host.querySelector(`[${BODY_DATA_ATTR}="true"]`);
|
||||||
|
}
|
||||||
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
||||||
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: ContentTextComponent, isStandalone: true, selector: "content-text", inputs: { className: "className", weight: "weight", text: "text", ellipsis: "ellipsis", blur: "blur", style: "style", onView: "onView", renderMention: "renderMention", renderTag: "renderTag", renderLink: "renderLink" }, outputs: { viewed: "viewed", expandedChange: "expandedChange" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
||||||
|
<div #container>
|
||||||
|
<tach-typography
|
||||||
|
as="p"
|
||||||
|
variant="Body"
|
||||||
|
[weight]="weight"
|
||||||
|
[className]="className"
|
||||||
|
[preserveStyle]="mergedStyle"
|
||||||
|
[ellipsis]="rowsEllipsis"
|
||||||
|
[hostProps]="paragraphHostProps"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let part of visibleParts; trackBy: trackByKey">
|
||||||
|
<ng-container [ngSwitch]="part.kind">
|
||||||
|
<ng-container *ngSwitchCase="'text'">{{ part.text }}</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'mention'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultMention">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultMention>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.displayText }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'tag'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultTag">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultTag>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'link'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultLink">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultLink>
|
||||||
|
<tach-typography
|
||||||
|
as="a"
|
||||||
|
variant="Body"
|
||||||
|
color="link"
|
||||||
|
[weight]="weight"
|
||||||
|
[hostProps]="buildDefaultLinkHostProps(part.entity.url)"
|
||||||
|
>
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchDefault></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showTrailingEllipsis">…</ng-container>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showInlineReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-left: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</tach-typography>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showBlockReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-top: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "component", type: TachTypographyComponent, selector: "tach-typography", inputs: ["as", "variant", "color", "weight", "clickable", "markdownEnabled", "markdown", "className", "ellipsis", "nzProps", "hostProps", "preserveStyle"], outputs: ["tachClick"] }, { kind: "directive", type: ContentSuggestionsRenderResultDirective, selector: "[contentSuggestionsRenderResult]", inputs: ["contentSuggestionsRenderResult"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||||
|
}
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTextComponent, decorators: [{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
selector: "content-text",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgFor,
|
||||||
|
NgIf,
|
||||||
|
NgSwitch,
|
||||||
|
NgSwitchCase,
|
||||||
|
NgSwitchDefault,
|
||||||
|
TachTypographyComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<div #container>
|
||||||
|
<tach-typography
|
||||||
|
as="p"
|
||||||
|
variant="Body"
|
||||||
|
[weight]="weight"
|
||||||
|
[className]="className"
|
||||||
|
[preserveStyle]="mergedStyle"
|
||||||
|
[ellipsis]="rowsEllipsis"
|
||||||
|
[hostProps]="paragraphHostProps"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let part of visibleParts; trackBy: trackByKey">
|
||||||
|
<ng-container [ngSwitch]="part.kind">
|
||||||
|
<ng-container *ngSwitchCase="'text'">{{ part.text }}</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'mention'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultMention">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultMention>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.displayText }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'tag'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultTag">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultTag>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'link'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultLink">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultLink>
|
||||||
|
<tach-typography
|
||||||
|
as="a"
|
||||||
|
variant="Body"
|
||||||
|
color="link"
|
||||||
|
[weight]="weight"
|
||||||
|
[hostProps]="buildDefaultLinkHostProps(part.entity.url)"
|
||||||
|
>
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchDefault></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showTrailingEllipsis">…</ng-container>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showInlineReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-left: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</tach-typography>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showBlockReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-top: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}]
|
||||||
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { className: [{
|
||||||
|
type: Input
|
||||||
|
}], weight: [{
|
||||||
|
type: Input
|
||||||
|
}], text: [{
|
||||||
|
type: Input
|
||||||
|
}], ellipsis: [{
|
||||||
|
type: Input
|
||||||
|
}], blur: [{
|
||||||
|
type: Input
|
||||||
|
}], style: [{
|
||||||
|
type: Input
|
||||||
|
}], onView: [{
|
||||||
|
type: Input
|
||||||
|
}], renderMention: [{
|
||||||
|
type: Input
|
||||||
|
}], renderTag: [{
|
||||||
|
type: Input
|
||||||
|
}], renderLink: [{
|
||||||
|
type: Input
|
||||||
|
}], viewed: [{
|
||||||
|
type: Output
|
||||||
|
}], expandedChange: [{
|
||||||
|
type: Output
|
||||||
|
}], containerRef: [{
|
||||||
|
type: ViewChild,
|
||||||
|
args: ["container", { static: true }]
|
||||||
|
}] } });
|
||||||
|
//# sourceMappingURL=content-text.component.js.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/content-text.component.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/content-text.component.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
20
packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.d.ts
vendored
Normal file
20
packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { AngularContentEllipsisConfig, AngularLinkRenderer, AngularMentionRenderer, AngularTagRenderer } from "./types";
|
||||||
|
import { EventEmitter } from "@angular/core";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export declare class ContentTitleWithSuggestionsComponent {
|
||||||
|
className: string | undefined;
|
||||||
|
text: string | null | undefined;
|
||||||
|
ellipsis: AngularContentEllipsisConfig | undefined;
|
||||||
|
blur: boolean;
|
||||||
|
style: Record<string, string | number> | null | undefined;
|
||||||
|
onView: (() => void) | undefined;
|
||||||
|
renderMention: AngularMentionRenderer | undefined;
|
||||||
|
renderTag: AngularTagRenderer | undefined;
|
||||||
|
renderLink: AngularLinkRenderer | undefined;
|
||||||
|
readonly viewed: EventEmitter<void>;
|
||||||
|
readonly expandedChange: EventEmitter<boolean>;
|
||||||
|
get normalizedEllipsis(): AngularContentEllipsisConfig;
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<ContentTitleWithSuggestionsComponent, never>;
|
||||||
|
static ɵcmp: i0.ɵɵComponentDeclaration<ContentTitleWithSuggestionsComponent, "content-title-with-suggestions", never, { "className": { "alias": "className"; "required": false; }; "text": { "alias": "text"; "required": false; }; "ellipsis": { "alias": "ellipsis"; "required": false; }; "blur": { "alias": "blur"; "required": false; }; "style": { "alias": "style"; "required": false; }; "onView": { "alias": "onView"; "required": false; }; "renderMention": { "alias": "renderMention"; "required": false; }; "renderTag": { "alias": "renderTag"; "required": false; }; "renderLink": { "alias": "renderLink"; "required": false; }; }, { "viewed": "viewed"; "expandedChange": "expandedChange"; }, never, never, true, never>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=content-title-with-suggestions.component.d.ts.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"content-title-with-suggestions.component.d.ts","sourceRoot":"","sources":["../../../src/angular/content-title-with-suggestions.component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,4BAA4B,EAC5B,mBAAmB,EACnB,sBAAsB,EAEtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAsC,YAAY,EAAiB,MAAM,eAAe,CAAC;;AAIhG,qBAsBa,oCAAoC;IACtC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,4BAA4B,GAAG,SAAS,CAAC;IACnD,IAAI,UAAS;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1D,MAAM,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACjC,aAAa,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAClD,SAAS,EAAE,kBAAkB,GAAG,SAAS,CAAC;IAC1C,UAAU,EAAE,mBAAmB,GAAG,SAAS,CAAC;IAE3C,QAAQ,CAAC,MAAM,qBAA4B;IAC3C,QAAQ,CAAC,cAAc,wBAA+B;IAEhE,IAAI,kBAAkB,IAAI,4BAA4B,CAMrD;yCApBU,oCAAoC;2CAApC,oCAAoC;CAqBhD"}
|
||||||
87
packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js
vendored
Normal file
87
packages/content-suggestions/dist/angular/angular/content-title-with-suggestions.component.js
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggestions.component";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export class ContentTitleWithSuggestionsComponent {
|
||||||
|
className;
|
||||||
|
text;
|
||||||
|
ellipsis;
|
||||||
|
blur = false;
|
||||||
|
style;
|
||||||
|
onView;
|
||||||
|
renderMention;
|
||||||
|
renderTag;
|
||||||
|
renderLink;
|
||||||
|
viewed = new EventEmitter();
|
||||||
|
expandedChange = new EventEmitter();
|
||||||
|
get normalizedEllipsis() {
|
||||||
|
if (this.ellipsis !== undefined) {
|
||||||
|
return this.ellipsis;
|
||||||
|
}
|
||||||
|
return { rows: 2 };
|
||||||
|
}
|
||||||
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTitleWithSuggestionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||||
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: ContentTitleWithSuggestionsComponent, isStandalone: true, selector: "content-title-with-suggestions", inputs: { className: "className", text: "text", ellipsis: "ellipsis", blur: "blur", style: "style", onView: "onView", renderMention: "renderMention", renderTag: "renderTag", renderLink: "renderLink" }, outputs: { viewed: "viewed", expandedChange: "expandedChange" }, ngImport: i0, template: `
|
||||||
|
<content-text-with-suggestions
|
||||||
|
[className]="className"
|
||||||
|
weight="bold"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="normalizedEllipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text-with-suggestions>
|
||||||
|
`, isInline: true, dependencies: [{ kind: "component", type: ContentTextWithSuggestionsComponent, selector: "content-text-with-suggestions", inputs: ["className", "weight", "text", "ellipsis", "blur", "style", "onView", "renderMention", "renderTag", "renderLink"], outputs: ["viewed", "expandedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||||
|
}
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentTitleWithSuggestionsComponent, decorators: [{
|
||||||
|
type: Component,
|
||||||
|
args: [{
|
||||||
|
selector: "content-title-with-suggestions",
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextWithSuggestionsComponent],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<content-text-with-suggestions
|
||||||
|
[className]="className"
|
||||||
|
weight="bold"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="normalizedEllipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text-with-suggestions>
|
||||||
|
`,
|
||||||
|
}]
|
||||||
|
}], propDecorators: { className: [{
|
||||||
|
type: Input
|
||||||
|
}], text: [{
|
||||||
|
type: Input
|
||||||
|
}], ellipsis: [{
|
||||||
|
type: Input
|
||||||
|
}], blur: [{
|
||||||
|
type: Input
|
||||||
|
}], style: [{
|
||||||
|
type: Input
|
||||||
|
}], onView: [{
|
||||||
|
type: Input
|
||||||
|
}], renderMention: [{
|
||||||
|
type: Input
|
||||||
|
}], renderTag: [{
|
||||||
|
type: Input
|
||||||
|
}], renderLink: [{
|
||||||
|
type: Input
|
||||||
|
}], viewed: [{
|
||||||
|
type: Output
|
||||||
|
}], expandedChange: [{
|
||||||
|
type: Output
|
||||||
|
}] } });
|
||||||
|
//# sourceMappingURL=content-title-with-suggestions.component.js.map
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"content-title-with-suggestions.component.js","sourceRoot":"","sources":["../../../src/angular/content-title-with-suggestions.component.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEhG,OAAO,EAAE,mCAAmC,EAAE,MAAM,2CAA2C,CAAC;;AAwBhG,MAAM,OAAO,oCAAoC;IACtC,SAAS,CAAqB;IAC9B,IAAI,CAA4B;IAChC,QAAQ,CAA2C;IACnD,IAAI,GAAG,KAAK,CAAC;IACb,KAAK,CAAqD;IAC1D,MAAM,CAA2B;IACjC,aAAa,CAAqC;IAClD,SAAS,CAAiC;IAC1C,UAAU,CAAkC;IAElC,MAAM,GAAG,IAAI,YAAY,EAAQ,CAAC;IAClC,cAAc,GAAG,IAAI,YAAY,EAAW,CAAC;IAEhE,IAAI,kBAAkB;QACpB,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,OAAO,IAAI,CAAC,QAAQ,CAAC;QACvB,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,CAAC,EAAsC,CAAC;IACzD,CAAC;wGApBU,oCAAoC;4FAApC,oCAAoC,qWAjBrC;;;;;;;;;;;;;;;GAeT,4DAjBS,mCAAmC;;4FAmBlC,oCAAoC;kBAtBhD,SAAS;mBAAC;oBACT,QAAQ,EAAE,gCAAgC;oBAC1C,UAAU,EAAE,IAAI;oBAChB,OAAO,EAAE,CAAC,mCAAmC,CAAC;oBAC9C,eAAe,EAAE,uBAAuB,CAAC,MAAM;oBAC/C,QAAQ,EAAE;;;;;;;;;;;;;;;GAeT;iBACF;;sBAEE,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBACL,KAAK;;sBAEL,MAAM;;sBACN,MAAM"}
|
||||||
18
packages/content-suggestions/dist/angular/angular/index.d.ts
vendored
Normal file
18
packages/content-suggestions/dist/angular/angular/index.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ContentTextComponent } from "./content-text.component";
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggestions.component";
|
||||||
|
import { ContentTitleWithSuggestionsComponent } from "./content-title-with-suggestions.component";
|
||||||
|
import { ContentSuggestionsRenderResultDirective } from "./render-result.directive";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
import * as i1 from "./content-text.component";
|
||||||
|
import * as i2 from "./content-text-with-suggestions.component";
|
||||||
|
import * as i3 from "./content-title-with-suggestions.component";
|
||||||
|
import * as i4 from "./render-result.directive";
|
||||||
|
export type { AngularContentEllipsisConfig, AngularContentSnapshot, AngularContentTextProps, AngularContentTextWithSuggestionsProps, AngularContentTitleWithSuggestionsProps, AngularContentToken, AngularCountEllipsisConfig, AngularEllipsisExpandHandler, AngularEllipsisSymbol, AngularExpandInfo, AngularLinkRenderer, AngularLinkToken, AngularMentionRenderer, AngularMentionToken, AngularRenderResult, AngularRowsEllipsisConfig, AngularTagRenderer, AngularTagToken, AngularTextToken, } from "./types";
|
||||||
|
export { AngularContentSuggestionsAdapter, buildAngularTagHref, createAngularContentTokens, } from "./tokens";
|
||||||
|
export { ContentTextComponent, ContentTextWithSuggestionsComponent, ContentTitleWithSuggestionsComponent, ContentSuggestionsRenderResultDirective, };
|
||||||
|
export declare class ContentSuggestionsAngularModule {
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<ContentSuggestionsAngularModule, never>;
|
||||||
|
static ɵmod: i0.ɵɵNgModuleDeclaration<ContentSuggestionsAngularModule, never, [typeof i1.ContentTextComponent, typeof i2.ContentTextWithSuggestionsComponent, typeof i3.ContentTitleWithSuggestionsComponent, typeof i4.ContentSuggestionsRenderResultDirective], [typeof i1.ContentTextComponent, typeof i2.ContentTextWithSuggestionsComponent, typeof i3.ContentTitleWithSuggestionsComponent, typeof i4.ContentSuggestionsRenderResultDirective]>;
|
||||||
|
static ɵinj: i0.ɵɵInjectorDeclaration<ContentSuggestionsAngularModule>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/index.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/angular/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,mCAAmC,EAAE,MAAM,2CAA2C,CAAC;AAChG,OAAO,EAAE,oCAAoC,EAAE,MAAM,4CAA4C,CAAC;AAClG,OAAO,EAAE,uCAAuC,EAAE,MAAM,2BAA2B,CAAC;;;;;;AAEpF,YAAY,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,sCAAsC,EACtC,uCAAuC,EACvC,mBAAmB,EACnB,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,gCAAgC,EAChC,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,oBAAoB,EACpB,mCAAmC,EACnC,oCAAoC,EACpC,uCAAuC,GACxC,CAAC;AAEF,qBAca,+BAA+B;yCAA/B,+BAA+B;0CAA/B,+BAA+B;0CAA/B,+BAA+B;CAAG"}
|
||||||
39
packages/content-suggestions/dist/angular/angular/index.js
vendored
Normal file
39
packages/content-suggestions/dist/angular/angular/index.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { ContentTextComponent } from "./content-text.component";
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggestions.component";
|
||||||
|
import { ContentTitleWithSuggestionsComponent } from "./content-title-with-suggestions.component";
|
||||||
|
import { ContentSuggestionsRenderResultDirective } from "./render-result.directive";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export { AngularContentSuggestionsAdapter, buildAngularTagHref, createAngularContentTokens, } from "./tokens";
|
||||||
|
export { ContentTextComponent, ContentTextWithSuggestionsComponent, ContentTitleWithSuggestionsComponent, ContentSuggestionsRenderResultDirective, };
|
||||||
|
export class ContentSuggestionsAngularModule {
|
||||||
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsAngularModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
||||||
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsAngularModule, imports: [ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective], exports: [ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective] });
|
||||||
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsAngularModule, imports: [ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent] });
|
||||||
|
}
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsAngularModule, decorators: [{
|
||||||
|
type: NgModule,
|
||||||
|
args: [{
|
||||||
|
imports: [
|
||||||
|
ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
}] });
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/index.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/angular/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,mCAAmC,EAAE,MAAM,2CAA2C,CAAC;AAChG,OAAO,EAAE,oCAAoC,EAAE,MAAM,4CAA4C,CAAC;AAClG,OAAO,EAAE,uCAAuC,EAAE,MAAM,2BAA2B,CAAC;;AAwBpF,OAAO,EACL,gCAAgC,EAChC,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,oBAAoB,EACpB,mCAAmC,EACnC,oCAAoC,EACpC,uCAAuC,GACxC,CAAC;AAgBF,MAAM,OAAO,+BAA+B;wGAA/B,+BAA+B;yGAA/B,+BAA+B,YAZxC,oBAAoB;YACpB,mCAAmC;YACnC,oCAAoC;YACpC,uCAAuC,aAGvC,oBAAoB;YACpB,mCAAmC;YACnC,oCAAoC;YACpC,uCAAuC;yGAG9B,+BAA+B,YAZxC,oBAAoB;YACpB,mCAAmC;YACnC,oCAAoC;;4FAU3B,+BAA+B;kBAd3C,QAAQ;mBAAC;oBACR,OAAO,EAAE;wBACP,oBAAoB;wBACpB,mCAAmC;wBACnC,oCAAoC;wBACpC,uCAAuC;qBACxC;oBACD,OAAO,EAAE;wBACP,oBAAoB;wBACpB,mCAAmC;wBACnC,oCAAoC;wBACpC,uCAAuC;qBACxC;iBACF"}
|
||||||
2
packages/content-suggestions/dist/angular/angular/public-api.d.ts
vendored
Normal file
2
packages/content-suggestions/dist/angular/angular/public-api.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./index";
|
||||||
|
//# sourceMappingURL=public-api.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/public-api.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/public-api.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../../../src/angular/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
|
||||||
2
packages/content-suggestions/dist/angular/angular/public-api.js
vendored
Normal file
2
packages/content-suggestions/dist/angular/angular/public-api.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./index";
|
||||||
|
//# sourceMappingURL=public-api.js.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/public-api.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/public-api.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../../../src/angular/public-api.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
|
||||||
14
packages/content-suggestions/dist/angular/angular/render-result.directive.d.ts
vendored
Normal file
14
packages/content-suggestions/dist/angular/angular/render-result.directive.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { AngularRenderResult } from "./types";
|
||||||
|
import { ElementRef, OnChanges, Renderer2 } from "@angular/core";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export declare class ContentSuggestionsRenderResultDirective implements OnChanges {
|
||||||
|
private readonly elementRef;
|
||||||
|
private readonly renderer;
|
||||||
|
contentSuggestionsRenderResult: AngularRenderResult;
|
||||||
|
constructor(elementRef: ElementRef<HTMLElement>, renderer: Renderer2);
|
||||||
|
ngOnChanges(): void;
|
||||||
|
private clearHost;
|
||||||
|
static ɵfac: i0.ɵɵFactoryDeclaration<ContentSuggestionsRenderResultDirective, never>;
|
||||||
|
static ɵdir: i0.ɵɵDirectiveDeclaration<ContentSuggestionsRenderResultDirective, "[contentSuggestionsRenderResult]", never, { "contentSuggestionsRenderResult": { "alias": "contentSuggestionsRenderResult"; "required": false; }; }, {}, never, never, true, never>;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=render-result.directive.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/render-result.directive.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/render-result.directive.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"render-result.directive.d.ts","sourceRoot":"","sources":["../../../src/angular/render-result.directive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,OAAO,EAAa,UAAU,EAAS,SAAS,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;;AAEnF,qBAIa,uCAAwC,YAAW,SAAS;IAIrE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJlB,8BAA8B,EAAE,mBAAmB,CAAC;gBAG1C,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,QAAQ,EAAE,SAAS;IAKtC,WAAW,IAAI,IAAI;IAiBnB,OAAO,CAAC,SAAS;yCA3BN,uCAAuC;2CAAvC,uCAAuC;CAkCnD"}
|
||||||
43
packages/content-suggestions/dist/angular/angular/render-result.directive.js
vendored
Normal file
43
packages/content-suggestions/dist/angular/angular/render-result.directive.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Directive, Input } from "@angular/core";
|
||||||
|
import * as i0 from "@angular/core";
|
||||||
|
export class ContentSuggestionsRenderResultDirective {
|
||||||
|
elementRef;
|
||||||
|
renderer;
|
||||||
|
contentSuggestionsRenderResult;
|
||||||
|
constructor(elementRef, renderer) {
|
||||||
|
this.elementRef = elementRef;
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.renderer.setStyle(this.elementRef.nativeElement, "display", "contents");
|
||||||
|
}
|
||||||
|
ngOnChanges() {
|
||||||
|
this.clearHost();
|
||||||
|
const value = this.contentSuggestionsRenderResult;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value instanceof Node) {
|
||||||
|
this.renderer.appendChild(this.elementRef.nativeElement, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const textNode = this.renderer.createText(String(value));
|
||||||
|
this.renderer.appendChild(this.elementRef.nativeElement, textNode);
|
||||||
|
}
|
||||||
|
clearHost() {
|
||||||
|
const host = this.elementRef.nativeElement;
|
||||||
|
while (host.firstChild) {
|
||||||
|
host.removeChild(host.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsRenderResultDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
||||||
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: ContentSuggestionsRenderResultDirective, isStandalone: true, selector: "[contentSuggestionsRenderResult]", inputs: { contentSuggestionsRenderResult: "contentSuggestionsRenderResult" }, usesOnChanges: true, ngImport: i0 });
|
||||||
|
}
|
||||||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: ContentSuggestionsRenderResultDirective, decorators: [{
|
||||||
|
type: Directive,
|
||||||
|
args: [{
|
||||||
|
selector: "[contentSuggestionsRenderResult]",
|
||||||
|
standalone: true,
|
||||||
|
}]
|
||||||
|
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { contentSuggestionsRenderResult: [{
|
||||||
|
type: Input
|
||||||
|
}] } });
|
||||||
|
//# sourceMappingURL=render-result.directive.js.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/render-result.directive.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/render-result.directive.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"render-result.directive.js","sourceRoot":"","sources":["../../../src/angular/render-result.directive.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAc,KAAK,EAAwB,MAAM,eAAe,CAAC;;AAMnF,MAAM,OAAO,uCAAuC;IAI/B;IACA;IAJV,8BAA8B,CAAsB;IAE7D,YACmB,UAAmC,EACnC,QAAmB;QADnB,eAAU,GAAV,UAAU,CAAyB;QACnC,aAAQ,GAAR,QAAQ,CAAW;QAEpC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAC/E,CAAC;IAED,WAAW;QACT,IAAI,CAAC,SAAS,EAAE,CAAC;QAEjB,MAAM,KAAK,GAAG,IAAI,CAAC,8BAA8B,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QAED,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;YAC1B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAChE,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzD,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;IACrE,CAAC;IAEO,SAAS;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC;QAE3C,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC;YACvB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;wGAjCU,uCAAuC;4FAAvC,uCAAuC;;4FAAvC,uCAAuC;kBAJnD,SAAS;mBAAC;oBACT,QAAQ,EAAE,kCAAkC;oBAC5C,UAAU,EAAE,IAAI;iBACjB;;sBAEE,KAAK"}
|
||||||
8
packages/content-suggestions/dist/angular/angular/tokens.d.ts
vendored
Normal file
8
packages/content-suggestions/dist/angular/angular/tokens.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { AngularContentSnapshot, AngularContentToken } from "./types";
|
||||||
|
import type { TagEntity } from "../core";
|
||||||
|
export declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
|
||||||
|
export declare const buildAngularTagHref: (entity: TagEntity) => string;
|
||||||
|
export declare class AngularContentSuggestionsAdapter {
|
||||||
|
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=tokens.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/tokens.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/tokens.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../src/angular/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,mBAAmB,EAKpB,MAAM,SAAS,CAAC;AAEjB,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,SAAS,CAAC;AAgCxD,eAAO,MAAM,0BAA0B,GACrC,WAAW,MAAM,GAAG,IAAI,GAAG,SAAS,KACnC,mBAAmB,EAqBrB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,SAAS,KAAG,MAEvD,CAAC;AAEF,qBAAa,gCAAgC;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,sBAAsB;CAWvE"}
|
||||||
58
packages/content-suggestions/dist/angular/angular/tokens.js
vendored
Normal file
58
packages/content-suggestions/dist/angular/angular/tokens.js
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { findAllEntities } from "../core";
|
||||||
|
const mapEntityToToken = (entity) => {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
return {
|
||||||
|
kind: "mention",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (entity.type === "tag") {
|
||||||
|
return {
|
||||||
|
kind: "tag",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "link",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const createTextToken = (text, start, end) => ({
|
||||||
|
kind: "text",
|
||||||
|
text,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
export const createAngularContentTokens = (inputText) => {
|
||||||
|
const text = inputText ?? "";
|
||||||
|
const entities = findAllEntities(text);
|
||||||
|
let cursor = 0;
|
||||||
|
const tokens = [];
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start > cursor) {
|
||||||
|
tokens.push(createTextToken(text.slice(cursor, entity.start), cursor, entity.start));
|
||||||
|
}
|
||||||
|
tokens.push(mapEntityToToken(entity));
|
||||||
|
cursor = entity.end;
|
||||||
|
}
|
||||||
|
if (cursor < text.length) {
|
||||||
|
tokens.push(createTextToken(text.slice(cursor), cursor, text.length));
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
export const buildAngularTagHref = (entity) => {
|
||||||
|
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
|
||||||
|
};
|
||||||
|
export class AngularContentSuggestionsAdapter {
|
||||||
|
snapshot(inputText) {
|
||||||
|
const text = inputText ?? "";
|
||||||
|
const entities = findAllEntities(text);
|
||||||
|
const tokens = createAngularContentTokens(text);
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
entities,
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=tokens.js.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/tokens.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/tokens.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../../src/angular/tokens.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,gBAAgB,GAAG,CAAC,MAAqB,EAA4D,EAAE;IAC3G,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO;YACL,IAAI,EAAE,SAAS;YACf,MAAM;SACP,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,KAAK;YACX,MAAM;SACP,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,MAAM;KACP,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,eAAe,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,GAAW,EAAoB,EAAE,CAAC,CAAC;IACvF,IAAI,EAAE,MAAM;IACZ,IAAI;IACJ,KAAK;IACL,GAAG;CACJ,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CACxC,SAAoC,EACb,EAAE;IACzB,MAAM,IAAI,GAAG,SAAS,IAAI,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IAEvC,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,MAAM,GAA0B,EAAE,CAAC;IAEzC,KAAK,MAAM,MAAM,IAAI,QAAQ,EAAE,CAAC;QAC9B,IAAI,MAAM,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACvF,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;QACtC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC;IACtB,CAAC;IAED,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACzB,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAiB,EAAU,EAAE;IAC/D,OAAO,kBAAkB,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;AAC1E,CAAC,CAAC;AAEF,MAAM,OAAO,gCAAgC;IAC3C,QAAQ,CAAC,SAAoC;QAC3C,MAAM,IAAI,GAAG,SAAS,IAAI,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,0BAA0B,CAAC,IAAI,CAAC,CAAC;QAEhD,OAAO;YACL,IAAI;YACJ,QAAQ;YACR,MAAM;SACP,CAAC;IACJ,CAAC;CACF"}
|
||||||
70
packages/content-suggestions/dist/angular/angular/types.d.ts
vendored
Normal file
70
packages/content-suggestions/dist/angular/angular/types.d.ts
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { ContentEntity, LinkEntity, MentionEntity, TagEntity } from "../core";
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
export interface AngularTextToken {
|
||||||
|
kind: "text";
|
||||||
|
text: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
export interface AngularMentionToken {
|
||||||
|
kind: "mention";
|
||||||
|
entity: MentionEntity;
|
||||||
|
}
|
||||||
|
export interface AngularTagToken {
|
||||||
|
kind: "tag";
|
||||||
|
entity: TagEntity;
|
||||||
|
}
|
||||||
|
export interface AngularLinkToken {
|
||||||
|
kind: "link";
|
||||||
|
entity: LinkEntity;
|
||||||
|
}
|
||||||
|
export type AngularContentToken = AngularTextToken | AngularMentionToken | AngularTagToken | AngularLinkToken;
|
||||||
|
export interface AngularContentSnapshot {
|
||||||
|
text: string;
|
||||||
|
entities: ContentEntity[];
|
||||||
|
tokens: AngularContentToken[];
|
||||||
|
}
|
||||||
|
export interface AngularExpandInfo {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
export type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
|
||||||
|
export type AngularEllipsisExpandHandler = ((expanded: boolean) => void) | ((event: MouseEvent, info: AngularExpandInfo) => void);
|
||||||
|
export type AngularCountEllipsisConfig = {
|
||||||
|
count: number;
|
||||||
|
rows?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
symbol?: AngularEllipsisSymbol;
|
||||||
|
onExpand?: AngularEllipsisExpandHandler;
|
||||||
|
};
|
||||||
|
export type AngularRowsEllipsisConfig = {
|
||||||
|
rows: number;
|
||||||
|
count?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
symbol?: AngularEllipsisSymbol;
|
||||||
|
onExpand?: AngularEllipsisExpandHandler;
|
||||||
|
};
|
||||||
|
export type AngularContentEllipsisConfig = AngularCountEllipsisConfig | AngularRowsEllipsisConfig | false;
|
||||||
|
export type AngularRenderResult = Node | string | number | null | undefined;
|
||||||
|
export type AngularMentionRenderer = (entity: MentionEntity, index: number) => AngularRenderResult;
|
||||||
|
export type AngularTagRenderer = (entity: TagEntity, index: number) => AngularRenderResult;
|
||||||
|
export type AngularLinkRenderer = (entity: LinkEntity, index: number) => AngularRenderResult;
|
||||||
|
export interface AngularContentTextProps {
|
||||||
|
className?: string;
|
||||||
|
weight?: TypographyWeight;
|
||||||
|
text?: string | null;
|
||||||
|
ellipsis?: AngularContentEllipsisConfig;
|
||||||
|
blur?: boolean;
|
||||||
|
style?: Record<string, string | number> | null;
|
||||||
|
onView?: () => void;
|
||||||
|
renderMention?: AngularMentionRenderer;
|
||||||
|
renderTag?: AngularTagRenderer;
|
||||||
|
renderLink?: AngularLinkRenderer;
|
||||||
|
}
|
||||||
|
export type AngularContentTextWithSuggestionsProps = Omit<AngularContentTextProps, "renderMention" | "renderTag"> & {
|
||||||
|
renderMention?: AngularMentionRenderer;
|
||||||
|
renderTag?: AngularTagRenderer;
|
||||||
|
};
|
||||||
|
export type AngularContentTitleWithSuggestionsProps = Omit<AngularContentTextWithSuggestionsProps, "weight">;
|
||||||
|
//# sourceMappingURL=types.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/angular/types.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/types.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/angular/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,MAAM,mBAAmB,GAC3B,gBAAgB,GAChB,mBAAmB,GACnB,eAAe,GACf,gBAAgB,CAAC;AAErB,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC;AAE7E,MAAM,MAAM,4BAA4B,GACpC,CAAC,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC,GAC7B,CAAC,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC,CAAC;AAE3D,MAAM,MAAM,0BAA0B,GAAG;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,QAAQ,CAAC,EAAE,4BAA4B,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B,QAAQ,CAAC,EAAE,4BAA4B,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,4BAA4B,GACpC,0BAA0B,GAC1B,yBAAyB,GACzB,KAAK,CAAC;AAEV,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAE5E,MAAM,MAAM,sBAAsB,GAAG,CACnC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,KACV,mBAAmB,CAAC;AAEzB,MAAM,MAAM,kBAAkB,GAAG,CAC/B,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,KACV,mBAAmB,CAAC;AAEzB,MAAM,MAAM,mBAAmB,GAAG,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,KACV,mBAAmB,CAAC;AAEzB,MAAM,WAAW,uBAAuB;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,4BAA4B,CAAC;IACxC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,UAAU,CAAC,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,MAAM,sCAAsC,GAAG,IAAI,CACvD,uBAAuB,EACvB,eAAe,GAAG,WAAW,CAC9B,GAAG;IACF,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,SAAS,CAAC,EAAE,kBAAkB,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,uCAAuC,GAAG,IAAI,CACxD,sCAAsC,EACtC,QAAQ,CACT,CAAC"}
|
||||||
1
packages/content-suggestions/dist/angular/angular/types.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/angular/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/angular/types.ts"],"names":[],"mappings":""}
|
||||||
3
packages/content-suggestions/dist/angular/core/index.d.ts
vendored
Normal file
3
packages/content-suggestions/dist/angular/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type { BaseEntity, ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, TagEntity, } from "./types";
|
||||||
|
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent, } from "./parser";
|
||||||
|
//# sourceMappingURL=index.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/core/index.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/index.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,UAAU,EACV,aAAa,EACb,UAAU,EACV,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,SAAS,GACV,MAAM,SAAS,CAAC;AAEjB,OAAO,EACL,eAAe,EACf,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,UAAU,CAAC"}
|
||||||
2
packages/content-suggestions/dist/angular/core/index.js
vendored
Normal file
2
packages/content-suggestions/dist/angular/core/index.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent, } from "./parser";
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
packages/content-suggestions/dist/angular/core/index.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,eAAe,EACf,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,UAAU,CAAC"}
|
||||||
9
packages/content-suggestions/dist/angular/core/parser.d.ts
vendored
Normal file
9
packages/content-suggestions/dist/angular/core/parser.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, TagEntity } from "./types";
|
||||||
|
export declare const mentionLinkRegexp: RegExp;
|
||||||
|
export declare const parseMention: (mention: string) => ParsedMention | null;
|
||||||
|
export declare const findMentions: (text: string) => MentionEntity[];
|
||||||
|
export declare const findTags: (content: string) => TagEntity[];
|
||||||
|
export declare const findLinks: (content: string) => LinkEntity[];
|
||||||
|
export declare const findAllEntities: (content: string) => ContentEntity[];
|
||||||
|
export declare const processContent: (content: string) => ProcessedContent;
|
||||||
|
//# sourceMappingURL=parser.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/core/parser.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/parser.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/core/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,UAAU,EACV,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,SAAS,EACV,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,iBAAiB,QACgE,CAAC;AAE/F,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,aAAa,GAAG,IAkB9D,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,aAAa,EAkBxD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,KAAG,SAAS,EAiBnD,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,KAAG,UAAU,EAqBrD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,KAAG,aAAa,EAM9D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,gBAYhD,CAAC"}
|
||||||
85
packages/content-suggestions/dist/angular/core/parser.js
vendored
Normal file
85
packages/content-suggestions/dist/angular/core/parser.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export const mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
|
||||||
|
export const parseMention = (mention) => {
|
||||||
|
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
|
||||||
|
const match = mention.match(regex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mentionText = match[1];
|
||||||
|
const mentionId = match[2];
|
||||||
|
if (!mentionText || !mentionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mention: `@${mentionText}`,
|
||||||
|
id: mentionId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const findMentions = (text) => {
|
||||||
|
let match;
|
||||||
|
const matches = [];
|
||||||
|
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||||
|
const parsed = parseMention(match[0]);
|
||||||
|
const baseMention = {
|
||||||
|
start: match.index,
|
||||||
|
end: mentionLinkRegexp.lastIndex,
|
||||||
|
text: match[0],
|
||||||
|
type: "mention",
|
||||||
|
displayText: parsed?.mention ?? match[0],
|
||||||
|
};
|
||||||
|
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
export const findTags = (content) => {
|
||||||
|
const regex = /#[^\s]{1,201}/g;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + value.length,
|
||||||
|
text: value,
|
||||||
|
type: "tag",
|
||||||
|
tag: value.replace("#", ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
export const findLinks = (content) => {
|
||||||
|
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const rawUrl = match[0];
|
||||||
|
const hasProtocol = /^https?:\/\//i.test(rawUrl);
|
||||||
|
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawUrl.length,
|
||||||
|
text: rawUrl,
|
||||||
|
url: fullUrl,
|
||||||
|
type: "link",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
export const findAllEntities = (content) => {
|
||||||
|
const mentions = findMentions(content);
|
||||||
|
const tags = findTags(content);
|
||||||
|
const links = findLinks(content);
|
||||||
|
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
export const processContent = (content) => {
|
||||||
|
const processedText = content.replace(mentionLinkRegexp, match => {
|
||||||
|
const parsed = parseMention(match);
|
||||||
|
return parsed ? parsed.mention : match;
|
||||||
|
});
|
||||||
|
const tags = findTags(content).map(tag => tag.tag);
|
||||||
|
return {
|
||||||
|
processedText,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
//# sourceMappingURL=parser.js.map
|
||||||
1
packages/content-suggestions/dist/angular/core/parser.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/parser.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/core/parser.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,iBAAiB,GAC5B,4FAA4F,CAAC;AAE/F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAe,EAAwB,EAAE;IACpE,MAAM,KAAK,GAAG,8BAA8B,CAAC;IAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,EAAE,CAAC;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,OAAO,EAAE,IAAI,WAAW,EAAE;QAC1B,EAAE,EAAE,SAAS;KACd,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,IAAY,EAAmB,EAAE;IAC5D,IAAI,KAA6B,CAAC;IAClC,MAAM,OAAO,GAAoB,EAAE,CAAC;IAEpC,OAAO,CAAC,KAAK,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,WAAW,GAAkC;YACjD,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,iBAAiB,CAAC,SAAS;YAChC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACd,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,MAAM,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC;SACzC,CAAC;QAEF,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACjF,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAe,EAAE;IACvD,MAAM,KAAK,GAAG,gBAAgB,CAAC;IAC/B,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM;YAC/B,IAAI,EAAE,KAAK;YACX,IAAI,EAAE,KAAK;YACX,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC;SAC5B,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAe,EAAgB,EAAE;IACzD,MAAM,KAAK,GACT,8EAA8E,CAAC;IACjF,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,WAAW,GAAG,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,MAAM,EAAE,CAAC;QAE3D,OAAO,CAAC,IAAI,CAAC;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,GAAG,EAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM;YAChC,IAAI,EAAE,MAAM;YACZ,GAAG,EAAE,OAAO;YACZ,IAAI,EAAE,MAAM;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAmB,EAAE;IAClE,MAAM,QAAQ,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAEjC,OAAO,CAAC,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5E,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAe,EAAoB,EAAE;IAClE,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAAE;QAC/D,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEnD,OAAO;QACL,aAAa;QACb,IAAI;KACL,CAAC;AACJ,CAAC,CAAC"}
|
||||||
28
packages/content-suggestions/dist/angular/core/types.d.ts
vendored
Normal file
28
packages/content-suggestions/dist/angular/core/types.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export interface BaseEntity {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
export interface MentionEntity extends BaseEntity {
|
||||||
|
type: "mention";
|
||||||
|
displayText: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
export interface TagEntity extends BaseEntity {
|
||||||
|
type: "tag";
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
export interface LinkEntity extends BaseEntity {
|
||||||
|
type: "link";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
export type ContentEntity = MentionEntity | TagEntity | LinkEntity;
|
||||||
|
export interface ParsedMention {
|
||||||
|
mention: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
export interface ProcessedContent {
|
||||||
|
processedText: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
//# sourceMappingURL=types.d.ts.map
|
||||||
1
packages/content-suggestions/dist/angular/core/types.d.ts.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/types.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAU,SAAQ,UAAU;IAC3C,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB"}
|
||||||
1
packages/content-suggestions/dist/angular/core/types.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/core/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/types.ts"],"names":[],"mappings":""}
|
||||||
1
packages/content-suggestions/dist/angular/index.d.ts
vendored
Normal file
1
packages/content-suggestions/dist/angular/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./angular/index";
|
||||||
1
packages/content-suggestions/dist/angular/index.js
vendored
Normal file
1
packages/content-suggestions/dist/angular/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./angular/index.js";
|
||||||
97
packages/content-suggestions/dist/core/index.cjs
vendored
Normal file
97
packages/content-suggestions/dist/core/index.cjs
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// src/core/parser.ts
|
||||||
|
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
|
||||||
|
var parseMention = (mention) => {
|
||||||
|
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
|
||||||
|
const match = mention.match(regex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mentionText = match[1];
|
||||||
|
const mentionId = match[2];
|
||||||
|
if (!mentionText || !mentionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mention: `@${mentionText}`,
|
||||||
|
id: mentionId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var findMentions = (text) => {
|
||||||
|
let match;
|
||||||
|
const matches = [];
|
||||||
|
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||||
|
const parsed = parseMention(match[0]);
|
||||||
|
const baseMention = {
|
||||||
|
start: match.index,
|
||||||
|
end: mentionLinkRegexp.lastIndex,
|
||||||
|
text: match[0],
|
||||||
|
type: "mention",
|
||||||
|
displayText: parsed?.mention ?? match[0]
|
||||||
|
};
|
||||||
|
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
var findTags = (content) => {
|
||||||
|
const regex = /#[^\s]{1,201}/g;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + value.length,
|
||||||
|
text: value,
|
||||||
|
type: "tag",
|
||||||
|
tag: value.replace("#", "")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findLinks = (content) => {
|
||||||
|
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const rawUrl = match[0];
|
||||||
|
const hasProtocol = /^https?:\/\//i.test(rawUrl);
|
||||||
|
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawUrl.length,
|
||||||
|
text: rawUrl,
|
||||||
|
url: fullUrl,
|
||||||
|
type: "link"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findAllEntities = (content) => {
|
||||||
|
const mentions = findMentions(content);
|
||||||
|
const tags = findTags(content);
|
||||||
|
const links = findLinks(content);
|
||||||
|
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
var processContent = (content) => {
|
||||||
|
const processedText = content.replace(mentionLinkRegexp, (match) => {
|
||||||
|
const parsed = parseMention(match);
|
||||||
|
return parsed ? parsed.mention : match;
|
||||||
|
});
|
||||||
|
const tags = findTags(content).map((tag) => tag.tag);
|
||||||
|
return {
|
||||||
|
processedText,
|
||||||
|
tags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.findAllEntities = findAllEntities;
|
||||||
|
exports.findLinks = findLinks;
|
||||||
|
exports.findMentions = findMentions;
|
||||||
|
exports.findTags = findTags;
|
||||||
|
exports.mentionLinkRegexp = mentionLinkRegexp;
|
||||||
|
exports.parseMention = parseMention;
|
||||||
|
exports.processContent = processContent;
|
||||||
|
//# sourceMappingURL=index.cjs.map
|
||||||
|
//# sourceMappingURL=index.cjs.map
|
||||||
1
packages/content-suggestions/dist/core/index.cjs.map
vendored
Normal file
1
packages/content-suggestions/dist/core/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
37
packages/content-suggestions/dist/core/index.d.cts
vendored
Normal file
37
packages/content-suggestions/dist/core/index.d.cts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
interface BaseEntity {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
interface MentionEntity extends BaseEntity {
|
||||||
|
type: "mention";
|
||||||
|
displayText: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
interface TagEntity extends BaseEntity {
|
||||||
|
type: "tag";
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
interface LinkEntity extends BaseEntity {
|
||||||
|
type: "link";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
type ContentEntity = MentionEntity | TagEntity | LinkEntity;
|
||||||
|
interface ParsedMention {
|
||||||
|
mention: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
interface ProcessedContent {
|
||||||
|
processedText: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const mentionLinkRegexp: RegExp;
|
||||||
|
declare const parseMention: (mention: string) => ParsedMention | null;
|
||||||
|
declare const findMentions: (text: string) => MentionEntity[];
|
||||||
|
declare const findTags: (content: string) => TagEntity[];
|
||||||
|
declare const findLinks: (content: string) => LinkEntity[];
|
||||||
|
declare const findAllEntities: (content: string) => ContentEntity[];
|
||||||
|
declare const processContent: (content: string) => ProcessedContent;
|
||||||
|
|
||||||
|
export { type BaseEntity, type ContentEntity, type LinkEntity, type MentionEntity, type ParsedMention, type ProcessedContent, type TagEntity, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
|
||||||
37
packages/content-suggestions/dist/core/index.d.ts
vendored
Normal file
37
packages/content-suggestions/dist/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
interface BaseEntity {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
interface MentionEntity extends BaseEntity {
|
||||||
|
type: "mention";
|
||||||
|
displayText: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
interface TagEntity extends BaseEntity {
|
||||||
|
type: "tag";
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
interface LinkEntity extends BaseEntity {
|
||||||
|
type: "link";
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
type ContentEntity = MentionEntity | TagEntity | LinkEntity;
|
||||||
|
interface ParsedMention {
|
||||||
|
mention: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
interface ProcessedContent {
|
||||||
|
processedText: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const mentionLinkRegexp: RegExp;
|
||||||
|
declare const parseMention: (mention: string) => ParsedMention | null;
|
||||||
|
declare const findMentions: (text: string) => MentionEntity[];
|
||||||
|
declare const findTags: (content: string) => TagEntity[];
|
||||||
|
declare const findLinks: (content: string) => LinkEntity[];
|
||||||
|
declare const findAllEntities: (content: string) => ContentEntity[];
|
||||||
|
declare const processContent: (content: string) => ProcessedContent;
|
||||||
|
|
||||||
|
export { type BaseEntity, type ContentEntity, type LinkEntity, type MentionEntity, type ParsedMention, type ProcessedContent, type TagEntity, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
|
||||||
89
packages/content-suggestions/dist/core/index.js
vendored
Normal file
89
packages/content-suggestions/dist/core/index.js
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// src/core/parser.ts
|
||||||
|
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
|
||||||
|
var parseMention = (mention) => {
|
||||||
|
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
|
||||||
|
const match = mention.match(regex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mentionText = match[1];
|
||||||
|
const mentionId = match[2];
|
||||||
|
if (!mentionText || !mentionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mention: `@${mentionText}`,
|
||||||
|
id: mentionId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var findMentions = (text) => {
|
||||||
|
let match;
|
||||||
|
const matches = [];
|
||||||
|
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||||
|
const parsed = parseMention(match[0]);
|
||||||
|
const baseMention = {
|
||||||
|
start: match.index,
|
||||||
|
end: mentionLinkRegexp.lastIndex,
|
||||||
|
text: match[0],
|
||||||
|
type: "mention",
|
||||||
|
displayText: parsed?.mention ?? match[0]
|
||||||
|
};
|
||||||
|
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
var findTags = (content) => {
|
||||||
|
const regex = /#[^\s]{1,201}/g;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + value.length,
|
||||||
|
text: value,
|
||||||
|
type: "tag",
|
||||||
|
tag: value.replace("#", "")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findLinks = (content) => {
|
||||||
|
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const rawUrl = match[0];
|
||||||
|
const hasProtocol = /^https?:\/\//i.test(rawUrl);
|
||||||
|
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawUrl.length,
|
||||||
|
text: rawUrl,
|
||||||
|
url: fullUrl,
|
||||||
|
type: "link"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findAllEntities = (content) => {
|
||||||
|
const mentions = findMentions(content);
|
||||||
|
const tags = findTags(content);
|
||||||
|
const links = findLinks(content);
|
||||||
|
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
var processContent = (content) => {
|
||||||
|
const processedText = content.replace(mentionLinkRegexp, (match) => {
|
||||||
|
const parsed = parseMention(match);
|
||||||
|
return parsed ? parsed.mention : match;
|
||||||
|
});
|
||||||
|
const tags = findTags(content).map((tag) => tag.tag);
|
||||||
|
return {
|
||||||
|
processedText,
|
||||||
|
tags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
packages/content-suggestions/dist/core/index.js.map
vendored
Normal file
1
packages/content-suggestions/dist/core/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
396
packages/content-suggestions/dist/react/index.cjs
vendored
Normal file
396
packages/content-suggestions/dist/react/index.cjs
vendored
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var React = require('react');
|
||||||
|
var react = require('@hublib-web/tach-typography/react');
|
||||||
|
var jsxRuntime = require('react/jsx-runtime');
|
||||||
|
|
||||||
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
||||||
|
|
||||||
|
var React__default = /*#__PURE__*/_interopDefault(React);
|
||||||
|
|
||||||
|
// src/react/components/content-text.tsx
|
||||||
|
|
||||||
|
// src/core/parser.ts
|
||||||
|
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
|
||||||
|
var parseMention = (mention) => {
|
||||||
|
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
|
||||||
|
const match = mention.match(regex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mentionText = match[1];
|
||||||
|
const mentionId = match[2];
|
||||||
|
if (!mentionText || !mentionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mention: `@${mentionText}`,
|
||||||
|
id: mentionId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var findMentions = (text) => {
|
||||||
|
let match;
|
||||||
|
const matches = [];
|
||||||
|
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||||
|
const parsed = parseMention(match[0]);
|
||||||
|
const baseMention = {
|
||||||
|
start: match.index,
|
||||||
|
end: mentionLinkRegexp.lastIndex,
|
||||||
|
text: match[0],
|
||||||
|
type: "mention",
|
||||||
|
displayText: parsed?.mention ?? match[0]
|
||||||
|
};
|
||||||
|
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
var findTags = (content) => {
|
||||||
|
const regex = /#[^\s]{1,201}/g;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + value.length,
|
||||||
|
text: value,
|
||||||
|
type: "tag",
|
||||||
|
tag: value.replace("#", "")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findLinks = (content) => {
|
||||||
|
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const rawUrl = match[0];
|
||||||
|
const hasProtocol = /^https?:\/\//i.test(rawUrl);
|
||||||
|
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawUrl.length,
|
||||||
|
text: rawUrl,
|
||||||
|
url: fullUrl,
|
||||||
|
type: "link"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findAllEntities = (content) => {
|
||||||
|
const mentions = findMentions(content);
|
||||||
|
const tags = findTags(content);
|
||||||
|
const links = findLinks(content);
|
||||||
|
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
var processContent = (content) => {
|
||||||
|
const processedText = content.replace(mentionLinkRegexp, (match) => {
|
||||||
|
const parsed = parseMention(match);
|
||||||
|
return parsed ? parsed.mention : match;
|
||||||
|
});
|
||||||
|
const tags = findTags(content).map((tag) => tag.tag);
|
||||||
|
return {
|
||||||
|
processedText,
|
||||||
|
tags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var joinClassName = (...values) => values.filter(Boolean).join(" ");
|
||||||
|
var baseSelectableStyle = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
WebkitTouchCallout: "default",
|
||||||
|
WebkitUserSelect: "text",
|
||||||
|
KhtmlUserSelect: "text",
|
||||||
|
MozUserSelect: "text",
|
||||||
|
msUserSelect: "text",
|
||||||
|
userSelect: "text"
|
||||||
|
};
|
||||||
|
var blurStyle = {
|
||||||
|
filter: "blur(3px)",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
KhtmlUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "none"
|
||||||
|
};
|
||||||
|
var ReadMoreButton = ({ onClick, symbol = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", expanded = false, TitleComponent }) => /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
TitleComponent,
|
||||||
|
{
|
||||||
|
onClick: (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onClick(event);
|
||||||
|
},
|
||||||
|
weight: "bold",
|
||||||
|
level: 5,
|
||||||
|
children: typeof symbol === "function" ? symbol(expanded) : symbol
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var ContentText = React__default.default.memo(
|
||||||
|
({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
ellipsis = false,
|
||||||
|
blur = false,
|
||||||
|
weight = "normal",
|
||||||
|
style,
|
||||||
|
onView,
|
||||||
|
renderMention,
|
||||||
|
renderTag,
|
||||||
|
renderLink,
|
||||||
|
ParagraphComponent = react.TachTypography.Paragraph.Body,
|
||||||
|
TextComponent = react.TachTypography.Text.Body,
|
||||||
|
TitleComponent = react.TachTypography.Title.Body,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const containerRef = React.useRef(null);
|
||||||
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
|
const [containerInsideView, setContainerInsideView] = React.useState(false);
|
||||||
|
const [viewed, setViewed] = React.useState(false);
|
||||||
|
const content = text || "";
|
||||||
|
const entities = React.useMemo(() => findAllEntities(content), [content]);
|
||||||
|
const wrapWithKey = (node, key) => {
|
||||||
|
if (React__default.default.isValidElement(node)) {
|
||||||
|
return React__default.default.cloneElement(node, { key });
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsx(React__default.default.Fragment, { children: node }, key);
|
||||||
|
};
|
||||||
|
const buildMentionNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(TextComponent, { color: "link", weight, children: entity.displayText });
|
||||||
|
const customNode = renderMention?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildTagNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(TextComponent, { color: "link", weight, children: entity.text });
|
||||||
|
const customNode = renderTag?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildLinkNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
react.TachTypography.Link.Body,
|
||||||
|
{
|
||||||
|
target: "_blank",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
color: "link",
|
||||||
|
weight,
|
||||||
|
href: entity.url,
|
||||||
|
children: entity.text
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const customNode = renderLink?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildParts = (upto = null) => {
|
||||||
|
let lastIndex = 0;
|
||||||
|
const nodes = [];
|
||||||
|
for (const [i, entity] of entities.entries()) {
|
||||||
|
if (upto !== null && entity.start >= upto) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
|
||||||
|
if (entity.start > lastIndex && lastIndex < textEnd) {
|
||||||
|
nodes.push(content.slice(lastIndex, textEnd));
|
||||||
|
}
|
||||||
|
if (upto === null || entity.end <= upto) {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
nodes.push(buildMentionNode(entity, i));
|
||||||
|
} else if (entity.type === "tag") {
|
||||||
|
nodes.push(buildTagNode(entity, i));
|
||||||
|
} else if (entity.type === "link") {
|
||||||
|
nodes.push(buildLinkNode(entity, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = entity.end;
|
||||||
|
}
|
||||||
|
if (upto === null) {
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
nodes.push(content.slice(lastIndex));
|
||||||
|
}
|
||||||
|
} else if (lastIndex < upto) {
|
||||||
|
nodes.push(content.slice(lastIndex, upto));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
|
||||||
|
const expandedFromProps = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
|
||||||
|
const isExpandedControlled = expandedFromProps !== void 0;
|
||||||
|
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
|
||||||
|
const handleExpand = React.useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!isExpandedControlled) {
|
||||||
|
setExpanded(true);
|
||||||
|
}
|
||||||
|
ellipsisConfig?.onExpand?.(event, { expanded: true });
|
||||||
|
},
|
||||||
|
[ellipsisConfig, isExpandedControlled]
|
||||||
|
);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isExpandedControlled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
if (content.length <= count && !expanded) {
|
||||||
|
setExpanded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [content.length, ellipsisConfig, expanded, isExpandedControlled]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (mergedExpanded && !viewed && containerInsideView) {
|
||||||
|
setViewed(true);
|
||||||
|
onView?.();
|
||||||
|
}
|
||||||
|
}, [mergedExpanded, viewed, containerInsideView, onView]);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const ref = containerRef.current;
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.isIntersecting && !containerInsideView) {
|
||||||
|
setContainerInsideView(true);
|
||||||
|
}
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
observer.observe(ref);
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(ref);
|
||||||
|
};
|
||||||
|
}, [containerInsideView]);
|
||||||
|
const mergedStyle = {
|
||||||
|
...baseSelectableStyle,
|
||||||
|
...blur ? blurStyle : void 0,
|
||||||
|
...style
|
||||||
|
};
|
||||||
|
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||||
|
const { count, expandable } = ellipsisConfig;
|
||||||
|
if (!mergedExpanded && count && content.length > count) {
|
||||||
|
let cutoff = count;
|
||||||
|
let extended = true;
|
||||||
|
while (extended) {
|
||||||
|
extended = false;
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start < cutoff && entity.end > cutoff) {
|
||||||
|
cutoff = entity.end;
|
||||||
|
extended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const truncatedNodes = buildParts(cutoff);
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
...props,
|
||||||
|
children: [
|
||||||
|
truncatedNodes,
|
||||||
|
"\u2026",
|
||||||
|
expandable && /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ReadMoreButton,
|
||||||
|
{
|
||||||
|
symbol: ellipsisConfig.symbol,
|
||||||
|
onClick: handleExpand,
|
||||||
|
expanded: mergedExpanded,
|
||||||
|
TitleComponent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ellipsisConfig && "rows" in ellipsisConfig) {
|
||||||
|
const paragraphEllipsis = mergedExpanded ? false : {
|
||||||
|
...ellipsisConfig,
|
||||||
|
symbol: ellipsisConfig.expandable ? /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ReadMoreButton,
|
||||||
|
{
|
||||||
|
symbol: ellipsisConfig.symbol,
|
||||||
|
onClick: handleExpand,
|
||||||
|
expanded: mergedExpanded,
|
||||||
|
TitleComponent
|
||||||
|
}
|
||||||
|
) : ellipsisConfig.symbol
|
||||||
|
};
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
ellipsis: paragraphEllipsis,
|
||||||
|
...props,
|
||||||
|
children: buildParts()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
...props,
|
||||||
|
children: buildParts()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ContentText.displayName = "ContentText";
|
||||||
|
var DefaultMention = ({ entity }) => /* @__PURE__ */ jsxRuntime.jsx(react.TachTypography.Text.Body, { color: "link", children: entity.displayText });
|
||||||
|
var DefaultTag = ({ entity }) => /* @__PURE__ */ jsxRuntime.jsx(react.TachTypography.Text.Body, { color: "link", children: entity.text });
|
||||||
|
var ContentTextWithSuggestions = ({
|
||||||
|
renderMention,
|
||||||
|
renderTag,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ContentText,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
renderMention: (entity, index) => renderMention ? renderMention(entity, index) : /* @__PURE__ */ jsxRuntime.jsx(DefaultMention, { entity }),
|
||||||
|
renderTag: (entity, index) => renderTag ? renderTag(entity, index) : /* @__PURE__ */ jsxRuntime.jsx(DefaultTag, { entity })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
var ContentTitleWithSuggestions = ({
|
||||||
|
text,
|
||||||
|
ellipsis,
|
||||||
|
blur = false,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const normalizedEllipsis = ellipsis === void 0 ? { rows: 2 } : ellipsis;
|
||||||
|
const textProps = text === void 0 ? {} : { text };
|
||||||
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
||||||
|
ContentTextWithSuggestions,
|
||||||
|
{
|
||||||
|
weight: "bold",
|
||||||
|
blur,
|
||||||
|
ellipsis: normalizedEllipsis,
|
||||||
|
...textProps,
|
||||||
|
...rest
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.ContentText = ContentText;
|
||||||
|
exports.ContentTextWithSuggestions = ContentTextWithSuggestions;
|
||||||
|
exports.ContentTitleWithSuggestions = ContentTitleWithSuggestions;
|
||||||
|
exports.findAllEntities = findAllEntities;
|
||||||
|
exports.findLinks = findLinks;
|
||||||
|
exports.findMentions = findMentions;
|
||||||
|
exports.findTags = findTags;
|
||||||
|
exports.mentionLinkRegexp = mentionLinkRegexp;
|
||||||
|
exports.parseMention = parseMention;
|
||||||
|
exports.processContent = processContent;
|
||||||
|
//# sourceMappingURL=index.cjs.map
|
||||||
|
//# sourceMappingURL=index.cjs.map
|
||||||
1
packages/content-suggestions/dist/react/index.cjs.map
vendored
Normal file
1
packages/content-suggestions/dist/react/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
46
packages/content-suggestions/dist/react/index.d.cts
vendored
Normal file
46
packages/content-suggestions/dist/react/index.d.cts
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { EllipsisConfig } from 'antd/lib/typography/Base';
|
||||||
|
import { MentionEntity, TagEntity, LinkEntity } from '../core/index.cjs';
|
||||||
|
export { BaseEntity, ContentEntity, ParsedMention, ProcessedContent, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.cjs';
|
||||||
|
import { TachTypography } from '@hublib-web/tach-typography/react';
|
||||||
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
||||||
|
|
||||||
|
type CustomEllipsisConfig = ({
|
||||||
|
count: number;
|
||||||
|
rows?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
} & Partial<EllipsisConfig>) | ({
|
||||||
|
rows: number;
|
||||||
|
count?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
} & Partial<EllipsisConfig>) | false;
|
||||||
|
type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
|
||||||
|
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
|
||||||
|
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;
|
||||||
|
interface ContentTextProps {
|
||||||
|
className?: string;
|
||||||
|
weight?: TextBodyProps["weight"];
|
||||||
|
text?: string | null;
|
||||||
|
ellipsis?: CustomEllipsisConfig;
|
||||||
|
blur?: boolean;
|
||||||
|
style?: TextBodyProps["style"];
|
||||||
|
onView?: () => void;
|
||||||
|
renderMention?: (entity: MentionEntity, index: number) => ReactNode;
|
||||||
|
renderTag?: (entity: TagEntity, index: number) => ReactNode;
|
||||||
|
renderLink?: (entity: LinkEntity, index: number) => ReactNode;
|
||||||
|
ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
|
||||||
|
TextComponent?: React.ComponentType<TextBodyProps>;
|
||||||
|
TitleComponent?: React.ComponentType<TitleBodyProps>;
|
||||||
|
}
|
||||||
|
declare const ContentText: React.NamedExoticComponent<ContentTextProps>;
|
||||||
|
|
||||||
|
type BaseContentTextProps = Omit<ComponentProps<typeof ContentText>, "renderMention" | "renderTag"> & {
|
||||||
|
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
|
||||||
|
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
|
||||||
|
};
|
||||||
|
declare const ContentTextWithSuggestions: ({ renderMention, renderTag, ...props }: BaseContentTextProps) => react_jsx_runtime.JSX.Element;
|
||||||
|
|
||||||
|
type ContentTitleWithSuggestionsProps = Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight">;
|
||||||
|
declare const ContentTitleWithSuggestions: ({ text, ellipsis, blur, ...rest }: ContentTitleWithSuggestionsProps) => react_jsx_runtime.JSX.Element;
|
||||||
|
|
||||||
|
export { ContentText, type ContentTextProps, ContentTextWithSuggestions, ContentTitleWithSuggestions, LinkEntity, MentionEntity, TagEntity };
|
||||||
46
packages/content-suggestions/dist/react/index.d.ts
vendored
Normal file
46
packages/content-suggestions/dist/react/index.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { EllipsisConfig } from 'antd/lib/typography/Base';
|
||||||
|
import { MentionEntity, TagEntity, LinkEntity } from '../core/index.js';
|
||||||
|
export { BaseEntity, ContentEntity, ParsedMention, ProcessedContent, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.js';
|
||||||
|
import { TachTypography } from '@hublib-web/tach-typography/react';
|
||||||
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
||||||
|
|
||||||
|
type CustomEllipsisConfig = ({
|
||||||
|
count: number;
|
||||||
|
rows?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
} & Partial<EllipsisConfig>) | ({
|
||||||
|
rows: number;
|
||||||
|
count?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
} & Partial<EllipsisConfig>) | false;
|
||||||
|
type ParagraphBodyProps = ComponentProps<typeof TachTypography.Paragraph.Body>;
|
||||||
|
type TextBodyProps = ComponentProps<typeof TachTypography.Text.Body>;
|
||||||
|
type TitleBodyProps = ComponentProps<typeof TachTypography.Title.Body>;
|
||||||
|
interface ContentTextProps {
|
||||||
|
className?: string;
|
||||||
|
weight?: TextBodyProps["weight"];
|
||||||
|
text?: string | null;
|
||||||
|
ellipsis?: CustomEllipsisConfig;
|
||||||
|
blur?: boolean;
|
||||||
|
style?: TextBodyProps["style"];
|
||||||
|
onView?: () => void;
|
||||||
|
renderMention?: (entity: MentionEntity, index: number) => ReactNode;
|
||||||
|
renderTag?: (entity: TagEntity, index: number) => ReactNode;
|
||||||
|
renderLink?: (entity: LinkEntity, index: number) => ReactNode;
|
||||||
|
ParagraphComponent?: React.ComponentType<ParagraphBodyProps>;
|
||||||
|
TextComponent?: React.ComponentType<TextBodyProps>;
|
||||||
|
TitleComponent?: React.ComponentType<TitleBodyProps>;
|
||||||
|
}
|
||||||
|
declare const ContentText: React.NamedExoticComponent<ContentTextProps>;
|
||||||
|
|
||||||
|
type BaseContentTextProps = Omit<ComponentProps<typeof ContentText>, "renderMention" | "renderTag"> & {
|
||||||
|
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
|
||||||
|
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
|
||||||
|
};
|
||||||
|
declare const ContentTextWithSuggestions: ({ renderMention, renderTag, ...props }: BaseContentTextProps) => react_jsx_runtime.JSX.Element;
|
||||||
|
|
||||||
|
type ContentTitleWithSuggestionsProps = Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight">;
|
||||||
|
declare const ContentTitleWithSuggestions: ({ text, ellipsis, blur, ...rest }: ContentTitleWithSuggestionsProps) => react_jsx_runtime.JSX.Element;
|
||||||
|
|
||||||
|
export { ContentText, type ContentTextProps, ContentTextWithSuggestions, ContentTitleWithSuggestions, LinkEntity, MentionEntity, TagEntity };
|
||||||
381
packages/content-suggestions/dist/react/index.js
vendored
Normal file
381
packages/content-suggestions/dist/react/index.js
vendored
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import React, { useRef, useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
|
import { TachTypography } from '@hublib-web/tach-typography/react';
|
||||||
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
||||||
|
|
||||||
|
// src/react/components/content-text.tsx
|
||||||
|
|
||||||
|
// src/core/parser.ts
|
||||||
|
var mentionLinkRegexp = /@\[[^\]]+]\([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\)/g;
|
||||||
|
var parseMention = (mention) => {
|
||||||
|
const regex = /@\[([^\]]+)\]\(([\w-]{36})\)/;
|
||||||
|
const match = mention.match(regex);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mentionText = match[1];
|
||||||
|
const mentionId = match[2];
|
||||||
|
if (!mentionText || !mentionId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mention: `@${mentionText}`,
|
||||||
|
id: mentionId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var findMentions = (text) => {
|
||||||
|
let match;
|
||||||
|
const matches = [];
|
||||||
|
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||||
|
const parsed = parseMention(match[0]);
|
||||||
|
const baseMention = {
|
||||||
|
start: match.index,
|
||||||
|
end: mentionLinkRegexp.lastIndex,
|
||||||
|
text: match[0],
|
||||||
|
type: "mention",
|
||||||
|
displayText: parsed?.mention ?? match[0]
|
||||||
|
};
|
||||||
|
matches.push(parsed?.id ? { ...baseMention, userId: parsed.id } : baseMention);
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
var findTags = (content) => {
|
||||||
|
const regex = /#[^\s]{1,201}/g;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const value = match[0];
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + value.length,
|
||||||
|
text: value,
|
||||||
|
type: "tag",
|
||||||
|
tag: value.replace("#", "")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findLinks = (content) => {
|
||||||
|
const regex = /\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||||
|
const results = [];
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(content)) !== null) {
|
||||||
|
const rawUrl = match[0];
|
||||||
|
const hasProtocol = /^https?:\/\//i.test(rawUrl);
|
||||||
|
const fullUrl = hasProtocol ? rawUrl : `https://${rawUrl}`;
|
||||||
|
results.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawUrl.length,
|
||||||
|
text: rawUrl,
|
||||||
|
url: fullUrl,
|
||||||
|
type: "link"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
var findAllEntities = (content) => {
|
||||||
|
const mentions = findMentions(content);
|
||||||
|
const tags = findTags(content);
|
||||||
|
const links = findLinks(content);
|
||||||
|
return [...mentions, ...tags, ...links].sort((a, b) => a.start - b.start);
|
||||||
|
};
|
||||||
|
var processContent = (content) => {
|
||||||
|
const processedText = content.replace(mentionLinkRegexp, (match) => {
|
||||||
|
const parsed = parseMention(match);
|
||||||
|
return parsed ? parsed.mention : match;
|
||||||
|
});
|
||||||
|
const tags = findTags(content).map((tag) => tag.tag);
|
||||||
|
return {
|
||||||
|
processedText,
|
||||||
|
tags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var joinClassName = (...values) => values.filter(Boolean).join(" ");
|
||||||
|
var baseSelectableStyle = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
WebkitTouchCallout: "default",
|
||||||
|
WebkitUserSelect: "text",
|
||||||
|
KhtmlUserSelect: "text",
|
||||||
|
MozUserSelect: "text",
|
||||||
|
msUserSelect: "text",
|
||||||
|
userSelect: "text"
|
||||||
|
};
|
||||||
|
var blurStyle = {
|
||||||
|
filter: "blur(3px)",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
KhtmlUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "none"
|
||||||
|
};
|
||||||
|
var ReadMoreButton = ({ onClick, symbol = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", expanded = false, TitleComponent }) => /* @__PURE__ */ jsx(
|
||||||
|
TitleComponent,
|
||||||
|
{
|
||||||
|
onClick: (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onClick(event);
|
||||||
|
},
|
||||||
|
weight: "bold",
|
||||||
|
level: 5,
|
||||||
|
children: typeof symbol === "function" ? symbol(expanded) : symbol
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var ContentText = React.memo(
|
||||||
|
({
|
||||||
|
text,
|
||||||
|
className,
|
||||||
|
ellipsis = false,
|
||||||
|
blur = false,
|
||||||
|
weight = "normal",
|
||||||
|
style,
|
||||||
|
onView,
|
||||||
|
renderMention,
|
||||||
|
renderTag,
|
||||||
|
renderLink,
|
||||||
|
ParagraphComponent = TachTypography.Paragraph.Body,
|
||||||
|
TextComponent = TachTypography.Text.Body,
|
||||||
|
TitleComponent = TachTypography.Title.Body,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const [containerInsideView, setContainerInsideView] = useState(false);
|
||||||
|
const [viewed, setViewed] = useState(false);
|
||||||
|
const content = text || "";
|
||||||
|
const entities = useMemo(() => findAllEntities(content), [content]);
|
||||||
|
const wrapWithKey = (node, key) => {
|
||||||
|
if (React.isValidElement(node)) {
|
||||||
|
return React.cloneElement(node, { key });
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ jsx(React.Fragment, { children: node }, key);
|
||||||
|
};
|
||||||
|
const buildMentionNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsx(TextComponent, { color: "link", weight, children: entity.displayText });
|
||||||
|
const customNode = renderMention?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildTagNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsx(TextComponent, { color: "link", weight, children: entity.text });
|
||||||
|
const customNode = renderTag?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildLinkNode = (entity, index) => {
|
||||||
|
const defaultNode = /* @__PURE__ */ jsx(
|
||||||
|
TachTypography.Link.Body,
|
||||||
|
{
|
||||||
|
target: "_blank",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
color: "link",
|
||||||
|
weight,
|
||||||
|
href: entity.url,
|
||||||
|
children: entity.text
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const customNode = renderLink?.(entity, index) ?? defaultNode;
|
||||||
|
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
|
||||||
|
};
|
||||||
|
const buildParts = (upto = null) => {
|
||||||
|
let lastIndex = 0;
|
||||||
|
const nodes = [];
|
||||||
|
for (const [i, entity] of entities.entries()) {
|
||||||
|
if (upto !== null && entity.start >= upto) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
|
||||||
|
if (entity.start > lastIndex && lastIndex < textEnd) {
|
||||||
|
nodes.push(content.slice(lastIndex, textEnd));
|
||||||
|
}
|
||||||
|
if (upto === null || entity.end <= upto) {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
nodes.push(buildMentionNode(entity, i));
|
||||||
|
} else if (entity.type === "tag") {
|
||||||
|
nodes.push(buildTagNode(entity, i));
|
||||||
|
} else if (entity.type === "link") {
|
||||||
|
nodes.push(buildLinkNode(entity, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = entity.end;
|
||||||
|
}
|
||||||
|
if (upto === null) {
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
nodes.push(content.slice(lastIndex));
|
||||||
|
}
|
||||||
|
} else if (lastIndex < upto) {
|
||||||
|
nodes.push(content.slice(lastIndex, upto));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
const ellipsisConfig = ellipsis && typeof ellipsis === "object" ? ellipsis : null;
|
||||||
|
const expandedFromProps = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
|
||||||
|
const isExpandedControlled = expandedFromProps !== void 0;
|
||||||
|
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
|
||||||
|
const handleExpand = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!isExpandedControlled) {
|
||||||
|
setExpanded(true);
|
||||||
|
}
|
||||||
|
ellipsisConfig?.onExpand?.(event, { expanded: true });
|
||||||
|
},
|
||||||
|
[ellipsisConfig, isExpandedControlled]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExpandedControlled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
if (content.length <= count && !expanded) {
|
||||||
|
setExpanded(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [content.length, ellipsisConfig, expanded, isExpandedControlled]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (mergedExpanded && !viewed && containerInsideView) {
|
||||||
|
setViewed(true);
|
||||||
|
onView?.();
|
||||||
|
}
|
||||||
|
}, [mergedExpanded, viewed, containerInsideView, onView]);
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = containerRef.current;
|
||||||
|
if (!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entry.isIntersecting && !containerInsideView) {
|
||||||
|
setContainerInsideView(true);
|
||||||
|
}
|
||||||
|
}, { threshold: 0.5 });
|
||||||
|
observer.observe(ref);
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(ref);
|
||||||
|
};
|
||||||
|
}, [containerInsideView]);
|
||||||
|
const mergedStyle = {
|
||||||
|
...baseSelectableStyle,
|
||||||
|
...blur ? blurStyle : void 0,
|
||||||
|
...style
|
||||||
|
};
|
||||||
|
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||||
|
const { count, expandable } = ellipsisConfig;
|
||||||
|
if (!mergedExpanded && count && content.length > count) {
|
||||||
|
let cutoff = count;
|
||||||
|
let extended = true;
|
||||||
|
while (extended) {
|
||||||
|
extended = false;
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start < cutoff && entity.end > cutoff) {
|
||||||
|
cutoff = entity.end;
|
||||||
|
extended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const truncatedNodes = buildParts(cutoff);
|
||||||
|
return /* @__PURE__ */ jsxs(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
...props,
|
||||||
|
children: [
|
||||||
|
truncatedNodes,
|
||||||
|
"\u2026",
|
||||||
|
expandable && /* @__PURE__ */ jsx(
|
||||||
|
ReadMoreButton,
|
||||||
|
{
|
||||||
|
symbol: ellipsisConfig.symbol,
|
||||||
|
onClick: handleExpand,
|
||||||
|
expanded: mergedExpanded,
|
||||||
|
TitleComponent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ellipsisConfig && "rows" in ellipsisConfig) {
|
||||||
|
const paragraphEllipsis = mergedExpanded ? false : {
|
||||||
|
...ellipsisConfig,
|
||||||
|
symbol: ellipsisConfig.expandable ? /* @__PURE__ */ jsx(
|
||||||
|
ReadMoreButton,
|
||||||
|
{
|
||||||
|
symbol: ellipsisConfig.symbol,
|
||||||
|
onClick: handleExpand,
|
||||||
|
expanded: mergedExpanded,
|
||||||
|
TitleComponent
|
||||||
|
}
|
||||||
|
) : ellipsisConfig.symbol
|
||||||
|
};
|
||||||
|
return /* @__PURE__ */ jsx(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
ellipsis: paragraphEllipsis,
|
||||||
|
...props,
|
||||||
|
children: buildParts()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return /* @__PURE__ */ jsx(
|
||||||
|
ParagraphComponent,
|
||||||
|
{
|
||||||
|
ref: containerRef,
|
||||||
|
weight,
|
||||||
|
className: joinClassName(className),
|
||||||
|
style: mergedStyle,
|
||||||
|
...props,
|
||||||
|
children: buildParts()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ContentText.displayName = "ContentText";
|
||||||
|
var DefaultMention = ({ entity }) => /* @__PURE__ */ jsx(TachTypography.Text.Body, { color: "link", children: entity.displayText });
|
||||||
|
var DefaultTag = ({ entity }) => /* @__PURE__ */ jsx(TachTypography.Text.Body, { color: "link", children: entity.text });
|
||||||
|
var ContentTextWithSuggestions = ({
|
||||||
|
renderMention,
|
||||||
|
renderTag,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return /* @__PURE__ */ jsx(
|
||||||
|
ContentText,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
renderMention: (entity, index) => renderMention ? renderMention(entity, index) : /* @__PURE__ */ jsx(DefaultMention, { entity }),
|
||||||
|
renderTag: (entity, index) => renderTag ? renderTag(entity, index) : /* @__PURE__ */ jsx(DefaultTag, { entity })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
var ContentTitleWithSuggestions = ({
|
||||||
|
text,
|
||||||
|
ellipsis,
|
||||||
|
blur = false,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const normalizedEllipsis = ellipsis === void 0 ? { rows: 2 } : ellipsis;
|
||||||
|
const textProps = text === void 0 ? {} : { text };
|
||||||
|
return /* @__PURE__ */ jsx(
|
||||||
|
ContentTextWithSuggestions,
|
||||||
|
{
|
||||||
|
weight: "bold",
|
||||||
|
blur,
|
||||||
|
ellipsis: normalizedEllipsis,
|
||||||
|
...textProps,
|
||||||
|
...rest
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ContentText, ContentTextWithSuggestions, ContentTitleWithSuggestions, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
|
//# sourceMappingURL=index.js.map
|
||||||
1
packages/content-suggestions/dist/react/index.js.map
vendored
Normal file
1
packages/content-suggestions/dist/react/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
107
packages/content-suggestions/package.json
Normal file
107
packages/content-suggestions/package.json
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"name": "@hublib-web/content-suggestions",
|
||||||
|
"version": "0.2.1",
|
||||||
|
"description": "Content text/title with mentions, tags and links for React and Angular",
|
||||||
|
"license": "MIT",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/core/index.cjs",
|
||||||
|
"module": "./dist/core/index.js",
|
||||||
|
"types": "./dist/core/index.d.ts",
|
||||||
|
"sideEffects": false,
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"typesVersions": {
|
||||||
|
"*": {
|
||||||
|
"react": [
|
||||||
|
"dist/react/index.d.ts"
|
||||||
|
],
|
||||||
|
"angular": [
|
||||||
|
"dist/angular/index.d.ts"
|
||||||
|
],
|
||||||
|
"core": [
|
||||||
|
"dist/core/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/core/index.d.ts",
|
||||||
|
"import": "./dist/core/index.js",
|
||||||
|
"require": "./dist/core/index.cjs"
|
||||||
|
},
|
||||||
|
"./core": {
|
||||||
|
"types": "./dist/core/index.d.ts",
|
||||||
|
"import": "./dist/core/index.js",
|
||||||
|
"require": "./dist/core/index.cjs"
|
||||||
|
},
|
||||||
|
"./react": {
|
||||||
|
"types": "./dist/react/index.d.ts",
|
||||||
|
"import": "./dist/react/index.js",
|
||||||
|
"require": "./dist/react/index.cjs"
|
||||||
|
},
|
||||||
|
"./angular": {
|
||||||
|
"types": "./dist/angular/index.d.ts",
|
||||||
|
"import": "./dist/angular/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn clean && tsup && yarn build:angular",
|
||||||
|
"build:angular": "ngc -p tsconfig.angular.json && node ./scripts/fix-angular-entry.mjs",
|
||||||
|
"clean": "rm -rf dist storybook-static",
|
||||||
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
|
"test": "yarn run -T vitest run --passWithNoTests",
|
||||||
|
"lint": "eslint src --ext .ts,.tsx",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=17.0.0",
|
||||||
|
"@angular/core": ">=17.0.0",
|
||||||
|
"@hublib-web/tach-typography": "0.3.1",
|
||||||
|
"antd": ">=5.0.0",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"react-dom": ">=18.0.0",
|
||||||
|
"rxjs": ">=7.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@angular/common": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@angular/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"antd": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rxjs": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular/common": "^20.3.17",
|
||||||
|
"@angular/compiler": "^20.3.17",
|
||||||
|
"@angular/compiler-cli": "^20.3.17",
|
||||||
|
"@angular/core": "^20.3.17",
|
||||||
|
"@hublib-web/tach-typography": "workspace:0.3.1",
|
||||||
|
"@storybook/addon-essentials": "8.6.14",
|
||||||
|
"@storybook/react": "8.6.14",
|
||||||
|
"@storybook/react-vite": "8.6.14",
|
||||||
|
"@types/react": "^19.2.2",
|
||||||
|
"antd": "^5.29.3",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"rxjs": "^7.8.2",
|
||||||
|
"storybook": "8.6.14",
|
||||||
|
"tsup": "^8.5.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "6.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/content-suggestions/scripts/fix-angular-entry.mjs
Normal file
12
packages/content-suggestions/scripts/fix-angular-entry.mjs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { access, writeFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const distAngularDir = path.resolve("dist/angular");
|
||||||
|
await access(path.join(distAngularDir, "angular/index.js"));
|
||||||
|
await access(path.join(distAngularDir, "angular/index.d.ts"));
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(distAngularDir, "index.js"),
|
||||||
|
'export * from "./angular/index.js";\n',
|
||||||
|
);
|
||||||
|
await writeFile(path.join(distAngularDir, "index.d.ts"), 'export * from "./angular/index";\n');
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type {
|
||||||
|
AngularContentEllipsisConfig,
|
||||||
|
AngularLinkRenderer,
|
||||||
|
AngularMentionRenderer,
|
||||||
|
AngularTagRenderer,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
|
||||||
|
import { ContentTextComponent } from "./content-text.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "content-text-with-suggestions",
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextComponent],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<content-text
|
||||||
|
[className]="className"
|
||||||
|
[weight]="weight"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="ellipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ContentTextWithSuggestionsComponent {
|
||||||
|
@Input() className: string | undefined;
|
||||||
|
@Input() weight: TypographyWeight = "normal";
|
||||||
|
@Input() text: string | null | undefined;
|
||||||
|
@Input() ellipsis: AngularContentEllipsisConfig = false;
|
||||||
|
@Input() blur = false;
|
||||||
|
@Input() style: Record<string, string | number> | null | undefined;
|
||||||
|
@Input() onView: (() => void) | undefined;
|
||||||
|
@Input() renderMention: AngularMentionRenderer | undefined;
|
||||||
|
@Input() renderTag: AngularTagRenderer | undefined;
|
||||||
|
@Input() renderLink: AngularLinkRenderer | undefined;
|
||||||
|
|
||||||
|
@Output() readonly viewed = new EventEmitter<void>();
|
||||||
|
@Output() readonly expandedChange = new EventEmitter<boolean>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
import type { LinkEntity, MentionEntity, TagEntity } from "../core";
|
||||||
|
import type {
|
||||||
|
AngularContentEllipsisConfig,
|
||||||
|
AngularCountEllipsisConfig,
|
||||||
|
AngularLinkRenderer,
|
||||||
|
AngularRenderResult,
|
||||||
|
AngularRowsEllipsisConfig,
|
||||||
|
AngularTagRenderer,
|
||||||
|
AngularMentionRenderer,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import { NgFor, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault } from "@angular/common";
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
Output,
|
||||||
|
SimpleChanges,
|
||||||
|
ViewChild,
|
||||||
|
} from "@angular/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TachTypographyComponent,
|
||||||
|
type TachTypographyHostProps,
|
||||||
|
} from "@hublib-web/tach-typography/angular";
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
|
||||||
|
import { findAllEntities } from "../core";
|
||||||
|
import { ContentSuggestionsRenderResultDirective } from "./render-result.directive";
|
||||||
|
|
||||||
|
const READ_MORE_TEXT = "Читать полностью";
|
||||||
|
const BODY_DATA_ATTR = "data-content-suggestions-body";
|
||||||
|
|
||||||
|
const BASE_SELECTABLE_STYLE: Record<string, string> = {
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
WebkitTouchCallout: "default",
|
||||||
|
WebkitUserSelect: "text",
|
||||||
|
KhtmlUserSelect: "text",
|
||||||
|
MozUserSelect: "text",
|
||||||
|
msUserSelect: "text",
|
||||||
|
userSelect: "text",
|
||||||
|
};
|
||||||
|
|
||||||
|
const BLUR_STYLE: Record<string, string> = {
|
||||||
|
filter: "blur(3px)",
|
||||||
|
WebkitUserSelect: "none",
|
||||||
|
KhtmlUserSelect: "none",
|
||||||
|
MozUserSelect: "none",
|
||||||
|
msUserSelect: "none",
|
||||||
|
userSelect: "none",
|
||||||
|
pointerEvents: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
type TextPart = {
|
||||||
|
kind: "text";
|
||||||
|
key: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MentionPart = {
|
||||||
|
kind: "mention";
|
||||||
|
key: string;
|
||||||
|
entity: MentionEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TagPart = {
|
||||||
|
kind: "tag";
|
||||||
|
key: string;
|
||||||
|
entity: TagEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LinkPart = {
|
||||||
|
kind: "link";
|
||||||
|
key: string;
|
||||||
|
entity: LinkEntity;
|
||||||
|
index: number;
|
||||||
|
customRender: AngularRenderResult | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentPart = TextPart | MentionPart | TagPart | LinkPart;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "content-text",
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
NgFor,
|
||||||
|
NgIf,
|
||||||
|
NgSwitch,
|
||||||
|
NgSwitchCase,
|
||||||
|
NgSwitchDefault,
|
||||||
|
TachTypographyComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<div #container>
|
||||||
|
<tach-typography
|
||||||
|
as="p"
|
||||||
|
variant="Body"
|
||||||
|
[weight]="weight"
|
||||||
|
[className]="className"
|
||||||
|
[preserveStyle]="mergedStyle"
|
||||||
|
[ellipsis]="rowsEllipsis"
|
||||||
|
[hostProps]="paragraphHostProps"
|
||||||
|
>
|
||||||
|
<ng-container *ngFor="let part of visibleParts; trackBy: trackByKey">
|
||||||
|
<ng-container [ngSwitch]="part.kind">
|
||||||
|
<ng-container *ngSwitchCase="'text'">{{ part.text }}</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'mention'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultMention">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultMention>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.displayText }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'tag'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultTag">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultTag>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" [weight]="weight">
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="'link'">
|
||||||
|
<ng-container *ngIf="part.customRender !== null; else contentSuggestionsDefaultLink">
|
||||||
|
<span [contentSuggestionsRenderResult]="part.customRender"></span>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #contentSuggestionsDefaultLink>
|
||||||
|
<tach-typography
|
||||||
|
as="a"
|
||||||
|
variant="Body"
|
||||||
|
color="link"
|
||||||
|
[weight]="weight"
|
||||||
|
[hostProps]="buildDefaultLinkHostProps(part.entity.url)"
|
||||||
|
>
|
||||||
|
{{ part.entity.text }}
|
||||||
|
</tach-typography>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchDefault></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="showTrailingEllipsis">…</ng-container>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showInlineReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-left: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</tach-typography>
|
||||||
|
|
||||||
|
<button
|
||||||
|
*ngIf="showBlockReadMore"
|
||||||
|
type="button"
|
||||||
|
(click)="handleExpand($event)"
|
||||||
|
style="border: 0; background: none; padding: 0; margin-top: 6px; cursor: pointer;"
|
||||||
|
>
|
||||||
|
<tach-typography as="span" variant="Body" color="link" weight="bold">
|
||||||
|
{{ readMoreLabel }}
|
||||||
|
</tach-typography>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ContentTextComponent implements OnChanges, AfterViewInit, OnDestroy {
|
||||||
|
@Input() className: string | undefined;
|
||||||
|
@Input() weight: TypographyWeight = "normal";
|
||||||
|
@Input() text: string | null | undefined;
|
||||||
|
@Input() ellipsis: AngularContentEllipsisConfig = false;
|
||||||
|
@Input() blur = false;
|
||||||
|
@Input() style: Record<string, string | number> | null | undefined;
|
||||||
|
@Input() onView: (() => void) | undefined;
|
||||||
|
@Input() renderMention: AngularMentionRenderer | undefined;
|
||||||
|
@Input() renderTag: AngularTagRenderer | undefined;
|
||||||
|
@Input() renderLink: AngularLinkRenderer | undefined;
|
||||||
|
|
||||||
|
@Output() readonly viewed = new EventEmitter<void>();
|
||||||
|
@Output() readonly expandedChange = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
@ViewChild("container", { static: true })
|
||||||
|
private readonly containerRef!: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
|
visibleParts: ContentPart[] = [];
|
||||||
|
mergedStyle: Record<string, string | number> = { ...BASE_SELECTABLE_STYLE };
|
||||||
|
rowsEllipsis: { rows: number } | undefined;
|
||||||
|
showTrailingEllipsis = false;
|
||||||
|
showInlineReadMore = false;
|
||||||
|
showBlockReadMore = false;
|
||||||
|
readMoreLabel = READ_MORE_TEXT;
|
||||||
|
paragraphHostProps: TachTypographyHostProps = { [BODY_DATA_ATTR]: "true" };
|
||||||
|
|
||||||
|
private expanded = false;
|
||||||
|
private mergedExpanded = false;
|
||||||
|
private isExpandedControlled = false;
|
||||||
|
private isInsideView = false;
|
||||||
|
private viewedFired = false;
|
||||||
|
private pendingRowsOverflowCheck = false;
|
||||||
|
private intersectionObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly cdr: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnChanges(_changes: SimpleChanges): void {
|
||||||
|
this.recomputeContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit(): void {
|
||||||
|
this.initObserver();
|
||||||
|
this.scheduleRowsOverflowCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.intersectionObserver) {
|
||||||
|
this.intersectionObserver.disconnect();
|
||||||
|
this.intersectionObserver = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByKey(_index: number, part: ContentPart): string {
|
||||||
|
return part.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDefaultLinkHostProps(url: string): TachTypographyHostProps {
|
||||||
|
return {
|
||||||
|
href: url,
|
||||||
|
target: "_blank",
|
||||||
|
referrerPolicy: "no-referrer",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExpand(event: MouseEvent): void {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
if (!this.isExpandedControlled) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyExpand(event, true);
|
||||||
|
this.recomputeContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private recomputeContent(): void {
|
||||||
|
const content = this.text ?? "";
|
||||||
|
const entities = findAllEntities(content);
|
||||||
|
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||||
|
|
||||||
|
this.isExpandedControlled = Boolean(
|
||||||
|
ellipsisConfig && typeof ellipsisConfig.expanded === "boolean",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.isExpandedControlled && this.isCountConfig(ellipsisConfig)) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
if (content.length <= count && !this.expanded) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mergedExpanded = this.getMergedExpanded(ellipsisConfig, content);
|
||||||
|
this.readMoreLabel = this.resolveEllipsisSymbol(
|
||||||
|
ellipsisConfig?.symbol,
|
||||||
|
this.mergedExpanded,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.mergedStyle = {
|
||||||
|
...BASE_SELECTABLE_STYLE,
|
||||||
|
...(this.blur ? BLUR_STYLE : {}),
|
||||||
|
...(this.style ?? {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.visibleParts = this.buildParts(content, entities, null);
|
||||||
|
this.rowsEllipsis = undefined;
|
||||||
|
this.showTrailingEllipsis = false;
|
||||||
|
this.showInlineReadMore = false;
|
||||||
|
this.showBlockReadMore = false;
|
||||||
|
this.pendingRowsOverflowCheck = false;
|
||||||
|
|
||||||
|
if (this.isCountConfig(ellipsisConfig)) {
|
||||||
|
const count = ellipsisConfig.count ?? 0;
|
||||||
|
const shouldTruncate = !this.mergedExpanded && count > 0 && content.length > count;
|
||||||
|
|
||||||
|
if (shouldTruncate) {
|
||||||
|
const cutoff = this.computeEntitySafeCutoff(count, entities);
|
||||||
|
this.visibleParts = this.buildParts(content, entities, cutoff);
|
||||||
|
this.showTrailingEllipsis = true;
|
||||||
|
this.showInlineReadMore = Boolean(ellipsisConfig.expandable);
|
||||||
|
}
|
||||||
|
} else if (this.isRowsConfig(ellipsisConfig) && !this.mergedExpanded) {
|
||||||
|
this.rowsEllipsis = {
|
||||||
|
rows: ellipsisConfig.rows,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ellipsisConfig.expandable) {
|
||||||
|
this.pendingRowsOverflowCheck = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
this.scheduleRowsOverflowCheck();
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildParts(
|
||||||
|
content: string,
|
||||||
|
entities: ReturnType<typeof findAllEntities>,
|
||||||
|
upto: number | null,
|
||||||
|
): ContentPart[] {
|
||||||
|
const parts: ContentPart[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
for (const [index, entity] of entities.entries()) {
|
||||||
|
if (upto !== null && entity.start >= upto) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textEnd = upto !== null ? Math.min(entity.start, upto) : entity.start;
|
||||||
|
if (entity.start > lastIndex && lastIndex < textEnd) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${textEnd}`,
|
||||||
|
text: content.slice(lastIndex, textEnd),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upto === null || entity.end <= upto) {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
parts.push({
|
||||||
|
kind: "mention",
|
||||||
|
key: `mention-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderMention, entity, index),
|
||||||
|
});
|
||||||
|
} else if (entity.type === "tag") {
|
||||||
|
parts.push({
|
||||||
|
kind: "tag",
|
||||||
|
key: `tag-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderTag, entity, index),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
parts.push({
|
||||||
|
kind: "link",
|
||||||
|
key: `link-${entity.start}-${index}`,
|
||||||
|
entity,
|
||||||
|
index,
|
||||||
|
customRender: this.resolveCustomRender(this.renderLink, entity, index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = entity.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upto === null) {
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${content.length}`,
|
||||||
|
text: content.slice(lastIndex),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (lastIndex < upto) {
|
||||||
|
parts.push({
|
||||||
|
kind: "text",
|
||||||
|
key: `text-${lastIndex}-${upto}`,
|
||||||
|
text: content.slice(lastIndex, upto),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCustomRender<T>(
|
||||||
|
rendererFn: ((entity: T, index: number) => AngularRenderResult) | undefined,
|
||||||
|
entity: T,
|
||||||
|
index: number,
|
||||||
|
): AngularRenderResult | null {
|
||||||
|
const rendered = rendererFn?.(entity, index);
|
||||||
|
return rendered ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveEllipsisConfig(): AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null {
|
||||||
|
if (!this.ellipsis || typeof this.ellipsis !== "object") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isCountConfig(
|
||||||
|
config: AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null,
|
||||||
|
): config is AngularCountEllipsisConfig {
|
||||||
|
return Boolean(config && "count" in config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isRowsConfig(
|
||||||
|
config: AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null,
|
||||||
|
): config is AngularRowsEllipsisConfig {
|
||||||
|
return Boolean(config && "rows" in config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMergedExpanded(
|
||||||
|
config: AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null,
|
||||||
|
text: string,
|
||||||
|
): boolean {
|
||||||
|
const controlledExpanded =
|
||||||
|
config && typeof config.expanded === "boolean"
|
||||||
|
? config.expanded
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (controlledExpanded !== undefined) {
|
||||||
|
return controlledExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isCountConfig(config)) {
|
||||||
|
const count = config.count ?? 0;
|
||||||
|
if (count <= 0 || text.length <= count) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveEllipsisSymbol(
|
||||||
|
symbol: AngularCountEllipsisConfig["symbol"] | AngularRowsEllipsisConfig["symbol"],
|
||||||
|
expanded: boolean,
|
||||||
|
): string {
|
||||||
|
if (typeof symbol === "function") {
|
||||||
|
return symbol(expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbol ?? READ_MORE_TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private notifyExpand(event: MouseEvent, expanded: boolean): void {
|
||||||
|
const config = this.resolveEllipsisConfig();
|
||||||
|
const onExpand = config?.onExpand;
|
||||||
|
|
||||||
|
if (onExpand) {
|
||||||
|
if (onExpand.length >= 2) {
|
||||||
|
(onExpand as (e: MouseEvent, info: { expanded: boolean }) => void)(event, { expanded });
|
||||||
|
} else {
|
||||||
|
(onExpand as (nextExpanded: boolean) => void)(expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expandedChange.emit(expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private computeEntitySafeCutoff(
|
||||||
|
count: number,
|
||||||
|
entities: ReturnType<typeof findAllEntities>,
|
||||||
|
): number {
|
||||||
|
let cutoff = count;
|
||||||
|
let extended = true;
|
||||||
|
|
||||||
|
while (extended) {
|
||||||
|
extended = false;
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start < cutoff && entity.end > cutoff) {
|
||||||
|
cutoff = entity.end;
|
||||||
|
extended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cutoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initObserver(): void {
|
||||||
|
const host = this.containerRef?.nativeElement;
|
||||||
|
if (!host) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver === "undefined") {
|
||||||
|
this.isInsideView = true;
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
const entry = entries[0];
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isIntersecting && !this.isInsideView) {
|
||||||
|
this.isInsideView = true;
|
||||||
|
this.emitViewIfNeeded();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitViewIfNeeded(): void {
|
||||||
|
if (!this.mergedExpanded || this.viewedFired || !this.isInsideView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.viewedFired = true;
|
||||||
|
this.onView?.();
|
||||||
|
this.viewed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleRowsOverflowCheck(): void {
|
||||||
|
if (!this.pendingRowsOverflowCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
this.refreshRowsOverflow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshRowsOverflow(): void {
|
||||||
|
const paragraph = this.getParagraphElement();
|
||||||
|
if (!paragraph) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOverflow = paragraph.scrollHeight - paragraph.clientHeight > 1;
|
||||||
|
if (this.showBlockReadMore !== hasOverflow) {
|
||||||
|
this.showBlockReadMore = hasOverflow;
|
||||||
|
this.cdr.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getParagraphElement(): HTMLElement | null {
|
||||||
|
const host = this.containerRef?.nativeElement;
|
||||||
|
if (!host) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return host.querySelector(`[${BODY_DATA_ATTR}="true"]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type {
|
||||||
|
AngularContentEllipsisConfig,
|
||||||
|
AngularLinkRenderer,
|
||||||
|
AngularMentionRenderer,
|
||||||
|
AngularRowsEllipsisConfig,
|
||||||
|
AngularTagRenderer,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from "@angular/core";
|
||||||
|
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggestions.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "content-title-with-suggestions",
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextWithSuggestionsComponent],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: `
|
||||||
|
<content-text-with-suggestions
|
||||||
|
[className]="className"
|
||||||
|
weight="bold"
|
||||||
|
[text]="text"
|
||||||
|
[ellipsis]="normalizedEllipsis"
|
||||||
|
[blur]="blur"
|
||||||
|
[style]="style"
|
||||||
|
[onView]="onView"
|
||||||
|
[renderMention]="renderMention"
|
||||||
|
[renderTag]="renderTag"
|
||||||
|
[renderLink]="renderLink"
|
||||||
|
(viewed)="viewed.emit()"
|
||||||
|
(expandedChange)="expandedChange.emit($event)"
|
||||||
|
></content-text-with-suggestions>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class ContentTitleWithSuggestionsComponent {
|
||||||
|
@Input() className: string | undefined;
|
||||||
|
@Input() text: string | null | undefined;
|
||||||
|
@Input() ellipsis: AngularContentEllipsisConfig | undefined;
|
||||||
|
@Input() blur = false;
|
||||||
|
@Input() style: Record<string, string | number> | null | undefined;
|
||||||
|
@Input() onView: (() => void) | undefined;
|
||||||
|
@Input() renderMention: AngularMentionRenderer | undefined;
|
||||||
|
@Input() renderTag: AngularTagRenderer | undefined;
|
||||||
|
@Input() renderLink: AngularLinkRenderer | undefined;
|
||||||
|
|
||||||
|
@Output() readonly viewed = new EventEmitter<void>();
|
||||||
|
@Output() readonly expandedChange = new EventEmitter<boolean>();
|
||||||
|
|
||||||
|
get normalizedEllipsis(): AngularContentEllipsisConfig {
|
||||||
|
if (this.ellipsis !== undefined) {
|
||||||
|
return this.ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rows: 2 } satisfies AngularRowsEllipsisConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
97
packages/content-suggestions/src/angular/index.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// @vitest-environment jsdom
|
||||||
|
|
||||||
|
import "@angular/compiler";
|
||||||
|
|
||||||
|
import { Component, provideZonelessChangeDetection } from "@angular/core";
|
||||||
|
import { TestBed, getTestBed } from "@angular/core/testing";
|
||||||
|
import { BrowserTestingModule, platformBrowserTesting } from "@angular/platform-browser/testing";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
} from "../../dist/angular/index.js";
|
||||||
|
|
||||||
|
const ensureAngularTestEnvironment = (): void => {
|
||||||
|
try {
|
||||||
|
getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting());
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (!message.includes("Cannot set base providers")) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureAngularTestEnvironment();
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [ContentTextWithSuggestionsComponent, ContentTitleWithSuggestionsComponent],
|
||||||
|
template: `
|
||||||
|
<content-text-with-suggestions [text]="text"></content-text-with-suggestions>
|
||||||
|
<content-title-with-suggestions [text]="title"></content-title-with-suggestions>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
class ContentSuggestionsHostComponent {
|
||||||
|
text =
|
||||||
|
"Привет @[John Doe](d290f1ee-6c54-4b01-90e6-d701748f0851) #frontend смотри example.com";
|
||||||
|
title = "Заголовок @[Jane Roe](123e4567-e89b-12d3-a456-426614174000) #news docs.example.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("content-suggestions (angular)", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
TestBed.resetTestingModule();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders text with mention, tag and link tokens", async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const host: HTMLElement = fixture.nativeElement;
|
||||||
|
const content = host.querySelector("content-text-with-suggestions");
|
||||||
|
expect(content).not.toBeNull();
|
||||||
|
|
||||||
|
const text = content?.textContent ?? "";
|
||||||
|
expect(text).toContain("Привет");
|
||||||
|
expect(text).toContain("@John Doe");
|
||||||
|
expect(text).toContain("#frontend");
|
||||||
|
expect(text).toContain("example.com");
|
||||||
|
|
||||||
|
const link = content?.querySelector('a.tach-typography[href="https://example.com"]');
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.textContent).toContain("example.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders title variant and keeps entities visible", async () => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [provideZonelessChangeDetection()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(ContentSuggestionsHostComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
|
||||||
|
const host: HTMLElement = fixture.nativeElement;
|
||||||
|
const title = host.querySelector("content-title-with-suggestions");
|
||||||
|
expect(title).not.toBeNull();
|
||||||
|
|
||||||
|
const text = title?.textContent ?? "";
|
||||||
|
expect(text).toContain("Заголовок");
|
||||||
|
expect(text).toContain("@Jane Roe");
|
||||||
|
expect(text).toContain("#news");
|
||||||
|
expect(text).toContain("docs.example.com");
|
||||||
|
|
||||||
|
const boldNode = title?.querySelector(".tach-typography--bold");
|
||||||
|
expect(boldNode).not.toBeNull();
|
||||||
|
|
||||||
|
const link = title?.querySelector('a.tach-typography[href="https://docs.example.com"]');
|
||||||
|
expect(link).not.toBeNull();
|
||||||
|
expect(link?.textContent).toContain("docs.example.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
57
packages/content-suggestions/src/angular/index.ts
Normal file
57
packages/content-suggestions/src/angular/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ContentTextComponent } from "./content-text.component";
|
||||||
|
import { ContentTextWithSuggestionsComponent } from "./content-text-with-suggestions.component";
|
||||||
|
import { ContentTitleWithSuggestionsComponent } from "./content-title-with-suggestions.component";
|
||||||
|
import { ContentSuggestionsRenderResultDirective } from "./render-result.directive";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AngularContentEllipsisConfig,
|
||||||
|
AngularContentSnapshot,
|
||||||
|
AngularContentTextProps,
|
||||||
|
AngularContentTextWithSuggestionsProps,
|
||||||
|
AngularContentTitleWithSuggestionsProps,
|
||||||
|
AngularContentToken,
|
||||||
|
AngularCountEllipsisConfig,
|
||||||
|
AngularEllipsisExpandHandler,
|
||||||
|
AngularEllipsisSymbol,
|
||||||
|
AngularExpandInfo,
|
||||||
|
AngularLinkRenderer,
|
||||||
|
AngularLinkToken,
|
||||||
|
AngularMentionRenderer,
|
||||||
|
AngularMentionToken,
|
||||||
|
AngularRenderResult,
|
||||||
|
AngularRowsEllipsisConfig,
|
||||||
|
AngularTagRenderer,
|
||||||
|
AngularTagToken,
|
||||||
|
AngularTextToken,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export {
|
||||||
|
AngularContentSuggestionsAdapter,
|
||||||
|
buildAngularTagHref,
|
||||||
|
createAngularContentTokens,
|
||||||
|
} from "./tokens";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ContentTextComponent,
|
||||||
|
ContentTextWithSuggestionsComponent,
|
||||||
|
ContentTitleWithSuggestionsComponent,
|
||||||
|
ContentSuggestionsRenderResultDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ContentSuggestionsAngularModule {}
|
||||||
1
packages/content-suggestions/src/angular/public-api.ts
Normal file
1
packages/content-suggestions/src/angular/public-api.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./index";
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { AngularRenderResult } from "./types";
|
||||||
|
|
||||||
|
import { Directive, ElementRef, Input, OnChanges, Renderer2 } from "@angular/core";
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: "[contentSuggestionsRenderResult]",
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class ContentSuggestionsRenderResultDirective implements OnChanges {
|
||||||
|
@Input() contentSuggestionsRenderResult: AngularRenderResult;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly elementRef: ElementRef<HTMLElement>,
|
||||||
|
private readonly renderer: Renderer2,
|
||||||
|
) {
|
||||||
|
this.renderer.setStyle(this.elementRef.nativeElement, "display", "contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.clearHost();
|
||||||
|
|
||||||
|
const value = this.contentSuggestionsRenderResult;
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Node) {
|
||||||
|
this.renderer.appendChild(this.elementRef.nativeElement, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textNode = this.renderer.createText(String(value));
|
||||||
|
this.renderer.appendChild(this.elementRef.nativeElement, textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearHost(): void {
|
||||||
|
const host = this.elementRef.nativeElement;
|
||||||
|
|
||||||
|
while (host.firstChild) {
|
||||||
|
host.removeChild(host.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
83
packages/content-suggestions/src/angular/tokens.ts
Normal file
83
packages/content-suggestions/src/angular/tokens.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type {
|
||||||
|
AngularContentSnapshot,
|
||||||
|
AngularContentToken,
|
||||||
|
AngularLinkToken,
|
||||||
|
AngularMentionToken,
|
||||||
|
AngularTagToken,
|
||||||
|
AngularTextToken,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import type { ContentEntity, TagEntity } from "../core";
|
||||||
|
|
||||||
|
import { findAllEntities } from "../core";
|
||||||
|
|
||||||
|
const mapEntityToToken = (entity: ContentEntity): AngularMentionToken | AngularTagToken | AngularLinkToken => {
|
||||||
|
if (entity.type === "mention") {
|
||||||
|
return {
|
||||||
|
kind: "mention",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.type === "tag") {
|
||||||
|
return {
|
||||||
|
kind: "tag",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "link",
|
||||||
|
entity,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTextToken = (text: string, start: number, end: number): AngularTextToken => ({
|
||||||
|
kind: "text",
|
||||||
|
text,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createAngularContentTokens = (
|
||||||
|
inputText: string | null | undefined,
|
||||||
|
): AngularContentToken[] => {
|
||||||
|
const text = inputText ?? "";
|
||||||
|
const entities = findAllEntities(text);
|
||||||
|
|
||||||
|
let cursor = 0;
|
||||||
|
const tokens: AngularContentToken[] = [];
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (entity.start > cursor) {
|
||||||
|
tokens.push(createTextToken(text.slice(cursor, entity.start), cursor, entity.start));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(mapEntityToToken(entity));
|
||||||
|
cursor = entity.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor < text.length) {
|
||||||
|
tokens.push(createTextToken(text.slice(cursor), cursor, text.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildAngularTagHref = (entity: TagEntity): string => {
|
||||||
|
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AngularContentSuggestionsAdapter {
|
||||||
|
snapshot(inputText: string | null | undefined): AngularContentSnapshot {
|
||||||
|
const text = inputText ?? "";
|
||||||
|
const entities = findAllEntities(text);
|
||||||
|
const tokens = createAngularContentTokens(text);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
entities,
|
||||||
|
tokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
113
packages/content-suggestions/src/angular/types.ts
Normal file
113
packages/content-suggestions/src/angular/types.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import type { ContentEntity, LinkEntity, MentionEntity, TagEntity } from "../core";
|
||||||
|
|
||||||
|
import type { TypographyWeight } from "@hublib-web/tach-typography/core";
|
||||||
|
|
||||||
|
export interface AngularTextToken {
|
||||||
|
kind: "text";
|
||||||
|
text: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AngularMentionToken {
|
||||||
|
kind: "mention";
|
||||||
|
entity: MentionEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AngularTagToken {
|
||||||
|
kind: "tag";
|
||||||
|
entity: TagEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AngularLinkToken {
|
||||||
|
kind: "link";
|
||||||
|
entity: LinkEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AngularContentToken =
|
||||||
|
| AngularTextToken
|
||||||
|
| AngularMentionToken
|
||||||
|
| AngularTagToken
|
||||||
|
| AngularLinkToken;
|
||||||
|
|
||||||
|
export interface AngularContentSnapshot {
|
||||||
|
text: string;
|
||||||
|
entities: ContentEntity[];
|
||||||
|
tokens: AngularContentToken[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AngularExpandInfo {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
|
||||||
|
|
||||||
|
export type AngularEllipsisExpandHandler =
|
||||||
|
| ((expanded: boolean) => void)
|
||||||
|
| ((event: MouseEvent, info: AngularExpandInfo) => void);
|
||||||
|
|
||||||
|
export type AngularCountEllipsisConfig = {
|
||||||
|
count: number;
|
||||||
|
rows?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
symbol?: AngularEllipsisSymbol;
|
||||||
|
onExpand?: AngularEllipsisExpandHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AngularRowsEllipsisConfig = {
|
||||||
|
rows: number;
|
||||||
|
count?: never;
|
||||||
|
expandable?: boolean;
|
||||||
|
expanded?: boolean;
|
||||||
|
symbol?: AngularEllipsisSymbol;
|
||||||
|
onExpand?: AngularEllipsisExpandHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AngularContentEllipsisConfig =
|
||||||
|
| AngularCountEllipsisConfig
|
||||||
|
| AngularRowsEllipsisConfig
|
||||||
|
| false;
|
||||||
|
|
||||||
|
export type AngularRenderResult = Node | string | number | null | undefined;
|
||||||
|
|
||||||
|
export type AngularMentionRenderer = (
|
||||||
|
entity: MentionEntity,
|
||||||
|
index: number,
|
||||||
|
) => AngularRenderResult;
|
||||||
|
|
||||||
|
export type AngularTagRenderer = (
|
||||||
|
entity: TagEntity,
|
||||||
|
index: number,
|
||||||
|
) => AngularRenderResult;
|
||||||
|
|
||||||
|
export type AngularLinkRenderer = (
|
||||||
|
entity: LinkEntity,
|
||||||
|
index: number,
|
||||||
|
) => AngularRenderResult;
|
||||||
|
|
||||||
|
export interface AngularContentTextProps {
|
||||||
|
className?: string;
|
||||||
|
weight?: TypographyWeight;
|
||||||
|
text?: string | null;
|
||||||
|
ellipsis?: AngularContentEllipsisConfig;
|
||||||
|
blur?: boolean;
|
||||||
|
style?: Record<string, string | number> | null;
|
||||||
|
onView?: () => void;
|
||||||
|
renderMention?: AngularMentionRenderer;
|
||||||
|
renderTag?: AngularTagRenderer;
|
||||||
|
renderLink?: AngularLinkRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AngularContentTextWithSuggestionsProps = Omit<
|
||||||
|
AngularContentTextProps,
|
||||||
|
"renderMention" | "renderTag"
|
||||||
|
> & {
|
||||||
|
renderMention?: AngularMentionRenderer;
|
||||||
|
renderTag?: AngularTagRenderer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AngularContentTitleWithSuggestionsProps = Omit<
|
||||||
|
AngularContentTextWithSuggestionsProps,
|
||||||
|
"weight"
|
||||||
|
>;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user