Compare commits
10 Commits
v0.0.1
...
tach-typog
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
*.log
|
||||
storybook-static
|
||||
**/node_modules
|
||||
**/.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`
|
||||
- `@tach/video-player/react`
|
||||
- `@tach/video-player/angular`
|
||||
External projects install these packages directly from Git tags. Package `dist/` artifacts must be committed.
|
||||
|
||||
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/`:
|
||||
- `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.
|
||||
## Package READMEs
|
||||
|
||||
## 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`).
|
||||
2. Copy everything from this folder to the new repo root.
|
||||
3. In the new repo run:
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
corepack enable
|
||||
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
|
||||
git add .
|
||||
git commit -m "feat: initial @tach/video-player package"
|
||||
git push origin main
|
||||
yarn add "@hublib-web/<package>@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/<package>&tag=<package>-vX.Y.Z"
|
||||
```
|
||||
|
||||
5. Create a release tag:
|
||||
## Releases
|
||||
|
||||
```bash
|
||||
git tag v0.1.0
|
||||
git push origin v0.1.0
|
||||
```
|
||||
Releases are done with Git tags (no Changesets, no npm publish).
|
||||
|
||||
## Build Output
|
||||
Short flow:
|
||||
|
||||
`npm run build` compiles TypeScript into `dist/` and copies required assets (`css/scss/svg/...`).
|
||||
`dist/` must be committed before creating a new version tag.
|
||||
1. Bump version in `packages/<package>/package.json`.
|
||||
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`:
|
||||
|
||||
- `@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.
|
||||
Detailed policy and examples: [docs/release-policy.md](./docs/release-policy.md).
|
||||
|
||||
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",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/react/index.js",
|
||||
"types": "./dist/react/index.d.ts",
|
||||
"sideEffects": true,
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
"name": "@hublib-web/root",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node ./scripts/build.mjs",
|
||||
"typecheck": "tsc -p ./tsconfig.build.json --noEmit",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build",
|
||||
"prepack": "npm run build",
|
||||
"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
|
||||
}
|
||||
"build": "yarn workspaces foreach -A -p --topological-dev run build",
|
||||
"typecheck": "yarn workspaces foreach -A -p --topological-dev run typecheck",
|
||||
"test": "yarn workspaces foreach -A -p --topological-dev run test",
|
||||
"clean": "yarn workspaces foreach -A -p --topological-dev run clean",
|
||||
"lint": "yarn workspaces foreach -A -p --topological-dev run lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/react": "8.6.14",
|
||||
"@storybook/react-vite": "8.6.14",
|
||||
"@types/node": "22.12.0",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sass": "1.83.4",
|
||||
"@types/node": "^24.6.1",
|
||||
"eslint": "^9.37.0",
|
||||
"prettier": "^3.6.2",
|
||||
"storybook": "8.6.14",
|
||||
"typescript": "5.9.2",
|
||||
"vite": "5.4.14"
|
||||
"typescript": "^5.9.3",
|
||||
"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;
|
||||
126
packages/content-suggestions/README.md
Normal file
126
packages/content-suggestions/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# @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 adapter in `angular` for rendering/tokenization integration.
|
||||
- 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` is a peer dependency, so install a compatible tag for it as well.
|
||||
|
||||
## 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
|
||||
|
||||
Tokenization helper:
|
||||
|
||||
```ts
|
||||
import { createAngularContentTokens } from "@hublib-web/content-suggestions/angular";
|
||||
|
||||
const tokens = createAngularContentTokens(text);
|
||||
```
|
||||
|
||||
Ready-to-use UI renderer (React-like props API):
|
||||
|
||||
```ts
|
||||
import { AngularContentTextWithSuggestionsRenderer } from "@hublib-web/content-suggestions/angular";
|
||||
|
||||
const renderer = new AngularContentTextWithSuggestionsRenderer();
|
||||
renderer.attach(containerElement, {
|
||||
text,
|
||||
weight: "normal",
|
||||
ellipsis: { count: 180, expandable: true },
|
||||
blur: false,
|
||||
onView: () => {
|
||||
// analytics
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Title variant with defaults (`weight: "bold"`, `ellipsis: { rows: 2 }`):
|
||||
|
||||
```ts
|
||||
import { AngularContentTitleWithSuggestionsRenderer } from "@hublib-web/content-suggestions/angular";
|
||||
|
||||
const titleRenderer = new AngularContentTitleWithSuggestionsRenderer();
|
||||
titleRenderer.attach(titleContainer, { text: title });
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
495
packages/content-suggestions/dist/angular/index.cjs
vendored
Normal file
495
packages/content-suggestions/dist/angular/index.cjs
vendored
Normal file
@@ -0,0 +1,495 @@
|
||||
'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);
|
||||
};
|
||||
|
||||
// src/angular/index.ts
|
||||
var LINK_COLOR = "#1677ff";
|
||||
var READ_MORE_TEXT = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E";
|
||||
var toKebabCase = (value) => value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
||||
var applyStyleObject = (element, styles) => {
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
for (const [key, rawValue] of Object.entries(styles)) {
|
||||
if (rawValue === null || rawValue === void 0) {
|
||||
continue;
|
||||
}
|
||||
const styleValue = typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
|
||||
if (key.startsWith("--")) {
|
||||
element.style.setProperty(key, styleValue);
|
||||
continue;
|
||||
}
|
||||
const cssKey = key.includes("-") ? key : toKebabCase(key);
|
||||
element.style.setProperty(cssKey, styleValue);
|
||||
}
|
||||
};
|
||||
var toNode = (value) => {
|
||||
if (value === null || value === void 0) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Node) {
|
||||
return value;
|
||||
}
|
||||
return document.createTextNode(String(value));
|
||||
};
|
||||
var resolveEllipsisSymbol = (symbol, expanded) => {
|
||||
if (typeof symbol === "function") {
|
||||
return symbol(expanded);
|
||||
}
|
||||
return symbol ?? READ_MORE_TEXT;
|
||||
};
|
||||
var buildAngularTagHref = (entity) => {
|
||||
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
|
||||
};
|
||||
var 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({
|
||||
kind: "text",
|
||||
text: text.slice(cursor, entity.start),
|
||||
start: cursor,
|
||||
end: entity.start
|
||||
});
|
||||
}
|
||||
if (entity.type === "mention") {
|
||||
tokens.push({
|
||||
kind: "mention",
|
||||
entity
|
||||
});
|
||||
} else if (entity.type === "tag") {
|
||||
tokens.push({
|
||||
kind: "tag",
|
||||
entity
|
||||
});
|
||||
} else {
|
||||
tokens.push({
|
||||
kind: "link",
|
||||
entity
|
||||
});
|
||||
}
|
||||
cursor = entity.end;
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
tokens.push({
|
||||
kind: "text",
|
||||
text: text.slice(cursor),
|
||||
start: cursor,
|
||||
end: text.length
|
||||
});
|
||||
}
|
||||
return tokens;
|
||||
};
|
||||
var AngularContentSuggestionsAdapter = class {
|
||||
snapshot(inputText) {
|
||||
const text = inputText ?? "";
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
};
|
||||
var AngularContentTextRenderer = class {
|
||||
host = null;
|
||||
props = {};
|
||||
expanded = false;
|
||||
viewed = false;
|
||||
hostInsideView = false;
|
||||
observer = null;
|
||||
attach(host, props = {}) {
|
||||
this.destroy();
|
||||
this.host = host;
|
||||
this.props = { ...props };
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
this.initObserver();
|
||||
this.render();
|
||||
return this.getState();
|
||||
}
|
||||
update(nextProps) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...nextProps
|
||||
};
|
||||
this.render();
|
||||
return this.getState();
|
||||
}
|
||||
destroy() {
|
||||
if (this.observer && this.host) {
|
||||
this.observer.unobserve(this.host);
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.observer = null;
|
||||
if (this.host) {
|
||||
this.host.innerHTML = "";
|
||||
}
|
||||
this.host = null;
|
||||
this.props = {};
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
}
|
||||
getState() {
|
||||
return {
|
||||
props: { ...this.props },
|
||||
snapshot: this.createSnapshot(),
|
||||
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText())
|
||||
};
|
||||
}
|
||||
getText() {
|
||||
return this.props.text ?? "";
|
||||
}
|
||||
createSnapshot() {
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
resolveEllipsisConfig() {
|
||||
const ellipsis = this.props.ellipsis;
|
||||
if (!ellipsis || typeof ellipsis !== "object") {
|
||||
return null;
|
||||
}
|
||||
return ellipsis;
|
||||
}
|
||||
getMergedExpanded(ellipsisConfig, text) {
|
||||
const controlledExpanded = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
|
||||
if (controlledExpanded !== void 0) {
|
||||
return controlledExpanded;
|
||||
}
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
if (count <= 0 || text.length <= count) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.expanded;
|
||||
}
|
||||
createDefaultMentionNode(entity) {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.displayText;
|
||||
return span;
|
||||
}
|
||||
createDefaultTagNode(entity) {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.text;
|
||||
return span;
|
||||
}
|
||||
createDefaultLinkNode(entity) {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = entity.url;
|
||||
anchor.target = "_blank";
|
||||
anchor.referrerPolicy = "no-referrer";
|
||||
anchor.style.color = LINK_COLOR;
|
||||
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
anchor.textContent = entity.text;
|
||||
return anchor;
|
||||
}
|
||||
renderEntity(entity, index) {
|
||||
if (entity.type === "mention") {
|
||||
const customNode2 = this.props.renderMention?.(entity, index);
|
||||
return toNode(customNode2) ?? this.createDefaultMentionNode(entity);
|
||||
}
|
||||
if (entity.type === "tag") {
|
||||
const customNode2 = this.props.renderTag?.(entity, index);
|
||||
return toNode(customNode2) ?? this.createDefaultTagNode(entity);
|
||||
}
|
||||
const customNode = this.props.renderLink?.(entity, index);
|
||||
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
|
||||
}
|
||||
buildTextNodes(text, entities, upto = null) {
|
||||
let lastIndex = 0;
|
||||
const nodes = [];
|
||||
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) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, textEnd)));
|
||||
}
|
||||
if (upto === null || entity.end <= upto) {
|
||||
const entityNode = this.renderEntity(entity, index);
|
||||
if (entityNode) {
|
||||
nodes.push(entityNode);
|
||||
}
|
||||
}
|
||||
lastIndex = entity.end;
|
||||
}
|
||||
if (upto === null) {
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex)));
|
||||
}
|
||||
} else if (lastIndex < upto) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
applyBaseParagraphStyle(element) {
|
||||
element.style.whiteSpace = "pre-wrap";
|
||||
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
element.style.setProperty("-webkit-touch-callout", "default");
|
||||
element.style.setProperty("-webkit-user-select", "text");
|
||||
element.style.setProperty("-khtml-user-select", "text");
|
||||
element.style.setProperty("-moz-user-select", "text");
|
||||
element.style.setProperty("-ms-user-select", "text");
|
||||
element.style.setProperty("user-select", "text");
|
||||
if (this.props.blur) {
|
||||
element.style.filter = "blur(3px)";
|
||||
element.style.setProperty("-webkit-user-select", "none");
|
||||
element.style.setProperty("-khtml-user-select", "none");
|
||||
element.style.setProperty("-moz-user-select", "none");
|
||||
element.style.setProperty("-ms-user-select", "none");
|
||||
element.style.setProperty("user-select", "none");
|
||||
element.style.pointerEvents = "none";
|
||||
}
|
||||
applyStyleObject(element, this.props.style);
|
||||
}
|
||||
buildReadMoreButton(symbol) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
|
||||
button.style.border = "0";
|
||||
button.style.background = "none";
|
||||
button.style.padding = "0";
|
||||
button.style.marginLeft = "6px";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.color = LINK_COLOR;
|
||||
button.style.fontWeight = "700";
|
||||
button.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.handleExpand();
|
||||
};
|
||||
return button;
|
||||
}
|
||||
handleExpand() {
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
if (!ellipsisConfig) {
|
||||
return;
|
||||
}
|
||||
this.expanded = true;
|
||||
ellipsisConfig.onExpand?.(true);
|
||||
this.render();
|
||||
}
|
||||
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;
|
||||
}
|
||||
hasVerticalOverflow(element) {
|
||||
return element.scrollHeight - element.clientHeight > 1;
|
||||
}
|
||||
render() {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
|
||||
const wrapper = document.createElement("div");
|
||||
const paragraph = document.createElement("div");
|
||||
if (this.props.className) {
|
||||
paragraph.className = this.props.className;
|
||||
}
|
||||
this.applyBaseParagraphStyle(paragraph);
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
|
||||
if (shouldTruncate) {
|
||||
const cutoff = this.computeEntitySafeCutoff(count, entities);
|
||||
const nodes = this.buildTextNodes(text, entities, cutoff);
|
||||
paragraph.append(...nodes);
|
||||
paragraph.append(document.createTextNode("\u2026"));
|
||||
if (ellipsisConfig.expandable) {
|
||||
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
|
||||
paragraph.style.display = "-webkit-box";
|
||||
paragraph.style.setProperty("-webkit-box-orient", "vertical");
|
||||
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
|
||||
paragraph.style.overflow = "hidden";
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
|
||||
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
initObserver() {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
this.hostInsideView = true;
|
||||
return;
|
||||
}
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (entry.isIntersecting && !this.hostInsideView) {
|
||||
this.hostInsideView = true;
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
this.observer.observe(this.host);
|
||||
}
|
||||
emitOnViewIfNeeded(mergedExpanded) {
|
||||
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
|
||||
return;
|
||||
}
|
||||
this.viewed = true;
|
||||
this.props.onView?.();
|
||||
}
|
||||
};
|
||||
var AngularContentTextWithSuggestionsRenderer = class extends AngularContentTextRenderer {
|
||||
};
|
||||
var AngularContentTitleWithSuggestionsRenderer = class {
|
||||
renderer = new AngularContentTextWithSuggestionsRenderer();
|
||||
attach(host, props = {}) {
|
||||
return this.renderer.attach(host, this.normalizeProps(props));
|
||||
}
|
||||
update(props) {
|
||||
return this.renderer.update(this.normalizeProps(props));
|
||||
}
|
||||
destroy() {
|
||||
this.renderer.destroy();
|
||||
}
|
||||
getState() {
|
||||
return this.renderer.getState();
|
||||
}
|
||||
normalizeProps(props) {
|
||||
return {
|
||||
...props,
|
||||
weight: "bold",
|
||||
blur: props.blur ?? false,
|
||||
ellipsis: props.ellipsis === void 0 ? { rows: 2 } : props.ellipsis
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.AngularContentSuggestionsAdapter = AngularContentSuggestionsAdapter;
|
||||
exports.AngularContentTextRenderer = AngularContentTextRenderer;
|
||||
exports.AngularContentTextWithSuggestionsRenderer = AngularContentTextWithSuggestionsRenderer;
|
||||
exports.AngularContentTitleWithSuggestionsRenderer = AngularContentTitleWithSuggestionsRenderer;
|
||||
exports.buildAngularTagHref = buildAngularTagHref;
|
||||
exports.createAngularContentTokens = createAngularContentTokens;
|
||||
exports.toKebabCase = toKebabCase;
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
1
packages/content-suggestions/dist/angular/index.cjs.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
118
packages/content-suggestions/dist/angular/index.d.cts
vendored
Normal file
118
packages/content-suggestions/dist/angular/index.d.cts
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import { C as ContentEntity, M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.cjs';
|
||||
|
||||
interface AngularTextToken {
|
||||
kind: "text";
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
interface AngularMentionToken {
|
||||
kind: "mention";
|
||||
entity: MentionEntity;
|
||||
}
|
||||
interface AngularTagToken {
|
||||
kind: "tag";
|
||||
entity: TagEntity;
|
||||
}
|
||||
interface AngularLinkToken {
|
||||
kind: "link";
|
||||
entity: LinkEntity;
|
||||
}
|
||||
type AngularContentToken = AngularTextToken | AngularMentionToken | AngularTagToken | AngularLinkToken;
|
||||
interface AngularContentSnapshot {
|
||||
text: string;
|
||||
entities: ContentEntity[];
|
||||
tokens: AngularContentToken[];
|
||||
}
|
||||
type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
|
||||
type AngularCountEllipsisConfig = {
|
||||
count: number;
|
||||
rows?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
type AngularRowsEllipsisConfig = {
|
||||
rows: number;
|
||||
count?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
type AngularContentEllipsisConfig = AngularCountEllipsisConfig | AngularRowsEllipsisConfig | false;
|
||||
type AngularRenderResult = Node | string | number | null | undefined;
|
||||
type AngularMentionRenderer = (entity: MentionEntity, index: number) => AngularRenderResult;
|
||||
type AngularTagRenderer = (entity: TagEntity, index: number) => AngularRenderResult;
|
||||
type AngularLinkRenderer = (entity: LinkEntity, index: number) => AngularRenderResult;
|
||||
interface AngularContentTextProps {
|
||||
className?: string;
|
||||
weight?: "normal" | "bold";
|
||||
text?: string | null;
|
||||
ellipsis?: AngularContentEllipsisConfig;
|
||||
blur?: boolean;
|
||||
style?: Record<string, string | number> | null;
|
||||
onView?: () => void;
|
||||
renderMention?: AngularMentionRenderer;
|
||||
renderTag?: AngularTagRenderer;
|
||||
renderLink?: AngularLinkRenderer;
|
||||
}
|
||||
type AngularContentTextWithSuggestionsProps = Omit<AngularContentTextProps, "renderMention" | "renderTag"> & {
|
||||
renderMention?: AngularMentionRenderer;
|
||||
renderTag?: AngularTagRenderer;
|
||||
};
|
||||
type AngularContentTitleWithSuggestionsProps = Omit<AngularContentTextWithSuggestionsProps, "weight">;
|
||||
interface AngularContentTextRendererState {
|
||||
props: AngularContentTextProps;
|
||||
snapshot: AngularContentSnapshot;
|
||||
expanded: boolean;
|
||||
}
|
||||
declare const toKebabCase: (value: string) => string;
|
||||
declare const buildAngularTagHref: (entity: TagEntity) => string;
|
||||
|
||||
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
|
||||
declare class AngularContentSuggestionsAdapter {
|
||||
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
|
||||
}
|
||||
declare class AngularContentTextRenderer {
|
||||
private host;
|
||||
private props;
|
||||
private expanded;
|
||||
private viewed;
|
||||
private hostInsideView;
|
||||
private observer;
|
||||
attach(host: HTMLElement, props?: AngularContentTextProps): AngularContentTextRendererState;
|
||||
update(nextProps: AngularContentTextProps): AngularContentTextRendererState;
|
||||
destroy(): void;
|
||||
getState(): AngularContentTextRendererState;
|
||||
private getText;
|
||||
private createSnapshot;
|
||||
private resolveEllipsisConfig;
|
||||
private getMergedExpanded;
|
||||
private createDefaultMentionNode;
|
||||
private createDefaultTagNode;
|
||||
private createDefaultLinkNode;
|
||||
private renderEntity;
|
||||
private buildTextNodes;
|
||||
private applyBaseParagraphStyle;
|
||||
private buildReadMoreButton;
|
||||
private handleExpand;
|
||||
private computeEntitySafeCutoff;
|
||||
private hasVerticalOverflow;
|
||||
private render;
|
||||
private initObserver;
|
||||
private emitOnViewIfNeeded;
|
||||
}
|
||||
declare class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {
|
||||
}
|
||||
declare class AngularContentTitleWithSuggestionsRenderer {
|
||||
private readonly renderer;
|
||||
attach(host: HTMLElement, props?: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
|
||||
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
|
||||
destroy(): void;
|
||||
getState(): AngularContentTextRendererState;
|
||||
private normalizeProps;
|
||||
}
|
||||
|
||||
export { type AngularContentEllipsisConfig, type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentTextProps, AngularContentTextRenderer, type AngularContentTextRendererState, type AngularContentTextWithSuggestionsProps, AngularContentTextWithSuggestionsRenderer, type AngularContentTitleWithSuggestionsProps, AngularContentTitleWithSuggestionsRenderer, type AngularContentToken, type AngularCountEllipsisConfig, type AngularEllipsisSymbol, type AngularLinkRenderer, type AngularLinkToken, type AngularMentionRenderer, type AngularMentionToken, type AngularRenderResult, type AngularRowsEllipsisConfig, type AngularTagRenderer, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens, toKebabCase };
|
||||
118
packages/content-suggestions/dist/angular/index.d.ts
vendored
Normal file
118
packages/content-suggestions/dist/angular/index.d.ts
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import { C as ContentEntity, M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.js';
|
||||
|
||||
interface AngularTextToken {
|
||||
kind: "text";
|
||||
text: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
interface AngularMentionToken {
|
||||
kind: "mention";
|
||||
entity: MentionEntity;
|
||||
}
|
||||
interface AngularTagToken {
|
||||
kind: "tag";
|
||||
entity: TagEntity;
|
||||
}
|
||||
interface AngularLinkToken {
|
||||
kind: "link";
|
||||
entity: LinkEntity;
|
||||
}
|
||||
type AngularContentToken = AngularTextToken | AngularMentionToken | AngularTagToken | AngularLinkToken;
|
||||
interface AngularContentSnapshot {
|
||||
text: string;
|
||||
entities: ContentEntity[];
|
||||
tokens: AngularContentToken[];
|
||||
}
|
||||
type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
|
||||
type AngularCountEllipsisConfig = {
|
||||
count: number;
|
||||
rows?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
type AngularRowsEllipsisConfig = {
|
||||
rows: number;
|
||||
count?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
type AngularContentEllipsisConfig = AngularCountEllipsisConfig | AngularRowsEllipsisConfig | false;
|
||||
type AngularRenderResult = Node | string | number | null | undefined;
|
||||
type AngularMentionRenderer = (entity: MentionEntity, index: number) => AngularRenderResult;
|
||||
type AngularTagRenderer = (entity: TagEntity, index: number) => AngularRenderResult;
|
||||
type AngularLinkRenderer = (entity: LinkEntity, index: number) => AngularRenderResult;
|
||||
interface AngularContentTextProps {
|
||||
className?: string;
|
||||
weight?: "normal" | "bold";
|
||||
text?: string | null;
|
||||
ellipsis?: AngularContentEllipsisConfig;
|
||||
blur?: boolean;
|
||||
style?: Record<string, string | number> | null;
|
||||
onView?: () => void;
|
||||
renderMention?: AngularMentionRenderer;
|
||||
renderTag?: AngularTagRenderer;
|
||||
renderLink?: AngularLinkRenderer;
|
||||
}
|
||||
type AngularContentTextWithSuggestionsProps = Omit<AngularContentTextProps, "renderMention" | "renderTag"> & {
|
||||
renderMention?: AngularMentionRenderer;
|
||||
renderTag?: AngularTagRenderer;
|
||||
};
|
||||
type AngularContentTitleWithSuggestionsProps = Omit<AngularContentTextWithSuggestionsProps, "weight">;
|
||||
interface AngularContentTextRendererState {
|
||||
props: AngularContentTextProps;
|
||||
snapshot: AngularContentSnapshot;
|
||||
expanded: boolean;
|
||||
}
|
||||
declare const toKebabCase: (value: string) => string;
|
||||
declare const buildAngularTagHref: (entity: TagEntity) => string;
|
||||
|
||||
declare const createAngularContentTokens: (inputText: string | null | undefined) => AngularContentToken[];
|
||||
declare class AngularContentSuggestionsAdapter {
|
||||
snapshot(inputText: string | null | undefined): AngularContentSnapshot;
|
||||
}
|
||||
declare class AngularContentTextRenderer {
|
||||
private host;
|
||||
private props;
|
||||
private expanded;
|
||||
private viewed;
|
||||
private hostInsideView;
|
||||
private observer;
|
||||
attach(host: HTMLElement, props?: AngularContentTextProps): AngularContentTextRendererState;
|
||||
update(nextProps: AngularContentTextProps): AngularContentTextRendererState;
|
||||
destroy(): void;
|
||||
getState(): AngularContentTextRendererState;
|
||||
private getText;
|
||||
private createSnapshot;
|
||||
private resolveEllipsisConfig;
|
||||
private getMergedExpanded;
|
||||
private createDefaultMentionNode;
|
||||
private createDefaultTagNode;
|
||||
private createDefaultLinkNode;
|
||||
private renderEntity;
|
||||
private buildTextNodes;
|
||||
private applyBaseParagraphStyle;
|
||||
private buildReadMoreButton;
|
||||
private handleExpand;
|
||||
private computeEntitySafeCutoff;
|
||||
private hasVerticalOverflow;
|
||||
private render;
|
||||
private initObserver;
|
||||
private emitOnViewIfNeeded;
|
||||
}
|
||||
declare class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {
|
||||
}
|
||||
declare class AngularContentTitleWithSuggestionsRenderer {
|
||||
private readonly renderer;
|
||||
attach(host: HTMLElement, props?: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
|
||||
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState;
|
||||
destroy(): void;
|
||||
getState(): AngularContentTextRendererState;
|
||||
private normalizeProps;
|
||||
}
|
||||
|
||||
export { type AngularContentEllipsisConfig, type AngularContentSnapshot, AngularContentSuggestionsAdapter, type AngularContentTextProps, AngularContentTextRenderer, type AngularContentTextRendererState, type AngularContentTextWithSuggestionsProps, AngularContentTextWithSuggestionsRenderer, type AngularContentTitleWithSuggestionsProps, AngularContentTitleWithSuggestionsRenderer, type AngularContentToken, type AngularCountEllipsisConfig, type AngularEllipsisSymbol, type AngularLinkRenderer, type AngularLinkToken, type AngularMentionRenderer, type AngularMentionToken, type AngularRenderResult, type AngularRowsEllipsisConfig, type AngularTagRenderer, type AngularTagToken, type AngularTextToken, buildAngularTagHref, createAngularContentTokens, toKebabCase };
|
||||
487
packages/content-suggestions/dist/angular/index.js
vendored
Normal file
487
packages/content-suggestions/dist/angular/index.js
vendored
Normal file
@@ -0,0 +1,487 @@
|
||||
// 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);
|
||||
};
|
||||
|
||||
// src/angular/index.ts
|
||||
var LINK_COLOR = "#1677ff";
|
||||
var READ_MORE_TEXT = "\u0427\u0438\u0442\u0430\u0442\u044C \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E";
|
||||
var toKebabCase = (value) => value.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
|
||||
var applyStyleObject = (element, styles) => {
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
for (const [key, rawValue] of Object.entries(styles)) {
|
||||
if (rawValue === null || rawValue === void 0) {
|
||||
continue;
|
||||
}
|
||||
const styleValue = typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
|
||||
if (key.startsWith("--")) {
|
||||
element.style.setProperty(key, styleValue);
|
||||
continue;
|
||||
}
|
||||
const cssKey = key.includes("-") ? key : toKebabCase(key);
|
||||
element.style.setProperty(cssKey, styleValue);
|
||||
}
|
||||
};
|
||||
var toNode = (value) => {
|
||||
if (value === null || value === void 0) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Node) {
|
||||
return value;
|
||||
}
|
||||
return document.createTextNode(String(value));
|
||||
};
|
||||
var resolveEllipsisSymbol = (symbol, expanded) => {
|
||||
if (typeof symbol === "function") {
|
||||
return symbol(expanded);
|
||||
}
|
||||
return symbol ?? READ_MORE_TEXT;
|
||||
};
|
||||
var buildAngularTagHref = (entity) => {
|
||||
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
|
||||
};
|
||||
var 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({
|
||||
kind: "text",
|
||||
text: text.slice(cursor, entity.start),
|
||||
start: cursor,
|
||||
end: entity.start
|
||||
});
|
||||
}
|
||||
if (entity.type === "mention") {
|
||||
tokens.push({
|
||||
kind: "mention",
|
||||
entity
|
||||
});
|
||||
} else if (entity.type === "tag") {
|
||||
tokens.push({
|
||||
kind: "tag",
|
||||
entity
|
||||
});
|
||||
} else {
|
||||
tokens.push({
|
||||
kind: "link",
|
||||
entity
|
||||
});
|
||||
}
|
||||
cursor = entity.end;
|
||||
}
|
||||
if (cursor < text.length) {
|
||||
tokens.push({
|
||||
kind: "text",
|
||||
text: text.slice(cursor),
|
||||
start: cursor,
|
||||
end: text.length
|
||||
});
|
||||
}
|
||||
return tokens;
|
||||
};
|
||||
var AngularContentSuggestionsAdapter = class {
|
||||
snapshot(inputText) {
|
||||
const text = inputText ?? "";
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
};
|
||||
var AngularContentTextRenderer = class {
|
||||
host = null;
|
||||
props = {};
|
||||
expanded = false;
|
||||
viewed = false;
|
||||
hostInsideView = false;
|
||||
observer = null;
|
||||
attach(host, props = {}) {
|
||||
this.destroy();
|
||||
this.host = host;
|
||||
this.props = { ...props };
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
this.initObserver();
|
||||
this.render();
|
||||
return this.getState();
|
||||
}
|
||||
update(nextProps) {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...nextProps
|
||||
};
|
||||
this.render();
|
||||
return this.getState();
|
||||
}
|
||||
destroy() {
|
||||
if (this.observer && this.host) {
|
||||
this.observer.unobserve(this.host);
|
||||
this.observer.disconnect();
|
||||
}
|
||||
this.observer = null;
|
||||
if (this.host) {
|
||||
this.host.innerHTML = "";
|
||||
}
|
||||
this.host = null;
|
||||
this.props = {};
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
}
|
||||
getState() {
|
||||
return {
|
||||
props: { ...this.props },
|
||||
snapshot: this.createSnapshot(),
|
||||
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText())
|
||||
};
|
||||
}
|
||||
getText() {
|
||||
return this.props.text ?? "";
|
||||
}
|
||||
createSnapshot() {
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens
|
||||
};
|
||||
}
|
||||
resolveEllipsisConfig() {
|
||||
const ellipsis = this.props.ellipsis;
|
||||
if (!ellipsis || typeof ellipsis !== "object") {
|
||||
return null;
|
||||
}
|
||||
return ellipsis;
|
||||
}
|
||||
getMergedExpanded(ellipsisConfig, text) {
|
||||
const controlledExpanded = ellipsisConfig && typeof ellipsisConfig.expanded === "boolean" ? ellipsisConfig.expanded : void 0;
|
||||
if (controlledExpanded !== void 0) {
|
||||
return controlledExpanded;
|
||||
}
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
if (count <= 0 || text.length <= count) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.expanded;
|
||||
}
|
||||
createDefaultMentionNode(entity) {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.displayText;
|
||||
return span;
|
||||
}
|
||||
createDefaultTagNode(entity) {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.text;
|
||||
return span;
|
||||
}
|
||||
createDefaultLinkNode(entity) {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = entity.url;
|
||||
anchor.target = "_blank";
|
||||
anchor.referrerPolicy = "no-referrer";
|
||||
anchor.style.color = LINK_COLOR;
|
||||
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
anchor.textContent = entity.text;
|
||||
return anchor;
|
||||
}
|
||||
renderEntity(entity, index) {
|
||||
if (entity.type === "mention") {
|
||||
const customNode2 = this.props.renderMention?.(entity, index);
|
||||
return toNode(customNode2) ?? this.createDefaultMentionNode(entity);
|
||||
}
|
||||
if (entity.type === "tag") {
|
||||
const customNode2 = this.props.renderTag?.(entity, index);
|
||||
return toNode(customNode2) ?? this.createDefaultTagNode(entity);
|
||||
}
|
||||
const customNode = this.props.renderLink?.(entity, index);
|
||||
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
|
||||
}
|
||||
buildTextNodes(text, entities, upto = null) {
|
||||
let lastIndex = 0;
|
||||
const nodes = [];
|
||||
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) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, textEnd)));
|
||||
}
|
||||
if (upto === null || entity.end <= upto) {
|
||||
const entityNode = this.renderEntity(entity, index);
|
||||
if (entityNode) {
|
||||
nodes.push(entityNode);
|
||||
}
|
||||
}
|
||||
lastIndex = entity.end;
|
||||
}
|
||||
if (upto === null) {
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex)));
|
||||
}
|
||||
} else if (lastIndex < upto) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
applyBaseParagraphStyle(element) {
|
||||
element.style.whiteSpace = "pre-wrap";
|
||||
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
element.style.setProperty("-webkit-touch-callout", "default");
|
||||
element.style.setProperty("-webkit-user-select", "text");
|
||||
element.style.setProperty("-khtml-user-select", "text");
|
||||
element.style.setProperty("-moz-user-select", "text");
|
||||
element.style.setProperty("-ms-user-select", "text");
|
||||
element.style.setProperty("user-select", "text");
|
||||
if (this.props.blur) {
|
||||
element.style.filter = "blur(3px)";
|
||||
element.style.setProperty("-webkit-user-select", "none");
|
||||
element.style.setProperty("-khtml-user-select", "none");
|
||||
element.style.setProperty("-moz-user-select", "none");
|
||||
element.style.setProperty("-ms-user-select", "none");
|
||||
element.style.setProperty("user-select", "none");
|
||||
element.style.pointerEvents = "none";
|
||||
}
|
||||
applyStyleObject(element, this.props.style);
|
||||
}
|
||||
buildReadMoreButton(symbol) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
|
||||
button.style.border = "0";
|
||||
button.style.background = "none";
|
||||
button.style.padding = "0";
|
||||
button.style.marginLeft = "6px";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.color = LINK_COLOR;
|
||||
button.style.fontWeight = "700";
|
||||
button.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.handleExpand();
|
||||
};
|
||||
return button;
|
||||
}
|
||||
handleExpand() {
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
if (!ellipsisConfig) {
|
||||
return;
|
||||
}
|
||||
this.expanded = true;
|
||||
ellipsisConfig.onExpand?.(true);
|
||||
this.render();
|
||||
}
|
||||
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;
|
||||
}
|
||||
hasVerticalOverflow(element) {
|
||||
return element.scrollHeight - element.clientHeight > 1;
|
||||
}
|
||||
render() {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
|
||||
const wrapper = document.createElement("div");
|
||||
const paragraph = document.createElement("div");
|
||||
if (this.props.className) {
|
||||
paragraph.className = this.props.className;
|
||||
}
|
||||
this.applyBaseParagraphStyle(paragraph);
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
|
||||
if (shouldTruncate) {
|
||||
const cutoff = this.computeEntitySafeCutoff(count, entities);
|
||||
const nodes = this.buildTextNodes(text, entities, cutoff);
|
||||
paragraph.append(...nodes);
|
||||
paragraph.append(document.createTextNode("\u2026"));
|
||||
if (ellipsisConfig.expandable) {
|
||||
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
|
||||
paragraph.style.display = "-webkit-box";
|
||||
paragraph.style.setProperty("-webkit-box-orient", "vertical");
|
||||
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
|
||||
paragraph.style.overflow = "hidden";
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
|
||||
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
initObserver() {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
this.hostInsideView = true;
|
||||
return;
|
||||
}
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (entry.isIntersecting && !this.hostInsideView) {
|
||||
this.hostInsideView = true;
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
this.observer.observe(this.host);
|
||||
}
|
||||
emitOnViewIfNeeded(mergedExpanded) {
|
||||
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
|
||||
return;
|
||||
}
|
||||
this.viewed = true;
|
||||
this.props.onView?.();
|
||||
}
|
||||
};
|
||||
var AngularContentTextWithSuggestionsRenderer = class extends AngularContentTextRenderer {
|
||||
};
|
||||
var AngularContentTitleWithSuggestionsRenderer = class {
|
||||
renderer = new AngularContentTextWithSuggestionsRenderer();
|
||||
attach(host, props = {}) {
|
||||
return this.renderer.attach(host, this.normalizeProps(props));
|
||||
}
|
||||
update(props) {
|
||||
return this.renderer.update(this.normalizeProps(props));
|
||||
}
|
||||
destroy() {
|
||||
this.renderer.destroy();
|
||||
}
|
||||
getState() {
|
||||
return this.renderer.getState();
|
||||
}
|
||||
normalizeProps(props) {
|
||||
return {
|
||||
...props,
|
||||
weight: "bold",
|
||||
blur: props.blur ?? false,
|
||||
ellipsis: props.ellipsis === void 0 ? { rows: 2 } : props.ellipsis
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export { AngularContentSuggestionsAdapter, AngularContentTextRenderer, AngularContentTextWithSuggestionsRenderer, AngularContentTitleWithSuggestionsRenderer, buildAngularTagHref, createAngularContentTokens, toKebabCase };
|
||||
//# sourceMappingURL=index.js.map
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/content-suggestions/dist/angular/index.js.map
vendored
Normal file
1
packages/content-suggestions/dist/angular/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
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
12
packages/content-suggestions/dist/core/index.d.cts
vendored
Normal file
12
packages/content-suggestions/dist/core/index.d.cts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { C as ContentEntity, L as LinkEntity, M as MentionEntity, T as TagEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.cjs';
|
||||
export { B as BaseEntity } from '../types-BRt4hd7A.cjs';
|
||||
|
||||
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 { ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, TagEntity, findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent };
|
||||
12
packages/content-suggestions/dist/core/index.d.ts
vendored
Normal file
12
packages/content-suggestions/dist/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { C as ContentEntity, L as LinkEntity, M as MentionEntity, T as TagEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.js';
|
||||
export { B as BaseEntity } from '../types-BRt4hd7A.js';
|
||||
|
||||
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 { ContentEntity, LinkEntity, MentionEntity, ParsedMention, ProcessedContent, 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
48
packages/content-suggestions/dist/react/index.d.cts
vendored
Normal file
48
packages/content-suggestions/dist/react/index.d.cts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { EllipsisConfig } from 'antd/lib/typography/Base';
|
||||
import { M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.cjs';
|
||||
export { B as BaseEntity, C as ContentEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.cjs';
|
||||
import { TachTypography } from '@hublib-web/tach-typography/react';
|
||||
import * as react_jsx_runtime from 'react/jsx-runtime';
|
||||
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.cjs';
|
||||
|
||||
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;
|
||||
|
||||
interface ContentTitleWithSuggestionsProps extends 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 };
|
||||
48
packages/content-suggestions/dist/react/index.d.ts
vendored
Normal file
48
packages/content-suggestions/dist/react/index.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { EllipsisConfig } from 'antd/lib/typography/Base';
|
||||
import { M as MentionEntity, T as TagEntity, L as LinkEntity } from '../types-BRt4hd7A.js';
|
||||
export { B as BaseEntity, C as ContentEntity, P as ParsedMention, a as ProcessedContent } from '../types-BRt4hd7A.js';
|
||||
import { TachTypography } from '@hublib-web/tach-typography/react';
|
||||
import * as react_jsx_runtime from 'react/jsx-runtime';
|
||||
export { findAllEntities, findLinks, findMentions, findTags, mentionLinkRegexp, parseMention, processContent } from '../core/index.js';
|
||||
|
||||
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;
|
||||
|
||||
interface ContentTitleWithSuggestionsProps extends 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
29
packages/content-suggestions/dist/types-BRt4hd7A.d.cts
vendored
Normal file
29
packages/content-suggestions/dist/types-BRt4hd7A.d.cts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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[];
|
||||
}
|
||||
|
||||
export type { BaseEntity as B, ContentEntity as C, LinkEntity as L, MentionEntity as M, ParsedMention as P, TagEntity as T, ProcessedContent as a };
|
||||
29
packages/content-suggestions/dist/types-BRt4hd7A.d.ts
vendored
Normal file
29
packages/content-suggestions/dist/types-BRt4hd7A.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
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[];
|
||||
}
|
||||
|
||||
export type { BaseEntity as B, ContentEntity as C, LinkEntity as L, MentionEntity as M, ParsedMention as P, TagEntity as T, ProcessedContent as a };
|
||||
105
packages/content-suggestions/package.json
Normal file
105
packages/content-suggestions/package.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"name": "@hublib-web/content-suggestions",
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"require": "./dist/angular/index.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn clean && tsup",
|
||||
"clean": "rm -rf dist storybook-static",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "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.1.0",
|
||||
"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/core": "^20.3.17",
|
||||
"@hublib-web/tach-typography": "workspace:*",
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
650
packages/content-suggestions/src/angular/index.ts
Normal file
650
packages/content-suggestions/src/angular/index.ts
Normal file
@@ -0,0 +1,650 @@
|
||||
import type { ContentEntity, LinkEntity, MentionEntity, TagEntity } from "../core";
|
||||
|
||||
import { findAllEntities } from "../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 type AngularEllipsisSymbol = string | ((expanded: boolean) => string);
|
||||
|
||||
export type AngularCountEllipsisConfig = {
|
||||
count: number;
|
||||
rows?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
|
||||
export type AngularRowsEllipsisConfig = {
|
||||
rows: number;
|
||||
count?: never;
|
||||
expandable?: boolean;
|
||||
expanded?: boolean;
|
||||
symbol?: AngularEllipsisSymbol;
|
||||
onExpand?: (expanded: boolean) => void;
|
||||
};
|
||||
|
||||
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?: "normal" | "bold";
|
||||
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"
|
||||
>;
|
||||
|
||||
export interface AngularContentTextRendererState {
|
||||
props: AngularContentTextProps;
|
||||
snapshot: AngularContentSnapshot;
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
const LINK_COLOR = "#1677ff";
|
||||
const READ_MORE_TEXT = "Читать полностью";
|
||||
|
||||
const toKebabCase = (value: string) => value.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`);
|
||||
|
||||
const applyStyleObject = (
|
||||
element: HTMLElement,
|
||||
styles?: Record<string, string | number> | null,
|
||||
) => {
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, rawValue] of Object.entries(styles)) {
|
||||
if (rawValue === null || rawValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const styleValue =
|
||||
typeof rawValue === "number" ? `${rawValue}px` : String(rawValue);
|
||||
|
||||
if (key.startsWith("--")) {
|
||||
element.style.setProperty(key, styleValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cssKey = key.includes("-") ? key : toKebabCase(key);
|
||||
element.style.setProperty(cssKey, styleValue);
|
||||
}
|
||||
};
|
||||
|
||||
const toNode = (value: AngularRenderResult): Node | null => {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Node) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return document.createTextNode(String(value));
|
||||
};
|
||||
|
||||
const resolveEllipsisSymbol = (
|
||||
symbol: AngularEllipsisSymbol | undefined,
|
||||
expanded: boolean,
|
||||
): string => {
|
||||
if (typeof symbol === "function") {
|
||||
return symbol(expanded);
|
||||
}
|
||||
|
||||
return symbol ?? READ_MORE_TEXT;
|
||||
};
|
||||
|
||||
const buildAngularTagHref = (entity: TagEntity): string => {
|
||||
return `/search/?query=${encodeURIComponent(entity.tag.toLowerCase())}`;
|
||||
};
|
||||
|
||||
export { buildAngularTagHref };
|
||||
|
||||
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({
|
||||
kind: "text",
|
||||
text: text.slice(cursor, entity.start),
|
||||
start: cursor,
|
||||
end: entity.start,
|
||||
});
|
||||
}
|
||||
|
||||
if (entity.type === "mention") {
|
||||
tokens.push({
|
||||
kind: "mention",
|
||||
entity,
|
||||
});
|
||||
} else if (entity.type === "tag") {
|
||||
tokens.push({
|
||||
kind: "tag",
|
||||
entity,
|
||||
});
|
||||
} else {
|
||||
tokens.push({
|
||||
kind: "link",
|
||||
entity,
|
||||
});
|
||||
}
|
||||
|
||||
cursor = entity.end;
|
||||
}
|
||||
|
||||
if (cursor < text.length) {
|
||||
tokens.push({
|
||||
kind: "text",
|
||||
text: text.slice(cursor),
|
||||
start: cursor,
|
||||
end: text.length,
|
||||
});
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
export class AngularContentSuggestionsAdapter {
|
||||
snapshot(inputText: string | null | undefined): AngularContentSnapshot {
|
||||
const text = inputText ?? "";
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AngularContentTextRenderer {
|
||||
private host: HTMLElement | null = null;
|
||||
private props: AngularContentTextProps = {};
|
||||
private expanded = false;
|
||||
private viewed = false;
|
||||
private hostInsideView = false;
|
||||
private observer: IntersectionObserver | null = null;
|
||||
|
||||
attach(host: HTMLElement, props: AngularContentTextProps = {}): AngularContentTextRendererState {
|
||||
this.destroy();
|
||||
|
||||
this.host = host;
|
||||
this.props = { ...props };
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
|
||||
this.initObserver();
|
||||
this.render();
|
||||
|
||||
return this.getState();
|
||||
}
|
||||
|
||||
update(nextProps: AngularContentTextProps): AngularContentTextRendererState {
|
||||
this.props = {
|
||||
...this.props,
|
||||
...nextProps,
|
||||
};
|
||||
|
||||
this.render();
|
||||
|
||||
return this.getState();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.observer && this.host) {
|
||||
this.observer.unobserve(this.host);
|
||||
this.observer.disconnect();
|
||||
}
|
||||
|
||||
this.observer = null;
|
||||
|
||||
if (this.host) {
|
||||
this.host.innerHTML = "";
|
||||
}
|
||||
|
||||
this.host = null;
|
||||
this.props = {};
|
||||
this.expanded = false;
|
||||
this.viewed = false;
|
||||
this.hostInsideView = false;
|
||||
}
|
||||
|
||||
getState(): AngularContentTextRendererState {
|
||||
return {
|
||||
props: { ...this.props },
|
||||
snapshot: this.createSnapshot(),
|
||||
expanded: this.getMergedExpanded(this.resolveEllipsisConfig(), this.getText()),
|
||||
};
|
||||
}
|
||||
|
||||
private getText(): string {
|
||||
return this.props.text ?? "";
|
||||
}
|
||||
|
||||
private createSnapshot(): AngularContentSnapshot {
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const tokens = createAngularContentTokens(text);
|
||||
|
||||
return {
|
||||
text,
|
||||
entities,
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveEllipsisConfig():
|
||||
| AngularCountEllipsisConfig
|
||||
| AngularRowsEllipsisConfig
|
||||
| null {
|
||||
const ellipsis = this.props.ellipsis;
|
||||
|
||||
if (!ellipsis || typeof ellipsis !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ellipsis;
|
||||
}
|
||||
|
||||
private getMergedExpanded(
|
||||
ellipsisConfig: AngularCountEllipsisConfig | AngularRowsEllipsisConfig | null,
|
||||
text: string,
|
||||
): boolean {
|
||||
const controlledExpanded =
|
||||
ellipsisConfig && typeof ellipsisConfig.expanded === "boolean"
|
||||
? ellipsisConfig.expanded
|
||||
: undefined;
|
||||
|
||||
if (controlledExpanded !== undefined) {
|
||||
return controlledExpanded;
|
||||
}
|
||||
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
if (count <= 0 || text.length <= count) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.expanded;
|
||||
}
|
||||
|
||||
private createDefaultMentionNode(entity: MentionEntity): Node {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.displayText;
|
||||
return span;
|
||||
}
|
||||
|
||||
private createDefaultTagNode(entity: TagEntity): Node {
|
||||
const span = document.createElement("span");
|
||||
span.style.color = LINK_COLOR;
|
||||
span.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
span.textContent = entity.text;
|
||||
return span;
|
||||
}
|
||||
|
||||
private createDefaultLinkNode(entity: LinkEntity): Node {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = entity.url;
|
||||
anchor.target = "_blank";
|
||||
anchor.referrerPolicy = "no-referrer";
|
||||
anchor.style.color = LINK_COLOR;
|
||||
anchor.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
anchor.textContent = entity.text;
|
||||
return anchor;
|
||||
}
|
||||
|
||||
private renderEntity(entity: ContentEntity, index: number): Node | null {
|
||||
if (entity.type === "mention") {
|
||||
const customNode = this.props.renderMention?.(entity, index);
|
||||
return toNode(customNode) ?? this.createDefaultMentionNode(entity);
|
||||
}
|
||||
|
||||
if (entity.type === "tag") {
|
||||
const customNode = this.props.renderTag?.(entity, index);
|
||||
return toNode(customNode) ?? this.createDefaultTagNode(entity);
|
||||
}
|
||||
|
||||
const customNode = this.props.renderLink?.(entity, index);
|
||||
return toNode(customNode) ?? this.createDefaultLinkNode(entity);
|
||||
}
|
||||
|
||||
private buildTextNodes(
|
||||
text: string,
|
||||
entities: ContentEntity[],
|
||||
upto: number | null = null,
|
||||
): Node[] {
|
||||
let lastIndex = 0;
|
||||
const nodes: Node[] = [];
|
||||
|
||||
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) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, textEnd)));
|
||||
}
|
||||
|
||||
if (upto === null || entity.end <= upto) {
|
||||
const entityNode = this.renderEntity(entity, index);
|
||||
if (entityNode) {
|
||||
nodes.push(entityNode);
|
||||
}
|
||||
}
|
||||
|
||||
lastIndex = entity.end;
|
||||
}
|
||||
|
||||
if (upto === null) {
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex)));
|
||||
}
|
||||
} else if (lastIndex < upto) {
|
||||
nodes.push(document.createTextNode(text.slice(lastIndex, upto)));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private applyBaseParagraphStyle(element: HTMLElement): void {
|
||||
element.style.whiteSpace = "pre-wrap";
|
||||
element.style.fontWeight = this.props.weight === "bold" ? "700" : "400";
|
||||
element.style.setProperty("-webkit-touch-callout", "default");
|
||||
element.style.setProperty("-webkit-user-select", "text");
|
||||
element.style.setProperty("-khtml-user-select", "text");
|
||||
element.style.setProperty("-moz-user-select", "text");
|
||||
element.style.setProperty("-ms-user-select", "text");
|
||||
element.style.setProperty("user-select", "text");
|
||||
|
||||
if (this.props.blur) {
|
||||
element.style.filter = "blur(3px)";
|
||||
element.style.setProperty("-webkit-user-select", "none");
|
||||
element.style.setProperty("-khtml-user-select", "none");
|
||||
element.style.setProperty("-moz-user-select", "none");
|
||||
element.style.setProperty("-ms-user-select", "none");
|
||||
element.style.setProperty("user-select", "none");
|
||||
element.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
applyStyleObject(element, this.props.style);
|
||||
}
|
||||
|
||||
private buildReadMoreButton(symbol: AngularEllipsisSymbol | undefined): HTMLButtonElement {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = resolveEllipsisSymbol(symbol, this.expanded);
|
||||
button.style.border = "0";
|
||||
button.style.background = "none";
|
||||
button.style.padding = "0";
|
||||
button.style.marginLeft = "6px";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.color = LINK_COLOR;
|
||||
button.style.fontWeight = "700";
|
||||
button.onclick = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.handleExpand();
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
||||
private handleExpand(): void {
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
|
||||
if (!ellipsisConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.expanded = true;
|
||||
ellipsisConfig.onExpand?.(true);
|
||||
this.render();
|
||||
}
|
||||
|
||||
private computeEntitySafeCutoff(
|
||||
count: number,
|
||||
entities: ContentEntity[],
|
||||
): 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 hasVerticalOverflow(element: HTMLElement): boolean {
|
||||
return element.scrollHeight - element.clientHeight > 1;
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = this.getText();
|
||||
const entities = findAllEntities(text);
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, text);
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
const paragraph = document.createElement("div");
|
||||
|
||||
if (this.props.className) {
|
||||
paragraph.className = this.props.className;
|
||||
}
|
||||
|
||||
this.applyBaseParagraphStyle(paragraph);
|
||||
|
||||
if (ellipsisConfig && "count" in ellipsisConfig) {
|
||||
const count = ellipsisConfig.count ?? 0;
|
||||
const shouldTruncate = !mergedExpanded && count > 0 && text.length > count;
|
||||
|
||||
if (shouldTruncate) {
|
||||
const cutoff = this.computeEntitySafeCutoff(count, entities);
|
||||
const nodes = this.buildTextNodes(text, entities, cutoff);
|
||||
|
||||
paragraph.append(...nodes);
|
||||
paragraph.append(document.createTextNode("…"));
|
||||
|
||||
if (ellipsisConfig.expandable) {
|
||||
paragraph.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
}
|
||||
} else {
|
||||
paragraph.append(...this.buildTextNodes(text, entities));
|
||||
|
||||
if (ellipsisConfig && "rows" in ellipsisConfig && !mergedExpanded) {
|
||||
paragraph.style.display = "-webkit-box";
|
||||
paragraph.style.setProperty("-webkit-box-orient", "vertical");
|
||||
paragraph.style.setProperty("-webkit-line-clamp", String(ellipsisConfig.rows));
|
||||
paragraph.style.overflow = "hidden";
|
||||
|
||||
// Важно: сначала рендерим paragraph в DOM, потом меряем реальный overflow.
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
|
||||
if (ellipsisConfig.expandable && this.hasVerticalOverflow(paragraph)) {
|
||||
wrapper.append(this.buildReadMoreButton(ellipsisConfig.symbol));
|
||||
}
|
||||
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.append(paragraph);
|
||||
this.host.replaceChildren(wrapper);
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
|
||||
private initObserver(): void {
|
||||
if (!this.host) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
this.hostInsideView = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
entries => {
|
||||
const entry = entries[0];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.isIntersecting && !this.hostInsideView) {
|
||||
this.hostInsideView = true;
|
||||
const ellipsisConfig = this.resolveEllipsisConfig();
|
||||
const mergedExpanded = this.getMergedExpanded(ellipsisConfig, this.getText());
|
||||
this.emitOnViewIfNeeded(mergedExpanded);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
this.observer.observe(this.host);
|
||||
}
|
||||
|
||||
private emitOnViewIfNeeded(mergedExpanded: boolean): void {
|
||||
if (!mergedExpanded || this.viewed || !this.hostInsideView) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewed = true;
|
||||
this.props.onView?.();
|
||||
}
|
||||
}
|
||||
|
||||
export class AngularContentTextWithSuggestionsRenderer extends AngularContentTextRenderer {}
|
||||
|
||||
export class AngularContentTitleWithSuggestionsRenderer {
|
||||
private readonly renderer = new AngularContentTextWithSuggestionsRenderer();
|
||||
|
||||
attach(
|
||||
host: HTMLElement,
|
||||
props: AngularContentTitleWithSuggestionsProps = {},
|
||||
): AngularContentTextRendererState {
|
||||
return this.renderer.attach(host, this.normalizeProps(props));
|
||||
}
|
||||
|
||||
update(props: AngularContentTitleWithSuggestionsProps): AngularContentTextRendererState {
|
||||
return this.renderer.update(this.normalizeProps(props));
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.renderer.destroy();
|
||||
}
|
||||
|
||||
getState(): AngularContentTextRendererState {
|
||||
return this.renderer.getState();
|
||||
}
|
||||
|
||||
private normalizeProps(
|
||||
props: AngularContentTitleWithSuggestionsProps,
|
||||
): AngularContentTextWithSuggestionsProps {
|
||||
return {
|
||||
...props,
|
||||
weight: "bold",
|
||||
blur: props.blur ?? false,
|
||||
ellipsis: props.ellipsis === undefined ? { rows: 2 } : props.ellipsis,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { toKebabCase };
|
||||
19
packages/content-suggestions/src/core/index.ts
Normal file
19
packages/content-suggestions/src/core/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type {
|
||||
BaseEntity,
|
||||
ContentEntity,
|
||||
LinkEntity,
|
||||
MentionEntity,
|
||||
ParsedMention,
|
||||
ProcessedContent,
|
||||
TagEntity,
|
||||
} from "./types";
|
||||
|
||||
export {
|
||||
findAllEntities,
|
||||
findLinks,
|
||||
findMentions,
|
||||
findTags,
|
||||
mentionLinkRegexp,
|
||||
parseMention,
|
||||
processContent,
|
||||
} from "./parser";
|
||||
115
packages/content-suggestions/src/core/parser.ts
Normal file
115
packages/content-suggestions/src/core/parser.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type {
|
||||
ContentEntity,
|
||||
LinkEntity,
|
||||
MentionEntity,
|
||||
ParsedMention,
|
||||
ProcessedContent,
|
||||
TagEntity,
|
||||
} from "./types";
|
||||
|
||||
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: string): ParsedMention | null => {
|
||||
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: string): MentionEntity[] => {
|
||||
let match: RegExpExecArray | null;
|
||||
const matches: MentionEntity[] = [];
|
||||
|
||||
while ((match = mentionLinkRegexp.exec(text)) !== null) {
|
||||
const parsed = parseMention(match[0]);
|
||||
const baseMention: Omit<MentionEntity, "userId"> = {
|
||||
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: string): TagEntity[] => {
|
||||
const regex = /#[^\s]{1,201}/g;
|
||||
const results: TagEntity[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
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: string): LinkEntity[] => {
|
||||
const regex =
|
||||
/\b((https?:\/\/)?(?:[\w-]+\.)+[a-z]{2,}(\/[\w\-._~:/?#[\]@!$&'()*+,;=]*)?)/gi;
|
||||
const results: LinkEntity[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
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: string): ContentEntity[] => {
|
||||
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: string): ProcessedContent => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
33
packages/content-suggestions/src/core/types.ts
Normal file
33
packages/content-suggestions/src/core/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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[];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import type { MentionEntity, TagEntity } from "../../core";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { TachTypography } from "@hublib-web/tach-typography/react";
|
||||
import { ContentText } from "./content-text";
|
||||
|
||||
type BaseContentTextProps = Omit<
|
||||
ComponentProps<typeof ContentText>,
|
||||
"renderMention" | "renderTag"
|
||||
> & {
|
||||
renderMention?: (entity: MentionEntity, index: number) => React.ReactNode;
|
||||
renderTag?: (entity: TagEntity, index: number) => React.ReactNode;
|
||||
};
|
||||
|
||||
const DefaultMention = ({ entity }: { entity: MentionEntity }) => (
|
||||
<TachTypography.Text.Body color="link">{entity.displayText}</TachTypography.Text.Body>
|
||||
);
|
||||
|
||||
const DefaultTag = ({ entity }: { entity: TagEntity }) => (
|
||||
<TachTypography.Text.Body color="link">
|
||||
{entity.text}
|
||||
</TachTypography.Text.Body>
|
||||
);
|
||||
|
||||
export const ContentTextWithSuggestions = ({
|
||||
renderMention,
|
||||
renderTag,
|
||||
...props
|
||||
}: BaseContentTextProps) => {
|
||||
return (
|
||||
<ContentText
|
||||
{...props}
|
||||
renderMention={(entity, index) =>
|
||||
renderMention ? renderMention(entity, index) : <DefaultMention entity={entity} />
|
||||
}
|
||||
renderTag={(entity, index) =>
|
||||
renderTag ? (
|
||||
renderTag(entity, index)
|
||||
) : (
|
||||
<DefaultTag entity={entity} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,353 @@
|
||||
import type { ComponentProps, CSSProperties, ReactNode } from "react";
|
||||
import type { EllipsisConfig } from "antd/lib/typography/Base";
|
||||
import type {
|
||||
ContentEntity,
|
||||
LinkEntity,
|
||||
MentionEntity,
|
||||
TagEntity,
|
||||
} from "../../core";
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { TachTypography } from "@hublib-web/tach-typography/react";
|
||||
import { findAllEntities } from "../../core";
|
||||
|
||||
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>;
|
||||
|
||||
const joinClassName = (...values: Array<string | false | undefined>) =>
|
||||
values.filter(Boolean).join(" ");
|
||||
|
||||
const baseSelectableStyle: CSSProperties = {
|
||||
whiteSpace: "pre-wrap",
|
||||
WebkitTouchCallout: "default",
|
||||
WebkitUserSelect: "text",
|
||||
KhtmlUserSelect: "text",
|
||||
MozUserSelect: "text",
|
||||
msUserSelect: "text",
|
||||
userSelect: "text",
|
||||
};
|
||||
|
||||
const blurStyle: CSSProperties = {
|
||||
filter: "blur(3px)",
|
||||
WebkitUserSelect: "none",
|
||||
KhtmlUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
userSelect: "none",
|
||||
pointerEvents: "none",
|
||||
};
|
||||
|
||||
const ReadMoreButton: React.FC<{
|
||||
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
symbol?: EllipsisConfig["symbol"];
|
||||
expanded?: boolean;
|
||||
TitleComponent: React.ComponentType<TitleBodyProps>;
|
||||
}> = ({ onClick, symbol = "Читать полностью", expanded = false, TitleComponent }) => (
|
||||
<TitleComponent
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick(event);
|
||||
}}
|
||||
weight="bold"
|
||||
level={5}
|
||||
>
|
||||
{typeof symbol === "function" ? symbol(expanded) : symbol}
|
||||
</TitleComponent>
|
||||
);
|
||||
|
||||
export 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>;
|
||||
}
|
||||
|
||||
export const ContentText: React.NamedExoticComponent<ContentTextProps> = 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
|
||||
}: ContentTextProps) => {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [containerInsideView, setContainerInsideView] = useState(false);
|
||||
const [viewed, setViewed] = useState(false);
|
||||
const content = text || "";
|
||||
|
||||
const entities = useMemo<ContentEntity[]>(() => findAllEntities(content), [content]);
|
||||
|
||||
const wrapWithKey = (node: ReactNode, key: React.Key) => {
|
||||
if (React.isValidElement(node)) {
|
||||
return React.cloneElement(node, { key });
|
||||
}
|
||||
|
||||
return <React.Fragment key={key}>{node}</React.Fragment>;
|
||||
};
|
||||
|
||||
const buildMentionNode = (entity: MentionEntity, index: number) => {
|
||||
const defaultNode = (
|
||||
<TextComponent color="link" weight={weight}>
|
||||
{entity.displayText}
|
||||
</TextComponent>
|
||||
);
|
||||
const customNode = renderMention?.(entity, index) ?? defaultNode;
|
||||
return wrapWithKey(customNode, `mention-${entity.start}-${index}`);
|
||||
};
|
||||
|
||||
const buildTagNode = (entity: TagEntity, index: number) => {
|
||||
const defaultNode = (
|
||||
<TextComponent color="link" weight={weight}>
|
||||
{entity.text}
|
||||
</TextComponent>
|
||||
);
|
||||
const customNode = renderTag?.(entity, index) ?? defaultNode;
|
||||
return wrapWithKey(customNode, `tag-${entity.start}-${index}`);
|
||||
};
|
||||
|
||||
const buildLinkNode = (entity: LinkEntity, index: number) => {
|
||||
const defaultNode = (
|
||||
<TachTypography.Link.Body
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
color="link"
|
||||
weight={weight}
|
||||
href={entity.url}
|
||||
>
|
||||
{entity.text}
|
||||
</TachTypography.Link.Body>
|
||||
);
|
||||
const customNode = renderLink?.(entity, index) ?? defaultNode;
|
||||
return wrapWithKey(customNode, `link-${entity.start}-${index}`);
|
||||
};
|
||||
|
||||
const buildParts = (upto: number | null = null): ReactNode[] => {
|
||||
let lastIndex = 0;
|
||||
const nodes: ReactNode[] = [];
|
||||
|
||||
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
|
||||
: undefined;
|
||||
const isExpandedControlled = expandedFromProps !== undefined;
|
||||
const mergedExpanded = isExpandedControlled ? expandedFromProps : expanded;
|
||||
|
||||
const handleExpand = useCallback(
|
||||
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
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: CSSProperties = {
|
||||
...baseSelectableStyle,
|
||||
...(blur ? blurStyle : undefined),
|
||||
...(style as CSSProperties | undefined),
|
||||
};
|
||||
|
||||
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 (
|
||||
<ParagraphComponent
|
||||
ref={containerRef}
|
||||
weight={weight}
|
||||
className={joinClassName(className)}
|
||||
style={mergedStyle}
|
||||
{...(props as ParagraphBodyProps)}
|
||||
>
|
||||
{truncatedNodes}…
|
||||
{expandable && (
|
||||
<ReadMoreButton
|
||||
symbol={ellipsisConfig.symbol}
|
||||
onClick={handleExpand}
|
||||
expanded={mergedExpanded}
|
||||
TitleComponent={TitleComponent}
|
||||
/>
|
||||
)}
|
||||
</ParagraphComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (ellipsisConfig && "rows" in ellipsisConfig) {
|
||||
const paragraphEllipsis = mergedExpanded
|
||||
? false
|
||||
: {
|
||||
...ellipsisConfig,
|
||||
symbol: ellipsisConfig.expandable ? (
|
||||
<ReadMoreButton
|
||||
symbol={ellipsisConfig.symbol}
|
||||
onClick={handleExpand}
|
||||
expanded={mergedExpanded}
|
||||
TitleComponent={TitleComponent}
|
||||
/>
|
||||
) : (
|
||||
ellipsisConfig.symbol
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<ParagraphComponent
|
||||
ref={containerRef}
|
||||
weight={weight}
|
||||
className={joinClassName(className)}
|
||||
style={mergedStyle}
|
||||
ellipsis={paragraphEllipsis}
|
||||
{...(props as ParagraphBodyProps)}
|
||||
>
|
||||
{buildParts()}
|
||||
</ParagraphComponent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ParagraphComponent
|
||||
ref={containerRef}
|
||||
weight={weight}
|
||||
className={joinClassName(className)}
|
||||
style={mergedStyle}
|
||||
{...(props as ParagraphBodyProps)}
|
||||
>
|
||||
{buildParts()}
|
||||
</ParagraphComponent>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ContentText.displayName = "ContentText";
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { ContentTextWithSuggestions } from "./content-text-with-suggestions";
|
||||
|
||||
interface ContentTitleWithSuggestionsProps
|
||||
extends Omit<ComponentProps<typeof ContentTextWithSuggestions>, "weight"> {}
|
||||
|
||||
export const ContentTitleWithSuggestions = ({
|
||||
text,
|
||||
ellipsis,
|
||||
blur = false,
|
||||
...rest
|
||||
}: ContentTitleWithSuggestionsProps) => {
|
||||
const normalizedEllipsis = ellipsis === undefined ? { rows: 2 } : ellipsis;
|
||||
const textProps = text === undefined ? {} : { text };
|
||||
|
||||
return (
|
||||
<ContentTextWithSuggestions
|
||||
weight="bold"
|
||||
blur={blur}
|
||||
ellipsis={normalizedEllipsis}
|
||||
{...textProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
27
packages/content-suggestions/src/react/index.tsx
Normal file
27
packages/content-suggestions/src/react/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export { ContentText, type ContentTextProps } from "./components/content-text";
|
||||
export {
|
||||
ContentTextWithSuggestions,
|
||||
} from "./components/content-text-with-suggestions";
|
||||
export {
|
||||
ContentTitleWithSuggestions,
|
||||
} from "./components/content-title-with-suggestions";
|
||||
|
||||
export type {
|
||||
BaseEntity,
|
||||
ContentEntity,
|
||||
LinkEntity,
|
||||
MentionEntity,
|
||||
ParsedMention,
|
||||
ProcessedContent,
|
||||
TagEntity,
|
||||
} from "../core";
|
||||
|
||||
export {
|
||||
findAllEntities,
|
||||
findLinks,
|
||||
findMentions,
|
||||
findTags,
|
||||
mentionLinkRegexp,
|
||||
parseMention,
|
||||
processContent,
|
||||
} from "../core";
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { MentionEntity, TagEntity } from "../core";
|
||||
import React from "react";
|
||||
|
||||
import { TachTypography } from "@hublib-web/tach-typography/react";
|
||||
import { ContentTextWithSuggestions } from "../react";
|
||||
|
||||
const DEMO_TEXT =
|
||||
"Пример текста с упоминанием @[Иван Петров](123e4567-e89b-12d3-a456-426614174000), тегом #frontend и ссылкой docs.example.com";
|
||||
|
||||
const meta: Meta<typeof ContentTextWithSuggestions> = {
|
||||
title: "React/ContentTextWithSuggestions",
|
||||
component: ContentTextWithSuggestions,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
text: DEMO_TEXT,
|
||||
blur: false,
|
||||
},
|
||||
argTypes: {
|
||||
text: { control: "text" },
|
||||
blur: { control: "boolean" },
|
||||
ellipsis: { control: false },
|
||||
renderMention: { control: false },
|
||||
renderTag: { control: false },
|
||||
renderLink: { control: false },
|
||||
ParagraphComponent: { control: false },
|
||||
TextComponent: { control: false },
|
||||
TitleComponent: { control: false },
|
||||
},
|
||||
render: args => (
|
||||
<div style={{ maxWidth: 780, width: "100%" }}>
|
||||
<ContentTextWithSuggestions {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
export const WithExpandableEllipsis: Story = {
|
||||
args: {
|
||||
ellipsis: {
|
||||
rows: 2,
|
||||
expandable: true,
|
||||
symbol: "Читать полностью",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomMentionAndTagRender: Story = {
|
||||
render: args => {
|
||||
const renderMention = (entity: MentionEntity) => (
|
||||
<TachTypography.Link.Body href={`https://example.com/users/${entity.userId ?? "unknown"}`} color="link">
|
||||
{entity.displayText}
|
||||
</TachTypography.Link.Body>
|
||||
);
|
||||
|
||||
const renderTag = (entity: TagEntity) => (
|
||||
<TachTypography.Link.Body href={`https://example.com/tags/${entity.tag}`} color="link">
|
||||
{entity.text}
|
||||
</TachTypography.Link.Body>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 780, width: "100%" }}>
|
||||
<ContentTextWithSuggestions
|
||||
{...args}
|
||||
renderMention={renderMention}
|
||||
renderTag={renderTag}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { ContentTitleWithSuggestions } from "../react";
|
||||
|
||||
const DEMO_TITLE =
|
||||
"Заголовок с @[Мария](123e4567-e89b-12d3-a456-426614174001), тегом #release и ссылкой github.com";
|
||||
|
||||
const meta: Meta<typeof ContentTitleWithSuggestions> = {
|
||||
title: "React/ContentTitleWithSuggestions",
|
||||
component: ContentTitleWithSuggestions,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
text: DEMO_TITLE,
|
||||
blur: false,
|
||||
ellipsis: {
|
||||
rows: 2,
|
||||
expandable: true,
|
||||
symbol: "Читать полностью",
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
text: { control: "text" },
|
||||
blur: { control: "boolean" },
|
||||
ellipsis: { control: false },
|
||||
renderMention: { control: false },
|
||||
renderTag: { control: false },
|
||||
renderLink: { control: false },
|
||||
ParagraphComponent: { control: false },
|
||||
TextComponent: { control: false },
|
||||
TitleComponent: { control: false },
|
||||
},
|
||||
render: args => (
|
||||
<div style={{ maxWidth: 780, width: "100%" }}>
|
||||
<ContentTitleWithSuggestions {...args} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Playground: Story = {};
|
||||
|
||||
export const Blurred: Story = {
|
||||
args: {
|
||||
blur: true,
|
||||
},
|
||||
};
|
||||
10
packages/content-suggestions/tsconfig.json
Normal file
10
packages/content-suggestions/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"types": ["node", "react"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
25
packages/content-suggestions/tsup.config.ts
Normal file
25
packages/content-suggestions/tsup.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
"core/index": "src/core/index.ts",
|
||||
"react/index": "src/react/index.tsx",
|
||||
"angular/index": "src/angular/index.ts",
|
||||
},
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
target: "es2022",
|
||||
minify: false,
|
||||
treeshake: true,
|
||||
splitting: false,
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"antd",
|
||||
"@angular/core",
|
||||
"@angular/common",
|
||||
"@hublib-web/tach-typography/react",
|
||||
],
|
||||
});
|
||||
22
packages/tach-typography/.storybook/main.ts
Normal file
22
packages/tach-typography/.storybook/main.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-a11y",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
core: {
|
||||
disableTelemetry: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
90
packages/tach-typography/.storybook/preview.css
Normal file
90
packages/tach-typography/.storybook/preview.css
Normal file
@@ -0,0 +1,90 @@
|
||||
:root {
|
||||
--Text-Primary: #0f172a;
|
||||
--Text-Secondary: #334155;
|
||||
--Text-Tertiary: #64748b;
|
||||
--Text-Quaternary: #94a3b8;
|
||||
--System-HashtagsInPost: #2563eb;
|
||||
--Default-White: #ffffff;
|
||||
--Default-Dark: #020617;
|
||||
--System-Alert: #dc2626;
|
||||
--Accent-Malahit: #16a34a;
|
||||
--System-Attantion: #d97706;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
.sb-show-main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.tach-story-surface {
|
||||
min-width: 320px;
|
||||
max-width: 920px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid #dbe1ea;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.tach-story-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tach-story-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tach-story-grid--colors {
|
||||
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
|
||||
}
|
||||
|
||||
.tach-story-row {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tach-story-label {
|
||||
color: #475569;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tach-story-token-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.tach-story-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tach-story-table th,
|
||||
.tach-story-table td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tach-story-table thead {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.tach-story-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
27
packages/tach-typography/.storybook/preview.ts
Normal file
27
packages/tach-typography/.storybook/preview.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
|
||||
import "antd/dist/reset.css";
|
||||
import "../src/styles/tach-typography.css";
|
||||
import "./preview.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
controls: {
|
||||
expanded: true,
|
||||
sort: "requiredFirst",
|
||||
},
|
||||
actions: {
|
||||
argTypesRegex: "^on[A-Z].*",
|
||||
},
|
||||
backgrounds: {
|
||||
default: "surface",
|
||||
values: [
|
||||
{ name: "surface", value: "#F5F7FB" },
|
||||
{ name: "dark", value: "#0F172A" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
121
packages/tach-typography/README.md
Normal file
121
packages/tach-typography/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# @hublib-web/tach-typography
|
||||
|
||||
Typography package with shared tokens and framework adapters:
|
||||
|
||||
- `react` adapter based on `antd/Typography`
|
||||
- `angular` adapter based on `ng-zorro-antd/typography`
|
||||
|
||||
## Install from Git (SSH tag)
|
||||
|
||||
```bash
|
||||
yarn add "@hublib-web/tach-typography@git+ssh://git@github.com/ORG/REPO.git#workspace=@hublib-web/tach-typography&tag=tach-typography-v0.2.0"
|
||||
```
|
||||
|
||||
## Install inside this monorepo
|
||||
|
||||
```bash
|
||||
yarn add @hublib-web/tach-typography
|
||||
```
|
||||
|
||||
## Release this package
|
||||
|
||||
1. Bump `version` in `packages/tach-typography/package.json`.
|
||||
2. Build package artifacts:
|
||||
|
||||
```bash
|
||||
yarn workspace @hublib-web/tach-typography build
|
||||
```
|
||||
|
||||
3. Commit release files:
|
||||
|
||||
```bash
|
||||
git add packages/tach-typography/package.json packages/tach-typography/dist
|
||||
git commit -m "release(tach-typography): v0.2.0"
|
||||
```
|
||||
|
||||
4. Create and push tag:
|
||||
|
||||
```bash
|
||||
git tag -a tach-typography-v0.2.0 -m "@hublib-web/tach-typography v0.2.0"
|
||||
git push origin main --follow-tags
|
||||
```
|
||||
|
||||
Detailed docs:
|
||||
|
||||
- [Release policy](../../docs/release-policy.md)
|
||||
- [Git installation](../../docs/git-installation.md)
|
||||
|
||||
## React usage (Ant Design)
|
||||
|
||||
```tsx
|
||||
import "@hublib-web/tach-typography/styles.css";
|
||||
import { TachTypography } from "@hublib-web/tach-typography/react";
|
||||
|
||||
export const Example = () => (
|
||||
<TachTypography.Text.Body color="link" weight="bold" ellipsis={{ rows: 1 }}>
|
||||
Hello from React + AntD
|
||||
</TachTypography.Text.Body>
|
||||
);
|
||||
```
|
||||
|
||||
Markdown as a prop (keeps existing API):
|
||||
|
||||
```tsx
|
||||
<TachTypography.Text.AccentH1 markdownEnabled>
|
||||
**Bold** _italic_ [Docs](https://example.com)
|
||||
</TachTypography.Text.AccentH1>
|
||||
```
|
||||
|
||||
## Angular usage (NG-ZORRO)
|
||||
|
||||
```ts
|
||||
import { Component } from "@angular/core";
|
||||
import { TachTypographyComponent } from "@hublib-web/tach-typography/angular";
|
||||
|
||||
@Component({
|
||||
selector: "app-example",
|
||||
standalone: true,
|
||||
imports: [TachTypographyComponent],
|
||||
template: `
|
||||
<tach-typography
|
||||
variant="Body"
|
||||
color="link"
|
||||
weight="bold"
|
||||
[nzProps]="{ nzCopyable: true, nzType: 'secondary' }"
|
||||
>
|
||||
Hello from Angular + NG-ZORRO
|
||||
</tach-typography>
|
||||
|
||||
<tach-typography
|
||||
as="a"
|
||||
variant="Body"
|
||||
color="link"
|
||||
[hostProps]="{ href: '/docs', target: '_blank', rel: 'noopener noreferrer' }"
|
||||
[nzProps]="{ nzType: 'secondary' }"
|
||||
>
|
||||
Open docs
|
||||
</tach-typography>
|
||||
`,
|
||||
})
|
||||
export class ExampleComponent {}
|
||||
```
|
||||
|
||||
Markdown in Angular:
|
||||
|
||||
```html
|
||||
<tach-typography variant="Body" [markdownEnabled]="true" [markdown]="'**Bold** _italic_'"></tach-typography>
|
||||
```
|
||||
|
||||
## Storybook (dev/design system)
|
||||
|
||||
Run from repository root:
|
||||
|
||||
```bash
|
||||
yarn workspace @hublib-web/tach-typography storybook
|
||||
```
|
||||
|
||||
Build static Storybook:
|
||||
|
||||
```bash
|
||||
yarn workspace @hublib-web/tach-typography storybook:build
|
||||
```
|
||||
85
packages/tach-typography/dist/angular/angular/index.d.ts
vendored
Normal file
85
packages/tach-typography/dist/angular/angular/index.d.ts
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ElementRef, EventEmitter, OnChanges, Renderer2, SimpleChanges } from "@angular/core";
|
||||
import { NzTypographyComponent } from "ng-zorro-antd/typography";
|
||||
import { type EllipsisOptions, type TypographyClassOptions, type TypographyColor, type TypographyRenderOptions, type TypographyVariant, type TypographyWeight } from "../core";
|
||||
import * as i0 from "@angular/core";
|
||||
import * as i1 from "ng-zorro-antd/typography";
|
||||
export type AngularTypographyClassInput = TypographyClassOptions;
|
||||
export interface AngularTypographyRenderOptions extends TypographyRenderOptions {
|
||||
preserveStyle?: Record<string, string | number>;
|
||||
}
|
||||
export type TachTypographyHostTag = "span" | "p" | "a" | "h1" | "h2" | "h3" | "h4";
|
||||
type NonFunctionNonEmitterKeys<T> = {
|
||||
[K in keyof T]-?: T[K] extends (...args: never[]) => unknown ? never : T[K] extends EventEmitter<unknown> ? never : K;
|
||||
}[keyof T];
|
||||
type NzTypographyInputKey = Extract<NonFunctionNonEmitterKeys<NzTypographyComponent>, `nz${string}`>;
|
||||
export type TachTypographyNzProps = Partial<Pick<NzTypographyComponent, NzTypographyInputKey>>;
|
||||
export type TachTypographyHostProps = Record<string, unknown>;
|
||||
export declare const tachAngularTypographyClassName: (options?: AngularTypographyClassInput) => string;
|
||||
export declare const tachAngularTypographyClassList: (options?: AngularTypographyClassInput) => string[];
|
||||
export declare const tachAngularTypographyStyles: (ellipsis?: EllipsisOptions, preserveStyle?: Record<string, string | number>) => Record<string, string | number>;
|
||||
export declare class TachTypographyDirective implements OnChanges {
|
||||
private readonly elementRef;
|
||||
private readonly renderer;
|
||||
tachTypography: TypographyVariant | "" | undefined;
|
||||
tachTypographyVariant: TypographyVariant;
|
||||
tachTypographyColor: TypographyColor;
|
||||
tachTypographyWeight: TypographyWeight;
|
||||
tachTypographyClickable: boolean;
|
||||
tachTypographyClassName: string | undefined;
|
||||
tachTypographyEllipsis: EllipsisOptions | undefined;
|
||||
private readonly appliedClasses;
|
||||
private readonly appliedStyleProperties;
|
||||
constructor(elementRef: ElementRef<HTMLElement>, renderer: Renderer2);
|
||||
ngOnChanges(_changes: SimpleChanges): void;
|
||||
private syncClasses;
|
||||
private syncEllipsisStyles;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyDirective, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyDirective, "[tachTypography]", never, { "tachTypography": { "alias": "tachTypography"; "required": false; }; "tachTypographyVariant": { "alias": "tachTypographyVariant"; "required": false; }; "tachTypographyColor": { "alias": "tachTypographyColor"; "required": false; }; "tachTypographyWeight": { "alias": "tachTypographyWeight"; "required": false; }; "tachTypographyClickable": { "alias": "tachTypographyClickable"; "required": false; }; "tachTypographyClassName": { "alias": "tachTypographyClassName"; "required": false; }; "tachTypographyEllipsis": { "alias": "tachTypographyEllipsis"; "required": false; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class TachTypographyNzPropsDirective implements OnChanges {
|
||||
tachTypographyNzProps: TachTypographyNzProps | null | undefined;
|
||||
private readonly appliedNzKeys;
|
||||
private readonly nzTypography;
|
||||
ngOnChanges(): void;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyNzPropsDirective, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyNzPropsDirective, "[tachTypographyNzProps]", never, { "tachTypographyNzProps": { "alias": "tachTypographyNzProps"; "required": false; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class TachTypographyHostPropsDirective implements OnChanges {
|
||||
private readonly elementRef;
|
||||
private readonly renderer;
|
||||
tachTypographyHostProps: TachTypographyHostProps | null | undefined;
|
||||
private readonly appliedHostProps;
|
||||
constructor(elementRef: ElementRef<HTMLElement>, renderer: Renderer2);
|
||||
ngOnChanges(): void;
|
||||
private shouldApplyAsAttribute;
|
||||
private hasPropertyOnElement;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyHostPropsDirective, never>;
|
||||
static ɵdir: i0.ɵɵDirectiveDeclaration<TachTypographyHostPropsDirective, "[tachTypographyHostProps]", never, { "tachTypographyHostProps": { "alias": "tachTypographyHostProps"; "required": false; }; }, {}, never, never, true, never>;
|
||||
}
|
||||
export declare class TachTypographyComponent implements OnChanges {
|
||||
hostTag: TachTypographyHostTag;
|
||||
variant: TypographyVariant;
|
||||
color: TypographyColor;
|
||||
weight: TypographyWeight;
|
||||
clickable: boolean;
|
||||
markdownEnabled: boolean;
|
||||
markdown: string | undefined;
|
||||
className: string | undefined;
|
||||
ellipsis: EllipsisOptions | undefined;
|
||||
nzProps: TachTypographyNzProps | undefined;
|
||||
hostProps: TachTypographyHostProps | undefined;
|
||||
preserveStyle: Record<string, string | number> | undefined;
|
||||
readonly tachClick: EventEmitter<MouseEvent>;
|
||||
renderedMarkdown: string;
|
||||
ngOnChanges(_changes: SimpleChanges): void;
|
||||
handleClick(event: MouseEvent): void;
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyComponent, never>;
|
||||
static ɵcmp: i0.ɵɵComponentDeclaration<TachTypographyComponent, "tach-typography", never, { "hostTag": { "alias": "as"; "required": false; }; "variant": { "alias": "variant"; "required": false; }; "color": { "alias": "color"; "required": false; }; "weight": { "alias": "weight"; "required": false; }; "clickable": { "alias": "clickable"; "required": false; }; "markdownEnabled": { "alias": "markdownEnabled"; "required": false; }; "markdown": { "alias": "markdown"; "required": false; }; "className": { "alias": "className"; "required": false; }; "ellipsis": { "alias": "ellipsis"; "required": false; }; "nzProps": { "alias": "nzProps"; "required": false; }; "hostProps": { "alias": "hostProps"; "required": false; }; "preserveStyle": { "alias": "preserveStyle"; "required": false; }; }, { "tachClick": "tachClick"; }, never, ["*", "*", "*", "*", "*", "*", "*"], true, never>;
|
||||
}
|
||||
export declare class TachTypographyNzModule {
|
||||
static ɵfac: i0.ɵɵFactoryDeclaration<TachTypographyNzModule, never>;
|
||||
static ɵmod: i0.ɵɵNgModuleDeclaration<TachTypographyNzModule, never, [typeof i1.NzTypographyModule, typeof TachTypographyDirective, typeof TachTypographyNzPropsDirective, typeof TachTypographyHostPropsDirective, typeof TachTypographyComponent], [typeof i1.NzTypographyModule, typeof TachTypographyDirective, typeof TachTypographyNzPropsDirective, typeof TachTypographyHostPropsDirective, typeof TachTypographyComponent]>;
|
||||
static ɵinj: i0.ɵɵInjectorDeclaration<TachTypographyNzModule>;
|
||||
}
|
||||
export {};
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/angular/index.d.ts.map
vendored
Normal file
1
packages/tach-typography/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":"AACA,OAAO,EAIL,UAAU,EACV,YAAY,EAIZ,SAAS,EAET,SAAS,EACT,aAAa,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,qBAAqB,EAAsB,MAAM,0BAA0B,CAAC;AAErF,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACtB,MAAM,SAAS,CAAC;;;AAEjB,MAAM,MAAM,2BAA2B,GAAG,sBAAsB,CAAC;AAEjE,MAAM,WAAW,8BAA+B,SAAQ,uBAAuB;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CACjD;AAED,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AACnF,KAAK,yBAAyB,CAAC,CAAC,IAAI;KACjC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,GACxD,KAAK,GACL,CAAC,CAAC,CAAC,CAAC,SAAS,YAAY,CAAC,OAAO,CAAC,GAChC,KAAK,GACL,CAAC;CACR,CAAC,MAAM,CAAC,CAAC,CAAC;AACX,KAAK,oBAAoB,GAAG,OAAO,CAAC,yBAAyB,CAAC,qBAAqB,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC,CAAC;AACrG,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,qBAAqB,EAAE,oBAAoB,CAAC,CAAC,CAAC;AAC/F,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAa9D,eAAO,MAAM,8BAA8B,GACzC,UAAS,2BAAgC,KACxC,MAEF,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACzC,UAAS,2BAAgC,KACxC,MAAM,EAER,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,WAAW,eAAe,EAC1B,gBAAe,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAM,KAClD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWhC,CAAC;AAEF,qBAIa,uBAAwB,YAAW,SAAS;IAarD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAblB,cAAc,EAAE,iBAAiB,GAAG,EAAE,GAAG,SAAS,CAAC;IACnD,qBAAqB,EAAE,iBAAiB,CAAU;IAClD,mBAAmB,EAAE,eAAe,CAAa;IACjD,oBAAoB,EAAE,gBAAgB,CAAY;IAClD,uBAAuB,UAAS;IAChC,uBAAuB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,sBAAsB,EAAE,eAAe,GAAG,SAAS,CAAC;IAE7D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;gBAGzC,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,QAAQ,EAAE,SAAS;IAGtC,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAK1C,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,kBAAkB;yCAjDf,uBAAuB;2CAAvB,uBAAuB;CAoEnC;AAED,qBAIa,8BAA+B,YAAW,SAAS;IACrD,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAAC;IAEzE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiE;IAE9F,WAAW,IAAI,IAAI;yCANR,8BAA8B;2CAA9B,8BAA8B;CAmC1C;AAED,qBAIa,gCAAiC,YAAW,SAAS;IAM9D,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IANlB,uBAAuB,EAAE,uBAAuB,GAAG,IAAI,GAAG,SAAS,CAAC;IAE7E,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;gBAGpD,UAAU,EAAE,UAAU,CAAC,WAAW,CAAC,EACnC,QAAQ,EAAE,SAAS;IAGtC,WAAW,IAAI,IAAI;IAkDnB,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,oBAAoB;yCAxEjB,gCAAgC;2CAAhC,gCAAgC;CA2E5C;AAED,qBAuKa,uBAAwB,YAAW,SAAS;IAC1C,OAAO,EAAE,qBAAqB,CAAU;IAC5C,OAAO,EAAE,iBAAiB,CAAU;IACpC,KAAK,EAAE,eAAe,CAAa;IACnC,MAAM,EAAE,gBAAgB,CAAY;IACpC,SAAS,UAAS;IAClB,eAAe,UAAS;IACxB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,eAAe,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,qBAAqB,GAAG,SAAS,CAAC;IAC3C,SAAS,EAAE,uBAAuB,GAAG,SAAS,CAAC;IAC/C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,SAAS,CAAC;IAE1D,QAAQ,CAAC,SAAS,2BAAkC;IAE9D,gBAAgB,SAAM;IAEtB,WAAW,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAM1C,WAAW,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;yCAxBzB,uBAAuB;2CAAvB,uBAAuB;CA2BnC;AAED,qBAgBa,sBAAsB;yCAAtB,sBAAsB;0CAAtB,sBAAsB,+CApZtB,uBAAuB,SA0EvB,8BAA8B,SAyC9B,gCAAgC,SAoPhC,uBAAuB,yCAvWvB,uBAAuB,SA0EvB,8BAA8B,SAyC9B,gCAAgC,SAoPhC,uBAAuB;0CA6CvB,sBAAsB;CAAG"}
|
||||
628
packages/tach-typography/dist/angular/angular/index.js
vendored
Normal file
628
packages/tach-typography/dist/angular/angular/index.js
vendored
Normal file
@@ -0,0 +1,628 @@
|
||||
import { NgIf, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, Directive, EventEmitter, inject, Input, NgModule, Output, } from "@angular/core";
|
||||
import { NzTypographyComponent, NzTypographyModule } from "ng-zorro-antd/typography";
|
||||
import { tachTypographyClassList, tachTypographyClassName, tachTypographyEllipsisStyle, tachTypographyMarkdownToHtml, } from "../core";
|
||||
import * as i0 from "@angular/core";
|
||||
import * as i1 from "ng-zorro-antd/typography";
|
||||
const camelToKebab = (value) => value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
|
||||
const toCssProperty = (styleKey) => {
|
||||
if (styleKey.startsWith("Webkit")) {
|
||||
return `-webkit-${camelToKebab(styleKey.slice(6))}`;
|
||||
}
|
||||
return camelToKebab(styleKey);
|
||||
};
|
||||
export const tachAngularTypographyClassName = (options = {}) => {
|
||||
return tachTypographyClassName(options);
|
||||
};
|
||||
export const tachAngularTypographyClassList = (options = {}) => {
|
||||
return tachTypographyClassList(options);
|
||||
};
|
||||
export const tachAngularTypographyStyles = (ellipsis, preserveStyle = {}) => {
|
||||
const ellipsisStyle = tachTypographyEllipsisStyle(ellipsis);
|
||||
if (!ellipsisStyle) {
|
||||
return preserveStyle;
|
||||
}
|
||||
return {
|
||||
...ellipsisStyle,
|
||||
...preserveStyle,
|
||||
};
|
||||
};
|
||||
export class TachTypographyDirective {
|
||||
elementRef;
|
||||
renderer;
|
||||
tachTypography;
|
||||
tachTypographyVariant = "Body";
|
||||
tachTypographyColor = "primary";
|
||||
tachTypographyWeight = "normal";
|
||||
tachTypographyClickable = false;
|
||||
tachTypographyClassName;
|
||||
tachTypographyEllipsis;
|
||||
appliedClasses = new Set();
|
||||
appliedStyleProperties = new Set();
|
||||
constructor(elementRef, renderer) {
|
||||
this.elementRef = elementRef;
|
||||
this.renderer = renderer;
|
||||
}
|
||||
ngOnChanges(_changes) {
|
||||
this.syncClasses();
|
||||
this.syncEllipsisStyles();
|
||||
}
|
||||
syncClasses() {
|
||||
const nextClassList = tachTypographyClassList({
|
||||
variant: this.tachTypography || this.tachTypographyVariant,
|
||||
color: this.tachTypographyColor,
|
||||
weight: this.tachTypographyWeight,
|
||||
clickable: this.tachTypographyClickable,
|
||||
className: this.tachTypographyClassName,
|
||||
});
|
||||
const nextSet = new Set(nextClassList);
|
||||
for (const className of this.appliedClasses) {
|
||||
if (!nextSet.has(className)) {
|
||||
this.renderer.removeClass(this.elementRef.nativeElement, className);
|
||||
}
|
||||
}
|
||||
for (const className of nextSet) {
|
||||
this.renderer.addClass(this.elementRef.nativeElement, className);
|
||||
}
|
||||
this.appliedClasses.clear();
|
||||
for (const className of nextSet) {
|
||||
this.appliedClasses.add(className);
|
||||
}
|
||||
}
|
||||
syncEllipsisStyles() {
|
||||
const nextStyles = tachTypographyEllipsisStyle(this.tachTypographyEllipsis) || {};
|
||||
const nextStyleKeys = new Set(Object.keys(nextStyles));
|
||||
for (const styleKey of this.appliedStyleProperties) {
|
||||
if (!nextStyleKeys.has(styleKey)) {
|
||||
this.renderer.removeStyle(this.elementRef.nativeElement, toCssProperty(styleKey));
|
||||
}
|
||||
}
|
||||
for (const [styleKey, styleValue] of Object.entries(nextStyles)) {
|
||||
this.renderer.setStyle(this.elementRef.nativeElement, toCssProperty(styleKey), styleValue);
|
||||
}
|
||||
this.appliedStyleProperties.clear();
|
||||
for (const styleKey of nextStyleKeys) {
|
||||
this.appliedStyleProperties.add(styleKey);
|
||||
}
|
||||
}
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
||||
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyDirective, isStandalone: true, selector: "[tachTypography]", inputs: { tachTypography: "tachTypography", tachTypographyVariant: "tachTypographyVariant", tachTypographyColor: "tachTypographyColor", tachTypographyWeight: "tachTypographyWeight", tachTypographyClickable: "tachTypographyClickable", tachTypographyClassName: "tachTypographyClassName", tachTypographyEllipsis: "tachTypographyEllipsis" }, usesOnChanges: true, ngImport: i0 });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyDirective, decorators: [{
|
||||
type: Directive,
|
||||
args: [{
|
||||
selector: "[tachTypography]",
|
||||
standalone: true,
|
||||
}]
|
||||
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { tachTypography: [{
|
||||
type: Input
|
||||
}], tachTypographyVariant: [{
|
||||
type: Input
|
||||
}], tachTypographyColor: [{
|
||||
type: Input
|
||||
}], tachTypographyWeight: [{
|
||||
type: Input
|
||||
}], tachTypographyClickable: [{
|
||||
type: Input
|
||||
}], tachTypographyClassName: [{
|
||||
type: Input
|
||||
}], tachTypographyEllipsis: [{
|
||||
type: Input
|
||||
}] } });
|
||||
export class TachTypographyNzPropsDirective {
|
||||
tachTypographyNzProps;
|
||||
appliedNzKeys = new Set();
|
||||
nzTypography = inject(NzTypographyComponent, { self: true, optional: true });
|
||||
ngOnChanges() {
|
||||
if (!this.nzTypography) {
|
||||
return;
|
||||
}
|
||||
const nzTypography = this.nzTypography;
|
||||
const nextProps = this.tachTypographyNzProps ?? {};
|
||||
const nextKeys = new Set();
|
||||
for (const [key, value] of Object.entries(nextProps)) {
|
||||
if (!key.startsWith("nz")) {
|
||||
continue;
|
||||
}
|
||||
nzTypography[key] = value;
|
||||
nextKeys.add(key);
|
||||
}
|
||||
for (const key of this.appliedNzKeys) {
|
||||
if (!nextKeys.has(key)) {
|
||||
nzTypography[key] = undefined;
|
||||
}
|
||||
}
|
||||
this.appliedNzKeys.clear();
|
||||
for (const key of nextKeys) {
|
||||
this.appliedNzKeys.add(key);
|
||||
}
|
||||
}
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzPropsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
||||
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyNzPropsDirective, isStandalone: true, selector: "[tachTypographyNzProps]", inputs: { tachTypographyNzProps: "tachTypographyNzProps" }, usesOnChanges: true, ngImport: i0 });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzPropsDirective, decorators: [{
|
||||
type: Directive,
|
||||
args: [{
|
||||
selector: "[tachTypographyNzProps]",
|
||||
standalone: true,
|
||||
}]
|
||||
}], propDecorators: { tachTypographyNzProps: [{
|
||||
type: Input
|
||||
}] } });
|
||||
export class TachTypographyHostPropsDirective {
|
||||
elementRef;
|
||||
renderer;
|
||||
tachTypographyHostProps;
|
||||
appliedHostProps = new Map();
|
||||
constructor(elementRef, renderer) {
|
||||
this.elementRef = elementRef;
|
||||
this.renderer = renderer;
|
||||
}
|
||||
ngOnChanges() {
|
||||
const nextProps = this.tachTypographyHostProps ?? {};
|
||||
const nextAppliedProps = new Map();
|
||||
for (const [key, value] of Object.entries(nextProps)) {
|
||||
if (key === "class" || key === "className" || key === "style") {
|
||||
continue;
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
const applyAsAttr = this.shouldApplyAsAttribute(key, value);
|
||||
if (applyAsAttr) {
|
||||
if (typeof value === "boolean") {
|
||||
if (value) {
|
||||
this.renderer.setAttribute(this.elementRef.nativeElement, key, "");
|
||||
}
|
||||
else {
|
||||
this.renderer.removeAttribute(this.elementRef.nativeElement, key);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.renderer.setAttribute(this.elementRef.nativeElement, key, String(value));
|
||||
}
|
||||
nextAppliedProps.set(key, "attr");
|
||||
continue;
|
||||
}
|
||||
this.renderer.setProperty(this.elementRef.nativeElement, key, value);
|
||||
nextAppliedProps.set(key, "prop");
|
||||
}
|
||||
for (const [key, kind] of this.appliedHostProps.entries()) {
|
||||
if (nextAppliedProps.has(key)) {
|
||||
continue;
|
||||
}
|
||||
if (kind === "attr") {
|
||||
this.renderer.removeAttribute(this.elementRef.nativeElement, key);
|
||||
}
|
||||
else {
|
||||
this.renderer.setProperty(this.elementRef.nativeElement, key, undefined);
|
||||
}
|
||||
}
|
||||
this.appliedHostProps.clear();
|
||||
for (const [key, kind] of nextAppliedProps.entries()) {
|
||||
this.appliedHostProps.set(key, kind);
|
||||
}
|
||||
}
|
||||
shouldApplyAsAttribute(key, value) {
|
||||
if (key.startsWith("data-") || key.startsWith("aria-")) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return !this.hasPropertyOnElement(key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
hasPropertyOnElement(key) {
|
||||
return key in this.elementRef.nativeElement;
|
||||
}
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyHostPropsDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
|
||||
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyHostPropsDirective, isStandalone: true, selector: "[tachTypographyHostProps]", inputs: { tachTypographyHostProps: "tachTypographyHostProps" }, usesOnChanges: true, ngImport: i0 });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyHostPropsDirective, decorators: [{
|
||||
type: Directive,
|
||||
args: [{
|
||||
selector: "[tachTypographyHostProps]",
|
||||
standalone: true,
|
||||
}]
|
||||
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { tachTypographyHostProps: [{
|
||||
type: Input
|
||||
}] } });
|
||||
export class TachTypographyComponent {
|
||||
hostTag = "span";
|
||||
variant = "Body";
|
||||
color = "primary";
|
||||
weight = "normal";
|
||||
clickable = false;
|
||||
markdownEnabled = false;
|
||||
markdown;
|
||||
className;
|
||||
ellipsis;
|
||||
nzProps;
|
||||
hostProps;
|
||||
preserveStyle;
|
||||
tachClick = new EventEmitter();
|
||||
renderedMarkdown = "";
|
||||
ngOnChanges(_changes) {
|
||||
this.renderedMarkdown = this.markdownEnabled
|
||||
? tachTypographyMarkdownToHtml(this.markdown ?? "")
|
||||
: "";
|
||||
}
|
||||
handleClick(event) {
|
||||
this.tachClick.emit(event);
|
||||
}
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
||||
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: TachTypographyComponent, isStandalone: true, selector: "tach-typography", inputs: { hostTag: ["as", "hostTag"], variant: "variant", color: "color", weight: "weight", clickable: "clickable", markdownEnabled: "markdownEnabled", markdown: "markdown", className: "className", ellipsis: "ellipsis", nzProps: "nzProps", hostProps: "hostProps", preserveStyle: "preserveStyle" }, outputs: { tachClick: "tachClick" }, usesOnChanges: true, ngImport: i0, template: `
|
||||
<ng-container [ngSwitch]="hostTag">
|
||||
<p
|
||||
*ngSwitchCase="'p'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentP">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentP>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</p>
|
||||
<a
|
||||
*ngSwitchCase="'a'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentA">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentA>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</a>
|
||||
<h1
|
||||
*ngSwitchCase="'h1'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH1">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH1>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h1>
|
||||
<h2
|
||||
*ngSwitchCase="'h2'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH2">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH2>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h2>
|
||||
<h3
|
||||
*ngSwitchCase="'h3'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH3">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH3>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h3>
|
||||
<h4
|
||||
*ngSwitchCase="'h4'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH4">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH4>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h4>
|
||||
<span
|
||||
*ngSwitchDefault
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentSpan">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentSpan>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</span>
|
||||
</ng-container>
|
||||
`, isInline: true, dependencies: [{ kind: "ngmodule", type: NzTypographyModule }, { kind: "component", type: i1.NzTypographyComponent, selector: " nz-typography, [nz-typography], p[nz-paragraph], span[nz-text], h1[nz-title], h2[nz-title], h3[nz-title], h4[nz-title] ", inputs: ["nzCopyable", "nzEditable", "nzDisabled", "nzExpandable", "nzEllipsis", "nzCopyTooltips", "nzCopyIcons", "nzEditTooltip", "nzEditIcon", "nzContent", "nzEllipsisRows", "nzType", "nzCopyText", "nzSuffix"], outputs: ["nzContentChange", "nzCopy", "nzExpandChange", "nzOnEllipsis"], exportAs: ["nzTypography"] }, { kind: "directive", type: TachTypographyDirective, selector: "[tachTypography]", inputs: ["tachTypography", "tachTypographyVariant", "tachTypographyColor", "tachTypographyWeight", "tachTypographyClickable", "tachTypographyClassName", "tachTypographyEllipsis"] }, { kind: "directive", type: TachTypographyNzPropsDirective, selector: "[tachTypographyNzProps]", inputs: ["tachTypographyNzProps"] }, { kind: "directive", type: TachTypographyHostPropsDirective, selector: "[tachTypographyHostProps]", inputs: ["tachTypographyHostProps"] }, { kind: "directive", type: NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyComponent, decorators: [{
|
||||
type: Component,
|
||||
args: [{
|
||||
selector: "tach-typography",
|
||||
standalone: true,
|
||||
imports: [
|
||||
NzTypographyModule,
|
||||
TachTypographyDirective,
|
||||
TachTypographyNzPropsDirective,
|
||||
TachTypographyHostPropsDirective,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgSwitchDefault,
|
||||
NgIf,
|
||||
NgStyle,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<ng-container [ngSwitch]="hostTag">
|
||||
<p
|
||||
*ngSwitchCase="'p'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentP">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentP>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</p>
|
||||
<a
|
||||
*ngSwitchCase="'a'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentA">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentA>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</a>
|
||||
<h1
|
||||
*ngSwitchCase="'h1'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH1">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH1>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h1>
|
||||
<h2
|
||||
*ngSwitchCase="'h2'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH2">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH2>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h2>
|
||||
<h3
|
||||
*ngSwitchCase="'h3'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH3">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH3>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h3>
|
||||
<h4
|
||||
*ngSwitchCase="'h4'"
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentH4">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentH4>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</h4>
|
||||
<span
|
||||
*ngSwitchDefault
|
||||
nz-typography
|
||||
[tachTypography]="variant"
|
||||
[tachTypographyColor]="color"
|
||||
[tachTypographyWeight]="weight"
|
||||
[tachTypographyClickable]="clickable"
|
||||
[tachTypographyClassName]="className"
|
||||
[tachTypographyEllipsis]="ellipsis"
|
||||
[tachTypographyNzProps]="nzProps"
|
||||
[tachTypographyHostProps]="hostProps"
|
||||
[ngStyle]="preserveStyle"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-container *ngIf="markdownEnabled; else tachTypographyContentSpan">
|
||||
<span [innerHTML]="renderedMarkdown"></span>
|
||||
</ng-container>
|
||||
<ng-template #tachTypographyContentSpan>
|
||||
<ng-content />
|
||||
</ng-template>
|
||||
</span>
|
||||
</ng-container>
|
||||
`,
|
||||
}]
|
||||
}], propDecorators: { hostTag: [{
|
||||
type: Input,
|
||||
args: ["as"]
|
||||
}], variant: [{
|
||||
type: Input
|
||||
}], color: [{
|
||||
type: Input
|
||||
}], weight: [{
|
||||
type: Input
|
||||
}], clickable: [{
|
||||
type: Input
|
||||
}], markdownEnabled: [{
|
||||
type: Input
|
||||
}], markdown: [{
|
||||
type: Input
|
||||
}], className: [{
|
||||
type: Input
|
||||
}], ellipsis: [{
|
||||
type: Input
|
||||
}], nzProps: [{
|
||||
type: Input
|
||||
}], hostProps: [{
|
||||
type: Input
|
||||
}], preserveStyle: [{
|
||||
type: Input
|
||||
}], tachClick: [{
|
||||
type: Output
|
||||
}] } });
|
||||
export class TachTypographyNzModule {
|
||||
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
||||
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, imports: [NzTypographyModule, TachTypographyDirective, TachTypographyNzPropsDirective, TachTypographyHostPropsDirective, TachTypographyComponent], exports: [NzTypographyModule, TachTypographyDirective, TachTypographyNzPropsDirective, TachTypographyHostPropsDirective, TachTypographyComponent] });
|
||||
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, imports: [NzTypographyModule,
|
||||
TachTypographyComponent, NzTypographyModule] });
|
||||
}
|
||||
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: TachTypographyNzModule, decorators: [{
|
||||
type: NgModule,
|
||||
args: [{
|
||||
imports: [
|
||||
NzTypographyModule,
|
||||
TachTypographyDirective,
|
||||
TachTypographyNzPropsDirective,
|
||||
TachTypographyHostPropsDirective,
|
||||
TachTypographyComponent,
|
||||
],
|
||||
exports: [
|
||||
NzTypographyModule,
|
||||
TachTypographyDirective,
|
||||
TachTypographyNzPropsDirective,
|
||||
TachTypographyHostPropsDirective,
|
||||
TachTypographyComponent,
|
||||
],
|
||||
}]
|
||||
}] });
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/tach-typography/dist/angular/angular/index.js.map
vendored
Normal file
1
packages/tach-typography/dist/angular/angular/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
packages/tach-typography/dist/angular/angular/public-api.d.ts
vendored
Normal file
2
packages/tach-typography/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/tach-typography/dist/angular/angular/public-api.d.ts.map
vendored
Normal file
1
packages/tach-typography/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/tach-typography/dist/angular/angular/public-api.js
vendored
Normal file
2
packages/tach-typography/dist/angular/angular/public-api.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./index";
|
||||
//# sourceMappingURL=public-api.js.map
|
||||
1
packages/tach-typography/dist/angular/angular/public-api.js.map
vendored
Normal file
1
packages/tach-typography/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"}
|
||||
4
packages/tach-typography/dist/angular/core/classnames.d.ts
vendored
Normal file
4
packages/tach-typography/dist/angular/core/classnames.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { TypographyClassOptions } from "./types";
|
||||
export declare const tachTypographyClassName: ({ variant, color, weight, clickable, className, }?: TypographyClassOptions) => string;
|
||||
export declare const tachTypographyClassList: (options?: TypographyClassOptions) => string[];
|
||||
//# sourceMappingURL=classnames.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/core/classnames.d.ts.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/classnames.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"classnames.d.ts","sourceRoot":"","sources":["../../../src/core/classnames.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAOtD,eAAO,MAAM,uBAAuB,GAAI,oDAMrC,sBAA2B,KAAG,MAShC,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,UAAS,sBAA2B,KAAG,MAAM,EAIpF,CAAC"}
|
||||
11
packages/tach-typography/dist/angular/core/classnames.js
vendored
Normal file
11
packages/tach-typography/dist/angular/core/classnames.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
const BASE_CLASS = "tach-typography";
|
||||
const join = (...parts) => parts.filter(Boolean).join(" ");
|
||||
export const tachTypographyClassName = ({ variant = "Body", color = "primary", weight = "normal", clickable = false, className, } = {}) => {
|
||||
return join(BASE_CLASS, `${BASE_CLASS}--${variant}`, `${BASE_CLASS}--color-${color}`, weight === "bold" && `${BASE_CLASS}--bold`, clickable && `${BASE_CLASS}--pointer`, className);
|
||||
};
|
||||
export const tachTypographyClassList = (options = {}) => {
|
||||
return tachTypographyClassName(options)
|
||||
.split(" ")
|
||||
.filter(Boolean);
|
||||
};
|
||||
//# sourceMappingURL=classnames.js.map
|
||||
1
packages/tach-typography/dist/angular/core/classnames.js.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/classnames.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"classnames.js","sourceRoot":"","sources":["../../../src/core/classnames.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC,MAAM,IAAI,GAAG,CAAC,GAAG,KAA+C,EAAU,EAAE,CAC1E,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAElC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,EACtC,OAAO,GAAG,MAAM,EAChB,KAAK,GAAG,SAAS,EACjB,MAAM,GAAG,QAAQ,EACjB,SAAS,GAAG,KAAK,EACjB,SAAS,MACiB,EAAE,EAAU,EAAE;IACxC,OAAO,IAAI,CACT,UAAU,EACV,GAAG,UAAU,KAAK,OAAO,EAAE,EAC3B,GAAG,UAAU,WAAW,KAAK,EAAE,EAC/B,MAAM,KAAK,MAAM,IAAI,GAAG,UAAU,QAAQ,EAC1C,SAAS,IAAI,GAAG,UAAU,WAAW,EACrC,SAAS,CACV,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,UAAkC,EAAE,EAAY,EAAE;IACxF,OAAO,uBAAuB,CAAC,OAAO,CAAC;SACpC,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC,CAAC"}
|
||||
5
packages/tach-typography/dist/angular/core/ellipsis.d.ts
vendored
Normal file
5
packages/tach-typography/dist/angular/core/ellipsis.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { EllipsisOptions } from "./types";
|
||||
type StyleRecord = Record<string, string | number>;
|
||||
export declare const tachTypographyEllipsisStyle: (ellipsis?: EllipsisOptions) => StyleRecord | undefined;
|
||||
export {};
|
||||
//# sourceMappingURL=ellipsis.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/core/ellipsis.d.ts.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/ellipsis.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ellipsis.d.ts","sourceRoot":"","sources":["../../../src/core/ellipsis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,2BAA2B,GACtC,WAAW,eAAe,KACzB,WAAW,GAAG,SAchB,CAAC"}
|
||||
14
packages/tach-typography/dist/angular/core/ellipsis.js
vendored
Normal file
14
packages/tach-typography/dist/angular/core/ellipsis.js
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export const tachTypographyEllipsisStyle = (ellipsis) => {
|
||||
if (!ellipsis) {
|
||||
return undefined;
|
||||
}
|
||||
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
|
||||
return {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: rows,
|
||||
};
|
||||
};
|
||||
//# sourceMappingURL=ellipsis.js.map
|
||||
1
packages/tach-typography/dist/angular/core/ellipsis.js.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/ellipsis.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ellipsis.js","sourceRoot":"","sources":["../../../src/core/ellipsis.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,QAA0B,EACD,EAAE;IAC3B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnE,OAAO;QACL,QAAQ,EAAE,QAAQ;QAClB,YAAY,EAAE,UAAU;QACxB,OAAO,EAAE,aAAa;QACtB,eAAe,EAAE,UAAU;QAC3B,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC,CAAC"}
|
||||
5
packages/tach-typography/dist/angular/core/index.d.ts
vendored
Normal file
5
packages/tach-typography/dist/angular/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./types";
|
||||
export * from "./classnames";
|
||||
export * from "./ellipsis";
|
||||
export * from "./markdown";
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/core/index.d.ts.map
vendored
Normal file
1
packages/tach-typography/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,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
|
||||
5
packages/tach-typography/dist/angular/core/index.js
vendored
Normal file
5
packages/tach-typography/dist/angular/core/index.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./types";
|
||||
export * from "./classnames";
|
||||
export * from "./ellipsis";
|
||||
export * from "./markdown";
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
packages/tach-typography/dist/angular/core/index.js.map
vendored
Normal file
1
packages/tach-typography/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":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
|
||||
2
packages/tach-typography/dist/angular/core/markdown.d.ts
vendored
Normal file
2
packages/tach-typography/dist/angular/core/markdown.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export declare const tachTypographyMarkdownToHtml: (markdown: string) => string;
|
||||
//# sourceMappingURL=markdown.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/core/markdown.d.ts.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/markdown.d.ts.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../src/core/markdown.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,4BAA4B,GAAI,UAAU,MAAM,KAAG,MAyC/D,CAAC"}
|
||||
47
packages/tach-typography/dist/angular/core/markdown.js
vendored
Normal file
47
packages/tach-typography/dist/angular/core/markdown.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
const TOKEN_PREFIX = "TACHMDTOKEN";
|
||||
const SAFE_HREF_PATTERN = /^(https?:|mailto:|tel:|\/|#)/i;
|
||||
const escapeHtml = (value) => value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
|
||||
const sanitizeHref = (value) => {
|
||||
const href = value.trim();
|
||||
if (!href || !SAFE_HREF_PATTERN.test(href)) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
};
|
||||
export const tachTypographyMarkdownToHtml = (markdown) => {
|
||||
const source = normalizeMarkdown(markdown);
|
||||
const tokenMap = new Map();
|
||||
let tokenId = 0;
|
||||
const tokenized = source
|
||||
.replace(/`([^`\n]+)`/g, (_match, code) => {
|
||||
const token = `${TOKEN_PREFIX}${tokenId++}`;
|
||||
tokenMap.set(token, `<code class="tach-typography__md-code">${escapeHtml(code)}</code>`);
|
||||
return token;
|
||||
})
|
||||
.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, (_match, label, hrefRaw) => {
|
||||
const token = `${TOKEN_PREFIX}${tokenId++}`;
|
||||
const href = sanitizeHref(hrefRaw);
|
||||
if (!href) {
|
||||
tokenMap.set(token, escapeHtml(label));
|
||||
return token;
|
||||
}
|
||||
tokenMap.set(token, `<a class="tach-typography__md-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`);
|
||||
return token;
|
||||
});
|
||||
let html = escapeHtml(tokenized)
|
||||
.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/__([^_\n]+)__/g, "<strong>$1</strong>")
|
||||
.replace(/\*([^*\n]+)\*/g, "<em>$1</em>")
|
||||
.replace(/_([^_\n]+)_/g, "<em>$1</em>")
|
||||
.replace(/~~([^~\n]+)~~/g, "<del>$1</del>")
|
||||
.replace(/\n/g, "<br />");
|
||||
html = html.replace(new RegExp(`${TOKEN_PREFIX}\\d+`, "g"), token => tokenMap.get(token) || token);
|
||||
return html;
|
||||
};
|
||||
//# sourceMappingURL=markdown.js.map
|
||||
1
packages/tach-typography/dist/angular/core/markdown.js.map
vendored
Normal file
1
packages/tach-typography/dist/angular/core/markdown.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../../src/core/markdown.ts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,aAAa,CAAC;AACnC,MAAM,iBAAiB,GAAG,+BAA+B,CAAC;AAE1D,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE,CAC3C,KAAK;KACF,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;KACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;KACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;KACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;KACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAE5B,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAEnF,MAAM,YAAY,GAAG,CAAC,KAAa,EAAiB,EAAE;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE1B,IAAI,CAAC,IAAI,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,QAAgB,EAAU,EAAE;IACvE,MAAM,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,MAAM,SAAS,GAAG,MAAM;SACrB,OAAO,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE;QAChD,MAAM,KAAK,GAAG,GAAG,YAAY,GAAG,OAAO,EAAE,EAAE,CAAC;QAC5C,QAAQ,CAAC,GAAG,CACV,KAAK,EACL,0CAA0C,UAAU,CAAC,IAAI,CAAC,SAAS,CACpE,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;SACD,OAAO,CAAC,8BAA8B,EAAE,CAAC,MAAM,EAAE,KAAa,EAAE,OAAe,EAAE,EAAE;QAClF,MAAM,KAAK,GAAG,GAAG,YAAY,GAAG,OAAO,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QAEnC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;YACvC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,QAAQ,CAAC,GAAG,CACV,KAAK,EACL,6CAA6C,UAAU,CAAC,IAAI,CAAC,+CAA+C,UAAU,CAAC,KAAK,CAAC,MAAM,CACpI,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;IAEL,IAAI,IAAI,GAAG,UAAU,CAAC,SAAS,CAAC;SAC7B,OAAO,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;SACpD,OAAO,CAAC,gBAAgB,EAAE,qBAAqB,CAAC;SAChD,OAAO,CAAC,gBAAgB,EAAE,aAAa,CAAC;SACxC,OAAO,CAAC,cAAc,EAAE,aAAa,CAAC;SACtC,OAAO,CAAC,gBAAgB,EAAE,eAAe,CAAC;SAC1C,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAE5B,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,GAAG,YAAY,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC;IAEnG,OAAO,IAAI,CAAC;AACd,CAAC,CAAC"}
|
||||
19
packages/tach-typography/dist/angular/core/types.d.ts
vendored
Normal file
19
packages/tach-typography/dist/angular/core/types.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
export declare const TYPOGRAPHY_VARIANTS: readonly ["LargeTitle", "Title1", "Title2", "Title3", "Headline", "Body", "Inputs", "Subheadline", "FootnoteUnderline", "Footnote", "Caption", "Caption2", "AccentH1", "AccentH2", "AccentSubttl", "AccentSubttl2", "AccentCaption", "AccentCaption2", "AccentRegularM", "AccentRegularS", "AccentLargeTtl", "AppMediumBody", "AppMediumSubtext", "AppMediumSubtextUnderline"];
|
||||
export declare const TYPOGRAPHY_COLORS: readonly ["primary", "secondary", "tertiary", "quaternary", "link", "white", "dark", "alert", "malahit", "attantion"];
|
||||
export type TypographyVariant = (typeof TYPOGRAPHY_VARIANTS)[number];
|
||||
export type TypographyColor = (typeof TYPOGRAPHY_COLORS)[number];
|
||||
export type TypographyWeight = "normal" | "bold";
|
||||
export interface TypographyClassOptions {
|
||||
variant?: TypographyVariant;
|
||||
color?: TypographyColor;
|
||||
weight?: TypographyWeight;
|
||||
clickable?: boolean;
|
||||
className?: string | undefined;
|
||||
}
|
||||
export type EllipsisOptions = boolean | {
|
||||
rows?: number;
|
||||
};
|
||||
export interface TypographyRenderOptions extends TypographyClassOptions {
|
||||
ellipsis?: EllipsisOptions;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
1
packages/tach-typography/dist/angular/core/types.d.ts.map
vendored
Normal file
1
packages/tach-typography/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,eAAO,MAAM,mBAAmB,gXAyBtB,CAAC;AAEX,eAAO,MAAM,iBAAiB,uHAWpB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACrE,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEjD,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,MAAM,eAAe,GACvB,OAAO,GACP;IACE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEN,MAAM,WAAW,uBAAwB,SAAQ,sBAAsB;IACrE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B"}
|
||||
39
packages/tach-typography/dist/angular/core/types.js
vendored
Normal file
39
packages/tach-typography/dist/angular/core/types.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
export const TYPOGRAPHY_VARIANTS = [
|
||||
"LargeTitle",
|
||||
"Title1",
|
||||
"Title2",
|
||||
"Title3",
|
||||
"Headline",
|
||||
"Body",
|
||||
"Inputs",
|
||||
"Subheadline",
|
||||
"FootnoteUnderline",
|
||||
"Footnote",
|
||||
"Caption",
|
||||
"Caption2",
|
||||
"AccentH1",
|
||||
"AccentH2",
|
||||
"AccentSubttl",
|
||||
"AccentSubttl2",
|
||||
"AccentCaption",
|
||||
"AccentCaption2",
|
||||
"AccentRegularM",
|
||||
"AccentRegularS",
|
||||
"AccentLargeTtl",
|
||||
"AppMediumBody",
|
||||
"AppMediumSubtext",
|
||||
"AppMediumSubtextUnderline",
|
||||
];
|
||||
export const TYPOGRAPHY_COLORS = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"tertiary",
|
||||
"quaternary",
|
||||
"link",
|
||||
"white",
|
||||
"dark",
|
||||
"alert",
|
||||
"malahit",
|
||||
"attantion",
|
||||
];
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
packages/tach-typography/dist/angular/core/types.js.map
vendored
Normal file
1
packages/tach-typography/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":"AAAA,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,YAAY;IACZ,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,MAAM;IACN,QAAQ;IACR,aAAa;IACb,mBAAmB;IACnB,UAAU;IACV,SAAS;IACT,UAAU;IACV,UAAU;IACV,UAAU;IACV,cAAc;IACd,eAAe;IACf,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,eAAe;IACf,kBAAkB;IAClB,2BAA2B;CACnB,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,SAAS;IACT,WAAW;IACX,UAAU;IACV,YAAY;IACZ,MAAM;IACN,OAAO;IACP,MAAM;IACN,OAAO;IACP,SAAS;IACT,WAAW;CACH,CAAC"}
|
||||
1
packages/tach-typography/dist/angular/index.d.ts
vendored
Normal file
1
packages/tach-typography/dist/angular/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./angular/index";
|
||||
1
packages/tach-typography/dist/angular/index.js
vendored
Normal file
1
packages/tach-typography/dist/angular/index.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./angular/index.js";
|
||||
129
packages/tach-typography/dist/core/index.cjs
vendored
Normal file
129
packages/tach-typography/dist/core/index.cjs
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
// src/core/types.ts
|
||||
var TYPOGRAPHY_VARIANTS = [
|
||||
"LargeTitle",
|
||||
"Title1",
|
||||
"Title2",
|
||||
"Title3",
|
||||
"Headline",
|
||||
"Body",
|
||||
"Inputs",
|
||||
"Subheadline",
|
||||
"FootnoteUnderline",
|
||||
"Footnote",
|
||||
"Caption",
|
||||
"Caption2",
|
||||
"AccentH1",
|
||||
"AccentH2",
|
||||
"AccentSubttl",
|
||||
"AccentSubttl2",
|
||||
"AccentCaption",
|
||||
"AccentCaption2",
|
||||
"AccentRegularM",
|
||||
"AccentRegularS",
|
||||
"AccentLargeTtl",
|
||||
"AppMediumBody",
|
||||
"AppMediumSubtext",
|
||||
"AppMediumSubtextUnderline"
|
||||
];
|
||||
var TYPOGRAPHY_COLORS = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"tertiary",
|
||||
"quaternary",
|
||||
"link",
|
||||
"white",
|
||||
"dark",
|
||||
"alert",
|
||||
"malahit",
|
||||
"attantion"
|
||||
];
|
||||
|
||||
// src/core/classnames.ts
|
||||
var BASE_CLASS = "tach-typography";
|
||||
var join = (...parts) => parts.filter(Boolean).join(" ");
|
||||
var tachTypographyClassName = ({
|
||||
variant = "Body",
|
||||
color = "primary",
|
||||
weight = "normal",
|
||||
clickable = false,
|
||||
className
|
||||
} = {}) => {
|
||||
return join(
|
||||
BASE_CLASS,
|
||||
`${BASE_CLASS}--${variant}`,
|
||||
`${BASE_CLASS}--color-${color}`,
|
||||
weight === "bold" && `${BASE_CLASS}--bold`,
|
||||
clickable && `${BASE_CLASS}--pointer`,
|
||||
className
|
||||
);
|
||||
};
|
||||
var tachTypographyClassList = (options = {}) => {
|
||||
return tachTypographyClassName(options).split(" ").filter(Boolean);
|
||||
};
|
||||
|
||||
// src/core/ellipsis.ts
|
||||
var tachTypographyEllipsisStyle = (ellipsis) => {
|
||||
if (!ellipsis) {
|
||||
return void 0;
|
||||
}
|
||||
const rows = typeof ellipsis === "object" ? ellipsis.rows ?? 1 : 1;
|
||||
return {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: rows
|
||||
};
|
||||
};
|
||||
|
||||
// src/core/markdown.ts
|
||||
var TOKEN_PREFIX = "TACHMDTOKEN";
|
||||
var SAFE_HREF_PATTERN = /^(https?:|mailto:|tel:|\/|#)/i;
|
||||
var escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
var normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
|
||||
var sanitizeHref = (value) => {
|
||||
const href = value.trim();
|
||||
if (!href || !SAFE_HREF_PATTERN.test(href)) {
|
||||
return null;
|
||||
}
|
||||
return href;
|
||||
};
|
||||
var tachTypographyMarkdownToHtml = (markdown) => {
|
||||
const source = normalizeMarkdown(markdown);
|
||||
const tokenMap = /* @__PURE__ */ new Map();
|
||||
let tokenId = 0;
|
||||
const tokenized = source.replace(/`([^`\n]+)`/g, (_match, code) => {
|
||||
const token = `${TOKEN_PREFIX}${tokenId++}`;
|
||||
tokenMap.set(
|
||||
token,
|
||||
`<code class="tach-typography__md-code">${escapeHtml(code)}</code>`
|
||||
);
|
||||
return token;
|
||||
}).replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, (_match, label, hrefRaw) => {
|
||||
const token = `${TOKEN_PREFIX}${tokenId++}`;
|
||||
const href = sanitizeHref(hrefRaw);
|
||||
if (!href) {
|
||||
tokenMap.set(token, escapeHtml(label));
|
||||
return token;
|
||||
}
|
||||
tokenMap.set(
|
||||
token,
|
||||
`<a class="tach-typography__md-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`
|
||||
);
|
||||
return token;
|
||||
});
|
||||
let html = escapeHtml(tokenized).replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>").replace(/__([^_\n]+)__/g, "<strong>$1</strong>").replace(/\*([^*\n]+)\*/g, "<em>$1</em>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~~([^~\n]+)~~/g, "<del>$1</del>").replace(/\n/g, "<br />");
|
||||
html = html.replace(new RegExp(`${TOKEN_PREFIX}\\d+`, "g"), (token) => tokenMap.get(token) || token);
|
||||
return html;
|
||||
};
|
||||
|
||||
exports.TYPOGRAPHY_COLORS = TYPOGRAPHY_COLORS;
|
||||
exports.TYPOGRAPHY_VARIANTS = TYPOGRAPHY_VARIANTS;
|
||||
exports.tachTypographyClassList = tachTypographyClassList;
|
||||
exports.tachTypographyClassName = tachTypographyClassName;
|
||||
exports.tachTypographyEllipsisStyle = tachTypographyEllipsisStyle;
|
||||
exports.tachTypographyMarkdownToHtml = tachTypographyMarkdownToHtml;
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
//# sourceMappingURL=index.cjs.map
|
||||
1
packages/tach-typography/dist/core/index.cjs.map
vendored
Normal file
1
packages/tach-typography/dist/core/index.cjs.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user