Compare commits

...
Author SHA1 Message Date
StandardNotes CI d65abb9a49 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-15 13:56:50 +00:00
Aman Harwara 68991abba7 feat: add smart view with custom json (#2000) 2022-11-15 18:58:13 +05:30
Mo 7732f55a28 chore: remove slack (#1999) 2022-11-14 12:01:37 -06:00
StandardNotes CI 97dedc8523 chore(release): publish
- @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-14 14:33:18 +00:00
Aman Harwara f656185c16 feat: GUI to create smart views (#1997) 2022-11-14 19:40:00 +05:30
Mo 1c23bc1747 fix(mobile): iOS IAP related adjustments 2022-11-14 07:50:50 -06:00
StandardNotes CI eba8dbc38c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-13 21:33:32 +00:00
Mo f42657fa9e fix: show files option in linking menu even if not entitled 2022-11-13 15:10:57 -06:00
StandardNotes CI 2b76d48392 chore(release): publish
- @standardnotes/[email protected].1
 - @standardnotes/[email protected]
2022-11-13 17:44:39 +00:00
Mo 5777e511a9 fix(mobile): android build 2022-11-13 11:16:47 -06:00
StandardNotes CI a57bf11b5d chore(release): publish
- @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-13 15:52:08 +00:00
Mo e074805c2b chore: lint 2022-11-13 09:28:53 -06:00
Mo d519aca685 feat: iap (#1996) 2022-11-13 09:28:16 -06:00
StandardNotes CI e56a960bbf chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-11 18:49:20 +00:00
Mo da6f36f34c fix: super improvements (#1995)
* feat(super): autolink selection with cmd + k

* feat: super note importer

* feat: handle html import

* fix: ignore load change event emitted by on change plugin
2022-11-11 12:24:46 -06:00
StandardNotes CI 731e9df1af chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-11 12:54:56 +00:00
Mo e27ac3126e feat(dev): insert current date and time command in super note (#1994) 2022-11-11 06:32:31 -06:00
StandardNotes CI bdfda04d69 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-11-10 15:59:28 +00:00
Mo 2dbc89594e fix: super note improvements (#1991)
* fix: super note previews

* fix; checkmark size

* fix: top padding

* fix: prevent delete shortcut

* fix: spellcheck control

* fix: only embed file if uploaded to current note

* fix: ability to create new tag from editor autocomplete

* feat: protected file embed handling

* fix: event payload
2022-11-10 09:35:53 -06:00
StandardNotes CI 0cbc23f740 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-11-10 08:15:58 +00:00
Aman Harwara 93b6616b78 fix: iframe classname 2022-11-10 13:18:09 +05:30
StandardNotes CI 444aea3688 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-11-09 21:25:15 +00:00
Mo fdf9ab0fcb feat: file drop handling for super notes (#1990) 2022-11-09 14:55:44 -06:00
StandardNotes CI c961d5e17d chore(release): publish
- @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
2022-11-09 20:52:23 +00:00
Mo 3498130bfb fix: load blocking by network await (#1989) 2022-11-09 14:25:32 -06:00
StandardNotes CI 0275afc99e chore(release): publish
- @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
2022-11-09 17:43:56 +00:00
Mo 10a9889cf4 refactor: directly include themes in web package (#1988) 2022-11-09 11:21:26 -06:00
Mo ac4882b357 fix: link color 2022-11-09 11:00:37 -06:00
Mo 3690104934 Revert "refactor: include themes in components folder"
This reverts commit 711bea0166.
2022-11-09 11:00:09 -06:00
Mo 711bea0166 refactor: include themes in components folder 2022-11-09 09:28:20 -06:00
Mo ec7f915642 refactor: super icons (#1974) 2022-11-09 09:14:49 -06:00
StandardNotes CI 772fbea381 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
2022-11-09 12:31:40 +00:00
Mo 6bcba01dab refactor: inner blocks note links (#1973) 2022-11-09 06:06:46 -06:00
StandardNotes CI 2438443e53 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-11-09 11:13:10 +00:00
Aman Harwara abaf6a95a6 Merge branch 'main' of github.com:standardnotes/app 2022-11-09 16:14:48 +05:30
Aman Harwara 73ff1c35ab fix: editor height 2022-11-09 16:14:38 +05:30
StandardNotes CI 5cf461dcfb chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-11-09 10:11:32 +00:00
Aman Harwara e9e5a20afa fix: links & new note 2022-11-09 15:17:36 +05:30
StandardNotes CI 104c7bf88c chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
2022-11-08 19:56:36 +00:00
Mo 70a9dbcea6 refactor: blocks plugins (#1956) 2022-11-08 13:31:48 -06:00
Mo bfca244061 refactor: introduce error boundary 2022-11-08 13:29:10 -06:00
StandardNotes CI 846d981d4c chore(release): publish
- @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
2022-11-07 17:13:32 +00:00
Mo 2ed01a071c refactor: lexical (#1954) 2022-11-07 10:42:44 -06:00
StandardNotes CI 99bae83f8b chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-11-07 08:25:46 +00:00
Aman Harwara 34b3f67b2a fix: mobx out of bounds read warning 2022-11-07 13:25:43 +05:30
StandardNotes CI a7569333de chore(release): publish
- @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
2022-11-06 14:20:23 +00:00
Mo 40a1a27444 refactor: block storage (#1952) 2022-11-06 07:51:41 -06:00
StandardNotes CI 7d64b1c0ff chore(release): publish
- @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected]
2022-11-04 22:33:51 +00:00
Mo 417c90a6f2 chore: fix eslint crashing issues (#1951) 2022-11-04 17:09:54 -05:00
554 changed files with 15498 additions and 2779 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"extends": ["./node_modules/@standardnotes/config/src/.eslintrc"],
"rules": {
"max-classes-per-file": ["error", 1]
},
"ignorePatterns": [".eslintrc.js", "*.webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__", "**/**/coverage"]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -2
View File
@@ -3,7 +3,7 @@
Standard Notes is an end-to-end encrypted note-taking app for digitalists and professionals. Capture your notes, files, and lifes work all in one secure place.
[![latest release version](https://img.shields.io/github/v/release/standardnotes/app)](https://github.com/standardnotes/app/releases)
[![Slack](https://img.shields.io/badge/slack-standardnotes-CC2B5E.svg?style=flat&logo=slack)](https://standardnotes.com/slack)
[![Discord](https://img.shields.io/badge/discord-standardnotes-CC2B5E.svg?style=flat&logo=discord)](https://standardnotes.com/discord)
[![Twitter Follow](https://img.shields.io/badge/follow-%40standardnotes-blue.svg?style=flat&logo=twitter)](https://twitter.com/standardnotes)
### Why Standard Notes?
@@ -38,7 +38,6 @@ Join us on
- [Discord](https://standardnotes.com/discord)
- [Twitter](https://twitter.com/StandardNotes)
- [Forum](https://forum.standardnotes.org)
- [Slack](https://standardnotes.com/slack)
Developers can create and publish their own extensions. Visit the [documentation hub](https://docs.standardnotes.com/) to learn more.
+60
View File
@@ -0,0 +1,60 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'prettier'],
ignorePatterns: ['**/*.spec.ts'],
rules: {
'standard/no-callback-literal': 0, // Disable this as we have too many callbacks relying on literals
'no-throw-literal': 0,
camelcase: 'off',
'sort-imports': 'off',
'eol-last': 'error',
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
'no-trailing-spaces': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
vars: 'all',
args: 'after-used',
ignoreRestSiblings: false,
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-floating-promises': ['error'],
'block-scoped-var': 'error',
'comma-dangle': ['error', 'always-multiline'],
curly: ['error', 'all'],
'no-confusing-arrow': 'error',
'no-inline-comments': 'warn',
'no-invalid-this': 'error',
'no-return-assign': 'warn',
'no-constructor-return': 'error',
'no-duplicate-imports': 'error',
'no-self-compare': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-unmodified-loop-condition': 'error',
'no-unused-private-class-members': 'error',
'object-curly-spacing': ['error', 'always'],
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'never'],
'prettier/prettier': [
'error',
{
singleQuote: true,
trailingComma: 'all',
printWidth: 120,
semi: false,
},
{
usePrettierrc: false,
},
],
},
}
-4
View File
@@ -1,4 +0,0 @@
{
"exclude": ["node_modules"],
"extends": "./node_modules/@standardnotes/config/src/linter.tsconfig.json"
}
-3
View File
@@ -1,3 +0,0 @@
node_modules
dist
coverage
-6
View File
@@ -1,6 +0,0 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"project": "./linter.tsconfig.json"
}
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
root: true,
extends: ['../../common.eslintrc.js'],
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
ignorePatterns: ['**/*.spec.ts'],
}
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-14)
**Note:** Version bump only for package @standardnotes/api
# [1.20.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-13)
### Features
* iap ([#1996](https://github.com/standardnotes/app/issues/1996)) ([d519aca](https://github.com/standardnotes/app/commit/d519aca685b32f4b6f7079ba9562ecaf1a18aecb))
## [1.19.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-09)
### Bug Fixes
* load blocking by network await ([#1989](https://github.com/standardnotes/app/issues/1989)) ([3498130](https://github.com/standardnotes/app/commit/3498130bfb4e7ee9764e5c687844fd78bf909150))
## [1.19.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-09)
**Note:** Version bump only for package @standardnotes/api
## [1.19.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-07)
**Note:** Version bump only for package @standardnotes/api
## [1.19.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-06)
**Note:** Version bump only for package @standardnotes/api
## [1.19.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api
## [1.19.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
**Note:** Version bump only for package @standardnotes/api
-4
View File
@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist"]
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.19.4",
"version": "1.20.1",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -21,8 +21,8 @@
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"test": "jest spec --coverage"
},
"devDependencies": {
@@ -3,4 +3,5 @@ export enum SubscriptionApiOperations {
CancelingInvite,
ListingInvites,
AcceptingInvite,
ConfirmAppleIAP,
}
@@ -11,6 +11,8 @@ import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/Su
import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface'
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
import { Uuid } from '@standardnotes/common'
import { AppleIAPConfirmResponse } from './../../Response/Subscription/AppleIAPConfirmResponse'
import { AppleIAPConfirmRequestParams } from '../../Request'
export class SubscriptionApiService implements SubscriptionApiServiceInterface {
private operationsInProgress: Map<SubscriptionApiOperations, boolean>
@@ -31,11 +33,11 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
[ApiEndpointParam.ApiVersion]: ApiVersion.v0,
})
this.operationsInProgress.set(SubscriptionApiOperations.ListingInvites, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.ListingInvites, false)
}
}
@@ -52,11 +54,11 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
inviteUuid,
})
this.operationsInProgress.set(SubscriptionApiOperations.CancelingInvite, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.CancelingInvite, false)
}
}
@@ -73,11 +75,11 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
identifier: inviteeEmail,
})
this.operationsInProgress.set(SubscriptionApiOperations.Inviting, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.Inviting, false)
}
}
@@ -93,11 +95,27 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
inviteUuid,
})
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, false)
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, false)
}
}
async confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<AppleIAPConfirmResponse> {
if (this.operationsInProgress.get(SubscriptionApiOperations.ConfirmAppleIAP)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(SubscriptionApiOperations.ConfirmAppleIAP, true)
try {
const response = await this.subscriptionServer.confirmAppleIAP(params)
return response
} finally {
this.operationsInProgress.set(SubscriptionApiOperations.ConfirmAppleIAP, false)
}
}
}
@@ -1,5 +1,7 @@
import { Uuid } from '@standardnotes/common'
import { AppleIAPConfirmResponse } from './../../Response/Subscription/AppleIAPConfirmResponse'
import { AppleIAPConfirmRequestParams } from '../../Request'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
@@ -10,4 +12,5 @@ export interface SubscriptionApiServiceInterface {
listInvites(): Promise<SubscriptionInviteListResponse>
cancelInvite(inviteUuid: Uuid): Promise<SubscriptionInviteCancelResponse>
acceptInvite(inviteUuid: Uuid): Promise<SubscriptionInviteAcceptResponse>
confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<AppleIAPConfirmResponse>
}
@@ -10,7 +10,11 @@ describe('HttpService', () => {
const host = 'http://bar'
let updateMetaCallback: (meta: HttpResponseMeta) => void
const createService = () => new HttpService(environment, appVersion, snjsVersion, host, updateMetaCallback)
const createService = () => {
const service = new HttpService(environment, appVersion, snjsVersion, updateMetaCallback)
service.setHost(host)
return service
}
beforeEach(() => {
updateMetaCallback = jest.fn()
+7 -2
View File
@@ -1,4 +1,4 @@
import { isString, joinPaths } from '@standardnotes/utils'
import { isString, joinPaths, sleep } from '@standardnotes/utils'
import { Environment } from '@standardnotes/models'
import { HttpRequestParams } from './HttpRequestParams'
import { HttpVerb } from './HttpVerb'
@@ -13,12 +13,13 @@ import { HttpErrorResponseBody } from './HttpErrorResponseBody'
export class HttpService implements HttpServiceInterface {
private authorizationToken?: string
private __latencySimulatorMs?: number
private host!: string
constructor(
private environment: Environment,
private appVersion: string,
private snjsVersion: string,
private host: string,
private updateMetaCallback: (meta: HttpResponseMeta) => void,
) {}
@@ -78,6 +79,10 @@ export class HttpService implements HttpServiceInterface {
private async runHttp(httpRequest: HttpRequest): Promise<HttpResponse> {
const request = this.createXmlRequest(httpRequest)
if (this.__latencySimulatorMs) {
await sleep(this.__latencySimulatorMs, true)
}
const response = await this.runRequest(request, this.createRequestBody(httpRequest))
if (response.meta) {
@@ -0,0 +1,7 @@
export type AppleIAPConfirmRequestParams = {
productId: string
transactionId: string
transactionDate: string
transactionReceipt: string
subscription_token: string
}
+1
View File
@@ -1,4 +1,5 @@
export * from './ApiEndpointParam'
export * from './Subscription/AppleIAPConfirmRequestParams'
export * from './Subscription/SubscriptionInviteAcceptRequestParams'
export * from './Subscription/SubscriptionInviteCancelRequestParams'
export * from './Subscription/SubscriptionInviteDeclineRequestParams'
@@ -0,0 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { AppleIAPConfirmResponseBody } from './AppleIAPConfirmResponseBody'
export interface AppleIAPConfirmResponse extends HttpResponse {
data: Either<AppleIAPConfirmResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1 @@
export type AppleIAPConfirmResponseBody = { success: true } | { success: false; message: string }
@@ -1,3 +1,5 @@
export * from './Subscription/AppleIAPConfirmResponse'
export * from './Subscription/AppleIAPConfirmResponseBody'
export * from './Subscription/SubscriptionInviteAcceptResponse'
export * from './Subscription/SubscriptionInviteAcceptResponseBody'
export * from './Subscription/SubscriptionInviteCancelResponse'
@@ -8,8 +8,13 @@ const SharingPaths = {
listInvites: '/v1/subscription-invites',
}
const ApplePaths = {
confirmAppleIAP: '/v1/subscriptions/apple_iap_confirm',
}
export const Paths = {
v1: {
...SharingPaths,
...ApplePaths,
},
}
@@ -1,4 +1,6 @@
import { AppleIAPConfirmResponse } from './../../Response/Subscription/AppleIAPConfirmResponse'
import { HttpServiceInterface } from '../../Http/HttpServiceInterface'
import { AppleIAPConfirmRequestParams } from '../../Request'
import { SubscriptionInviteAcceptRequestParams } from '../../Request/Subscription/SubscriptionInviteAcceptRequestParams'
import { SubscriptionInviteCancelRequestParams } from '../../Request/Subscription/SubscriptionInviteCancelRequestParams'
import { SubscriptionInviteDeclineRequestParams } from '../../Request/Subscription/SubscriptionInviteDeclineRequestParams'
@@ -45,4 +47,10 @@ export class SubscriptionServer implements SubscriptionServerInterface {
return response as SubscriptionInviteResponse
}
async confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<AppleIAPConfirmResponse> {
const response = await this.httpService.post(Paths.v1.confirmAppleIAP, params)
return response as AppleIAPConfirmResponse
}
}
@@ -1,3 +1,5 @@
import { AppleIAPConfirmResponse } from './../../Response/Subscription/AppleIAPConfirmResponse'
import { AppleIAPConfirmRequestParams } from './../../Request/Subscription/AppleIAPConfirmRequestParams'
import { SubscriptionInviteAcceptRequestParams } from '../../Request/Subscription/SubscriptionInviteAcceptRequestParams'
import { SubscriptionInviteCancelRequestParams } from '../../Request/Subscription/SubscriptionInviteCancelRequestParams'
import { SubscriptionInviteDeclineRequestParams } from '../../Request/Subscription/SubscriptionInviteDeclineRequestParams'
@@ -15,4 +17,5 @@ export interface SubscriptionServerInterface {
declineInvite(params: SubscriptionInviteDeclineRequestParams): Promise<SubscriptionInviteDeclineResponse>
cancelInvite(params: SubscriptionInviteCancelRequestParams): Promise<SubscriptionInviteCancelResponse>
listInvites(params: SubscriptionInviteListRequestParams): Promise<SubscriptionInviteListResponse>
confirmAppleIAP(params: AppleIAPConfirmRequestParams): Promise<AppleIAPConfirmResponse>
}
+12
View File
@@ -0,0 +1,12 @@
# NOTE: In general this should be kept in sync with .eslintignore
**/dist/**
**/config/**
**/build/**
**/npm/**
**/*.js.flow
**/*.d.ts
**/playwright*/**
**/vite.config.js
**/vite.prod.config.js
**/node_modules
+254
View File
@@ -0,0 +1,254 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
const restrictedGlobals = require('confusing-browser-globals');
const OFF = 0;
const ERROR = 2;
module.exports = {
root: true,
// Prettier must be last so it can override other configs (https://github.com/prettier/eslint-config-prettier#installation)
extends: [
'fbjs',
'plugin:react-hooks/recommended',
'plugin:lexical/all',
'prettier',
],
globals: {
JSX: true,
__DEV__: true,
},
overrides: [
{
// We apply these settings to the source files that get compiled.
// They can use all features including JSX (but shouldn't use `var`).
files: [
'packages/*/src/**/*.js',
'packages/*/__tests__/**/*.?(m)js',
'packages/*/src/**/*.jsx',
],
parser: 'babel-eslint',
parserOptions: {
allowImportExportEverywhere: true,
sourceType: 'module',
},
rules: {
'no-var': ERROR,
'prefer-const': ERROR,
strict: OFF,
},
},
{
// node scripts should be console logging so don't lint against that
files: ['scripts/**/*.js'],
rules: {
'no-console': OFF,
},
},
{
env: {
browser: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
},
plugins: ['react', '@typescript-eslint', 'header'],
rules: {
'@typescript-eslint/ban-ts-comment': OFF,
'@typescript-eslint/no-this-alias': OFF,
'@typescript-eslint/no-unused-vars': [ERROR, {args: 'none'}],
'header/header': [2, 'scripts/www/headerTemplate.js'],
},
},
{
// don't lint headers in entrypoint files so we can add TypeDoc module comments
files: ['packages/**/src/index.ts'],
rules: {
'header/header': OFF,
},
},
{
files: [
'packages/**/src/__tests__/**',
'packages/lexical-playground/**',
'packages/lexical-devtools/**',
],
rules: {
'lexical/no-optional-chaining': OFF,
},
},
],
parser: 'babel-eslint',
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
ecmaVersion: 8,
sourceType: 'script',
},
plugins: [
'sort-keys-fix',
'simple-import-sort',
'header',
// import helps to configure simple-import-sort
'import',
'jest',
'no-function-declare-after-return',
'react',
'no-only-tests',
'lexical',
],
// Stop ESLint from looking for a configuration file in parent folders
root: true,
// We're stricter than the default config, mostly. We'll override a few rules
// and then enable some React specific ones.
rules: {
'accessor-pairs': OFF,
'brace-style': [ERROR, '1tbs'],
'consistent-return': OFF,
'dot-location': [ERROR, 'property'],
// We use console['error']() as a signal to not transform it:
'dot-notation': [ERROR, {allowPattern: '^(error|warn)$'}],
'eol-last': ERROR,
eqeqeq: [ERROR, 'allow-null'],
// Prettier forces semicolons in a few places
'flowtype/object-type-delimiter': OFF,
'flowtype/sort-keys': ERROR,
'header/header': [2, 'scripts/www/headerTemplate.js'],
// (This helps configure simple-import-sort) Make sure all imports are at the top of the file
'import/first': ERROR,
// (This helps configure simple-import-sort) Make sure there's a newline after the imports
'import/newline-after-import': ERROR,
// (This helps configure simple-import-sort) Merge imports of the same file
'import/no-duplicates': ERROR,
indent: OFF,
'jsx-quotes': [ERROR, 'prefer-double'],
'keyword-spacing': [ERROR, {after: true, before: true}],
// Enforced by Prettier
// TODO: Prettier doesn't handle long strings or long comments. Not a big
// deal. But I turned it off because loading the plugin causes some obscure
// syntax error and it didn't seem worth investigating.
'max-len': OFF,
'no-bitwise': OFF,
'no-console': ERROR,
'no-debugger': ERROR,
// Prevent function declarations after return statements
'no-function-declare-after-return/no-function-declare-after-return': ERROR,
'no-inner-declarations': [ERROR, 'functions'],
'no-multi-spaces': ERROR,
'no-only-tests/no-only-tests': ERROR,
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
'no-restricted-syntax': [ERROR, 'WithStatement'],
'no-shadow': ERROR,
'no-unused-expressions': ERROR,
'no-unused-vars': [ERROR, {args: 'none'}],
'no-use-before-define': OFF,
// Flow fails with with non-string literal keys
'no-useless-computed-key': OFF,
'no-useless-concat': OFF,
// We apply these settings to files that should run on Node.
// They can't use JSX or ES6 modules, and must be in strict mode.
// They can, however, use other ES6 features.
// (Note these rules are overridden later for source files.)
'no-var': ERROR,
quotes: [ERROR, 'single', {allowTemplateLiterals: true, avoidEscape: true}],
// React & JSX
// Our transforms set this automatically
'react/jsx-boolean-value': [ERROR, 'always'],
'react/jsx-no-undef': ERROR,
// We don't care to do this
'react/jsx-sort-prop-types': OFF,
'react/jsx-tag-spacing': ERROR,
'react/jsx-uses-react': ERROR,
// We don't care to do this
'react/jsx-wrap-multilines': [
ERROR,
{assignment: false, declaration: false},
],
'react/no-is-mounted': OFF,
// This isn't useful in our test code
'react/react-in-jsx-scope': ERROR,
'react/self-closing-comp': ERROR,
// This sorts re-exports (`export * from 'foo';`), but not other types of exports.
'simple-import-sort/exports': ERROR,
'simple-import-sort/imports': [
ERROR,
{
// The default grouping, but with type imports first as a separate group.
// See: https://github.com/lydell/eslint-plugin-simple-import-sort/blob/d9a116f71302c5dcfc1581fc7ded8d77392f1924/examples/.eslintrc.js#L122-L133
groups: [['^.*\\u0000$'], ['^\\u0000'], ['^@?\\w'], ['^'], ['^\\.']],
},
],
'sort-keys-fix/sort-keys-fix': ERROR,
'space-before-blocks': ERROR,
'space-before-function-paren': OFF,
strict: ERROR,
'valid-typeof': [ERROR, {requireStringLiterals: true}],
},
};
+1
View File
@@ -0,0 +1 @@
dist
+15
View File
@@ -0,0 +1,15 @@
# NOTE: In general this should be kept in sync with .eslintignore
packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
packages/**/config/*.js
packages/playwright
packages/playwright-core
packages/**/vite.config.js
packages/**/vite.prod.config.js
**/*.md
**/node_modules
flow-typed
.github/CODEOWNERS
.prettierignore
+11
View File
@@ -0,0 +1,11 @@
'use strict';
module.exports = {
bracketSpacing: false,
singleQuote: true,
bracketSameLine: true,
printWidth: 80,
trailingComma: 'all',
htmlWhitespaceSensitivity: 'ignore',
attributeGroups: ['$DEFAULT', '^data-'],
};
+134
View File
@@ -0,0 +1,134 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-11)
### Bug Fixes
* super improvements ([#1995](https://github.com/standardnotes/app/issues/1995)) ([da6f36f](https://github.com/standardnotes/app/commit/da6f36f34cdee324339d2c923d61191b373513f3))
# [1.4.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-11)
### Features
* **dev:** insert current date and time command in super note ([#1994](https://github.com/standardnotes/app/issues/1994)) ([e27ac31](https://github.com/standardnotes/app/commit/e27ac3126ee4fec9e81df8722b6326ba6f38fe5e))
## [1.3.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-10)
### Bug Fixes
* super note improvements ([#1991](https://github.com/standardnotes/app/issues/1991)) ([2dbc895](https://github.com/standardnotes/app/commit/2dbc89594ea2759c49bc318d5efd2b3a5ef3b22d))
## [1.3.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-09)
### Bug Fixes
* link color ([ac4882b](https://github.com/standardnotes/app/commit/ac4882b357162f8737ac32ea9f55884276cc3011))
### Reverts
* Revert "refactor: include themes in components folder" ([3690104](https://github.com/standardnotes/app/commit/3690104934b444ca99eed86c1a41b22a3582811a))
## [1.3.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-09)
**Note:** Version bump only for package @standardnotes/blocks-editor
## [1.3.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-08)
**Note:** Version bump only for package @standardnotes/blocks-editor
## 1.3.7 (2022-11-07)
**Note:** Version bump only for package @standardnotes/blocks-editor
## [1.3.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-04)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-08-11)
### Bug Fixes
* optimize toasts for mobile ([#1392](https://github.com/standardnotes/app/issues/1392)) ([40d9392](https://github.com/standardnotes/app/commit/40d9392599e871225abcabcddd51de6cc99a0fe9))
## [1.3.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-14)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-13)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-06)
**Note:** Version bump only for package @standardnotes/toast
## [1.3.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-28)
**Note:** Version bump only for package @standardnotes/toast
# [1.3.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-27)
### Features
* **web:** tailwind css ([#1147](https://github.com/standardnotes/app/issues/1147)) ([b80038f](https://github.com/standardnotes/app/commit/b80038f607d7411912fa99366abf559a44874ef3))
## [1.2.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-18)
**Note:** Version bump only for package @standardnotes/toast
## [1.2.4-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-18)
**Note:** Version bump only for package @standardnotes/toast
## 1.2.3 (2022-06-16)
**Note:** Version bump only for package @standardnotes/toast
## 1.2.2 (2022-06-16)
**Note:** Version bump only for package @standardnotes/toast
## [1.2.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-16)
**Note:** Version bump only for package @standardnotes/toast
## 1.2.1-alpha.4 (2022-06-16)
**Note:** Version bump only for package @standardnotes/toast
## 1.2.1-alpha.3 (2022-06-16)
**Note:** Version bump only for package @standardnotes/toast
## 1.2.1-alpha.2 (2022-06-15)
**Note:** Version bump only for package @standardnotes/toast
## [1.2.1-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-14)
**Note:** Version bump only for package @standardnotes/toast
## [1.2.1-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-14)
**Note:** Version bump only for package @standardnotes/toast
# 1.2.0 (2022-06-10)
### Features
* mobile app package ([#1075](https://github.com/standardnotes/app/issues/1075)) ([8248a38](https://github.com/standardnotes/app/commit/8248a38280cb7c92da2b2e9c7db298f34ae8ffdf))
* styles package ([#1074](https://github.com/standardnotes/app/issues/1074)) ([3100327](https://github.com/standardnotes/app/commit/31003276b73d3e89824bc002fe616fa055e918c4))
* toast package ([#1073](https://github.com/standardnotes/app/issues/1073)) ([6d0b6e9](https://github.com/standardnotes/app/commit/6d0b6e9018b2a612b8df4827336883fe04033128))
* **wip:** components monorepo ([#1082](https://github.com/standardnotes/app/issues/1082)) ([e3d6001](https://github.com/standardnotes/app/commit/e3d6001a178e11e619ca724b2b155b7c0405c023))
# 1.1.0 (2022-06-10)
### Features
* mobile app package ([#1075](https://github.com/standardnotes/app/issues/1075)) ([8248a38](https://github.com/standardnotes/app/commit/8248a38280cb7c92da2b2e9c7db298f34ae8ffdf))
* styles package ([#1074](https://github.com/standardnotes/app/issues/1074)) ([3100327](https://github.com/standardnotes/app/commit/31003276b73d3e89824bc002fe616fa055e918c4))
* toast package ([#1073](https://github.com/standardnotes/app/issues/1073)) ([6d0b6e9](https://github.com/standardnotes/app/commit/6d0b6e9018b2a612b8df4827336883fe04033128))
* **wip:** components monorepo ([8c5e11c](https://github.com/standardnotes/app/commit/8c5e11c22b717ada7a6a9b3115fc4c9b757ec71c))
+1
View File
@@ -0,0 +1 @@
Based on https://github.com/facebook/lexical/tree/main/packages/lexical-playground
@@ -0,0 +1,32 @@
const path = require('path')
module.exports = () => {
return {
entry: './src/index.ts',
output: {
filename: './dist.js',
},
mode: 'production',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
externals: {
"@standardnotes/icons": path.resolve(__dirname, "./node_modules/@standardnotes/icons")
},
module: {
rules: [
{
test: /\.(js|tsx?)$/,
use: [
'babel-loader',
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
},
],
},
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "@standardnotes/blocks-editor",
"version": "1.4.1",
"private": true,
"main": "./src/index.ts",
"scripts": {
"start": "tsc -p tsconfig.json --watch"
},
"dependencies": {
"@lexical/react": "^0.6.0",
"@standardnotes/icons": "workspace:*",
"@types/react": "^18.0.20",
"@types/react-dom": "^18.0.6",
"lexical": "^0.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"eslint": "*",
"prettier": "*",
"typescript": "*"
}
}
@@ -0,0 +1,139 @@
import {FunctionComponent, useCallback, useState} from 'react';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
import {
CHECK_LIST,
ELEMENT_TRANSFORMERS,
TEXT_FORMAT_TRANSFORMERS,
TEXT_MATCH_TRANSFORMERS,
} from '@lexical/markdown';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {LinkPlugin} from '@lexical/react/LexicalLinkPlugin';
import {ListPlugin} from '@lexical/react/LexicalListPlugin';
import {$getRoot, EditorState, LexicalEditor} from 'lexical';
import HorizontalRulePlugin from '../Lexical/Plugins/HorizontalRulePlugin';
import TwitterPlugin from '../Lexical/Plugins/TwitterPlugin';
import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin';
import AutoEmbedPlugin from '../Lexical/Plugins/AutoEmbedPlugin';
import CollapsiblePlugin from '../Lexical/Plugins/CollapsiblePlugin';
import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin';
import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin';
import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin';
import {truncateString} from './Utils';
import {SuperEditorContentId} from './Constants';
const BlockDragEnabled = false;
type BlocksEditorProps = {
onChange: (value: string, preview: string) => void;
className?: string;
children?: React.ReactNode;
previewLength: number;
spellcheck?: boolean;
};
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
onChange,
className,
children,
previewLength,
spellcheck,
}) => {
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
const handleChange = useCallback(
(editorState: EditorState, _editor: LexicalEditor) => {
if (!didIgnoreFirstChange) {
setDidIgnoreFirstChange(true);
return;
}
editorState.read(() => {
const childrenNodes = $getRoot().getAllTextNodes().slice(0, 2);
let previewText = '';
childrenNodes.forEach((node, index) => {
previewText += node.getTextContent();
if (index !== childrenNodes.length - 1) {
previewText += '\n';
}
});
previewText = truncateString(previewText, previewLength);
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
onChange(stringifiedEditorState, previewText);
});
},
[onChange, didIgnoreFirstChange],
);
const [floatingAnchorElem, setFloatingAnchorElem] =
useState<HTMLDivElement | null>(null);
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
if (_floatingAnchorElem !== null) {
setFloatingAnchorElem(_floatingAnchorElem);
}
};
return (
<>
{children}
<RichTextPlugin
contentEditable={
<div id="blocks-editor" className="editor-scroller">
<div className="editor" ref={onRef}>
<ContentEditable
id={SuperEditorContentId}
className={`ContentEditable__root ${className}`}
spellCheck={spellcheck}
/>
</div>
</div>
}
placeholder=""
ErrorBoundary={LexicalErrorBoundary}
/>
<ListPlugin />
<MarkdownShortcutPlugin
transformers={[
CHECK_LIST,
...ELEMENT_TRANSFORMERS,
...TEXT_FORMAT_TRANSFORMERS,
...TEXT_MATCH_TRANSFORMERS,
]}
/>
<TablePlugin />
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
<HistoryPlugin />
<HorizontalRulePlugin />
<AutoFocusPlugin />
<ClearEditorPlugin />
<CheckListPlugin />
<CodeHighlightPlugin />
<LinkPlugin />
<HashtagPlugin />
<AutoEmbedPlugin />
<TwitterPlugin />
<YouTubePlugin />
<CollapsiblePlugin />
{floatingAnchorElem && (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin />
</>
)}
{floatingAnchorElem && BlockDragEnabled && (
<>{<DraggableBlockPlugin anchorElem={floatingAnchorElem} />}</>
)}
</>
);
};
@@ -0,0 +1,31 @@
import {FunctionComponent} from 'react';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import BlocksEditorTheme from '../Lexical/Theme/Theme';
import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
import {Klass, LexicalNode} from 'lexical';
type BlocksEditorComposerProps = {
initialValue: string;
children: React.ReactNode;
nodes?: Array<Klass<LexicalNode>>;
readonly?: boolean;
};
export const BlocksEditorComposer: FunctionComponent<
BlocksEditorComposerProps
> = ({initialValue, children, readonly, nodes = []}) => {
return (
<LexicalComposer
initialConfig={{
namespace: 'BlocksEditor',
theme: BlocksEditorTheme,
editable: !readonly,
onError: (error: Error) => console.error(error),
editorState:
initialValue && initialValue.length > 0 ? initialValue : undefined,
nodes: [...nodes, ...BlockEditorNodes],
}}>
<>{children}</>
</LexicalComposer>
);
};
@@ -0,0 +1 @@
export const SuperEditorContentId = 'super-editor-content';
@@ -0,0 +1,7 @@
export function truncateString(string: string, limit: number) {
if (string.length <= limit) {
return string;
} else {
return string.substring(0, limit) + '...';
}
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useCallback, useMemo, useState} from 'react';
import Modal from '../UI/Modal';
export default function useModal(): [
JSX.Element | null,
(title: string, showModal: (onClose: () => void) => JSX.Element) => void,
] {
const [modalContent, setModalContent] = useState<null | {
closeOnClickOutside: boolean;
content: JSX.Element;
title: string;
}>(null);
const onClose = useCallback(() => {
setModalContent(null);
}, []);
const modal = useMemo(() => {
if (modalContent === null) {
return null;
}
const {title, content, closeOnClickOutside} = modalContent;
return (
<Modal
onClose={onClose}
title={title}
closeOnClickOutside={closeOnClickOutside}>
{content}
</Modal>
);
}, [modalContent, onClose]);
const showModal = useCallback(
(
title: string,
// eslint-disable-next-line no-shadow
getContent: (onClose: () => void) => JSX.Element,
closeOnClickOutside = false,
) => {
setModalContent({
closeOnClickOutside,
content: getContent(onClose),
title,
});
},
[onClose],
);
return [modal, showModal];
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-down"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/></svg>

After

Width:  |  Height:  |  Size: 266 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 24 24" fill="currentColor"><path stroke="currentColor" d="M8.5 10a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm0 7a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm7-10a2 2 0 1 0-2-2 2 2 0 0 0 2 2Zm-7-4a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm7 14a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm0-7a2 2 0 1 0 2 2 2 2 0 0 0-2-2Z"/></svg>

After

Width:  |  Height:  |  Size: 344 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" class="bi bi-pencil-fill"><path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/></svg>

After

Width:  |  Height:  |  Size: 554 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus"><path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/></svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,37 @@
import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list';
import {MarkNode} from '@lexical/mark';
import {OverflowNode} from '@lexical/overflow';
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
import {TweetNode} from './TweetNode';
import {YouTubeNode} from './YouTubeNode';
import {CollapsibleContainerNode} from '../Plugins/CollapsiblePlugin/CollapsibleContainerNode';
import {CollapsibleContentNode} from '../Plugins/CollapsiblePlugin/CollapsibleContentNode';
import {CollapsibleTitleNode} from '../Plugins/CollapsiblePlugin/CollapsibleTitleNode';
export const BlockEditorNodes = [
AutoLinkNode,
CodeHighlightNode,
CodeNode,
CollapsibleContainerNode,
CollapsibleContentNode,
CollapsibleTitleNode,
HashtagNode,
HeadingNode,
HorizontalRuleNode,
LinkNode,
ListItemNode,
ListNode,
MarkNode,
OverflowNode,
QuoteNode,
TableCellNode,
TableNode,
TableRowNode,
TweetNode,
YouTubeNode,
];
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,416 @@
import {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
LexicalEditor,
LexicalNode,
NodeKey,
SerializedLexicalNode,
Spread,
DecoratorNode,
} from 'lexical';
import * as React from 'react';
import {Suspense} from 'react';
export type Cell = {
colSpan: number;
json: string;
type: 'normal' | 'header';
id: string;
width: number | null;
};
export type Row = {
cells: Array<Cell>;
height: null | number;
id: string;
};
export type Rows = Array<Row>;
export const cellHTMLCache: Map<string, string> = new Map();
export const cellTextContentCache: Map<string, string> = new Map();
const emptyEditorJSON =
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
const plainTextEditorJSON = (text: string) =>
text === ''
? emptyEditorJSON
: `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`;
const TableComponent = React.lazy(
// @ts-ignore
() => import('./TableComponent'),
);
export function createUID(): string {
return Math.random()
.toString(36)
.replace(/[^a-z]+/g, '')
.substr(0, 5);
}
function createCell(type: 'normal' | 'header'): Cell {
return {
colSpan: 1,
id: createUID(),
json: emptyEditorJSON,
type,
width: null,
};
}
export function createRow(): Row {
return {
cells: [],
height: null,
id: createUID(),
};
}
export type SerializedTableNode = Spread<
{
rows: Rows;
type: 'tablesheet';
version: 1;
},
SerializedLexicalNode
>;
export function extractRowsFromHTML(tableElem: HTMLTableElement): Rows {
const rowElems = tableElem.querySelectorAll('tr');
const rows: Rows = [];
for (let y = 0; y < rowElems.length; y++) {
const rowElem = rowElems[y];
const cellElems = rowElem.querySelectorAll('td,th');
if (!cellElems || cellElems.length === 0) {
continue;
}
const cells: Array<Cell> = [];
for (let x = 0; x < cellElems.length; x++) {
const cellElem = cellElems[x] as HTMLElement;
const isHeader = cellElem.nodeName === 'TH';
const cell = createCell(isHeader ? 'header' : 'normal');
cell.json = plainTextEditorJSON(
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
);
cells.push(cell);
}
const row = createRow();
row.cells = cells;
rows.push(row);
}
return rows;
}
function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
const rowElems = domNode.querySelectorAll('tr');
if (!rowElems || rowElems.length === 0) {
return null;
}
const rows: Rows = [];
for (let y = 0; y < rowElems.length; y++) {
const rowElem = rowElems[y];
const cellElems = rowElem.querySelectorAll('td,th');
if (!cellElems || cellElems.length === 0) {
continue;
}
const cells: Array<Cell> = [];
for (let x = 0; x < cellElems.length; x++) {
const cellElem = cellElems[x] as HTMLElement;
const isHeader = cellElem.nodeName === 'TH';
const cell = createCell(isHeader ? 'header' : 'normal');
cell.json = plainTextEditorJSON(
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
);
cells.push(cell);
}
const row = createRow();
row.cells = cells;
rows.push(row);
}
return {node: $createTableNode(rows)};
}
export function exportTableCellsToHTML(
rows: Rows,
rect?: {startX: number; endX: number; startY: number; endY: number},
): HTMLElement {
const table = document.createElement('table');
const colGroup = document.createElement('colgroup');
const tBody = document.createElement('tbody');
const firstRow = rows[0];
for (
let x = rect != null ? rect.startX : 0;
x < (rect != null ? rect.endX + 1 : firstRow.cells.length);
x++
) {
const col = document.createElement('col');
colGroup.append(col);
}
for (
let y = rect != null ? rect.startY : 0;
y < (rect != null ? rect.endY + 1 : rows.length);
y++
) {
const row = rows[y];
const cells = row.cells;
const rowElem = document.createElement('tr');
for (
let x = rect != null ? rect.startX : 0;
x < (rect != null ? rect.endX + 1 : cells.length);
x++
) {
const cell = cells[x];
const cellElem = document.createElement(
cell.type === 'header' ? 'th' : 'td',
);
cellElem.innerHTML = cellHTMLCache.get(cell.json) || '';
rowElem.appendChild(cellElem);
}
tBody.appendChild(rowElem);
}
table.appendChild(colGroup);
table.appendChild(tBody);
return table;
}
export class TableNode extends DecoratorNode<JSX.Element> {
__rows: Rows;
static getType(): string {
return 'tablesheet';
}
static clone(node: TableNode): TableNode {
return new TableNode(Array.from(node.__rows), node.__key);
}
static importJSON(serializedNode: SerializedTableNode): TableNode {
return $createTableNode(serializedNode.rows);
}
exportJSON(): SerializedTableNode {
return {
rows: this.__rows,
type: 'tablesheet',
version: 1,
};
}
static importDOM(): DOMConversionMap | null {
return {
table: (_node: Node) => ({
conversion: convertTableElement,
priority: 0,
}),
};
}
exportDOM(): DOMExportOutput {
return {element: exportTableCellsToHTML(this.__rows)};
}
constructor(rows?: Rows, key?: NodeKey) {
super(key);
this.__rows = rows || [];
}
createDOM(): HTMLElement {
const div = document.createElement('div');
div.style.display = 'contents';
return div;
}
updateDOM(): false {
return false;
}
mergeRows(startX: number, startY: number, mergeRows: Rows): void {
const self = this.getWritable();
const rows = self.__rows;
const endY = Math.min(rows.length, startY + mergeRows.length);
for (let y = startY; y < endY; y++) {
const row = rows[y];
const mergeRow = mergeRows[y - startY];
const cells = row.cells;
const cellsClone = Array.from(cells);
const rowClone = {...row, cells: cellsClone};
const mergeCells = mergeRow.cells;
const endX = Math.min(cells.length, startX + mergeCells.length);
for (let x = startX; x < endX; x++) {
const cell = cells[x];
const mergeCell = mergeCells[x - startX];
const cellClone = {...cell, json: mergeCell.json, type: mergeCell.type};
cellsClone[x] = cellClone;
}
rows[y] = rowClone;
}
}
updateCellJSON(x: number, y: number, json: string): void {
const self = this.getWritable();
const rows = self.__rows;
const row = rows[y];
const cells = row.cells;
const cell = cells[x];
const cellsClone = Array.from(cells);
const cellClone = {...cell, json};
const rowClone = {...row, cells: cellsClone};
cellsClone[x] = cellClone;
rows[y] = rowClone;
}
updateCellType(x: number, y: number, type: 'header' | 'normal'): void {
const self = this.getWritable();
const rows = self.__rows;
const row = rows[y];
const cells = row.cells;
const cell = cells[x];
const cellsClone = Array.from(cells);
const cellClone = {...cell, type};
const rowClone = {...row, cells: cellsClone};
cellsClone[x] = cellClone;
rows[y] = rowClone;
}
insertColumnAt(x: number): void {
const self = this.getWritable();
const rows = self.__rows;
for (let y = 0; y < rows.length; y++) {
const row = rows[y];
const cells = row.cells;
const cellsClone = Array.from(cells);
const rowClone = {...row, cells: cellsClone};
const type = (cells[x] || cells[x - 1]).type;
cellsClone.splice(x, 0, createCell(type));
rows[y] = rowClone;
}
}
deleteColumnAt(x: number): void {
const self = this.getWritable();
const rows = self.__rows;
for (let y = 0; y < rows.length; y++) {
const row = rows[y];
const cells = row.cells;
const cellsClone = Array.from(cells);
const rowClone = {...row, cells: cellsClone};
cellsClone.splice(x, 1);
rows[y] = rowClone;
}
}
addColumns(count: number): void {
const self = this.getWritable();
const rows = self.__rows;
for (let y = 0; y < rows.length; y++) {
const row = rows[y];
const cells = row.cells;
const cellsClone = Array.from(cells);
const rowClone = {...row, cells: cellsClone};
const type = cells[cells.length - 1].type;
for (let x = 0; x < count; x++) {
cellsClone.push(createCell(type));
}
rows[y] = rowClone;
}
}
insertRowAt(y: number): void {
const self = this.getWritable();
const rows = self.__rows;
const prevRow = rows[y] || rows[y - 1];
const cellCount = prevRow.cells.length;
const row = createRow();
for (let x = 0; x < cellCount; x++) {
const cell = createCell(prevRow.cells[x].type);
row.cells.push(cell);
}
rows.splice(y, 0, row);
}
deleteRowAt(y: number): void {
const self = this.getWritable();
const rows = self.__rows;
rows.splice(y, 1);
}
addRows(count: number): void {
const self = this.getWritable();
const rows = self.__rows;
const prevRow = rows[rows.length - 1];
const cellCount = prevRow.cells.length;
for (let y = 0; y < count; y++) {
const row = createRow();
for (let x = 0; x < cellCount; x++) {
const cell = createCell(prevRow.cells[x].type);
row.cells.push(cell);
}
rows.push(row);
}
}
updateColumnWidth(x: number, width: number): void {
const self = this.getWritable();
const rows = self.__rows;
for (let y = 0; y < rows.length; y++) {
const row = rows[y];
const cells = row.cells;
const cellsClone = Array.from(cells);
const rowClone = {...row, cells: cellsClone};
cellsClone[x].width = width;
rows[y] = rowClone;
}
}
decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
return (
<Suspense>
<TableComponent
nodeKey={this.__key}
theme={config.theme}
rows={this.__rows}
/>
</Suspense>
);
}
}
export function $isTableNode(
node: LexicalNode | null | undefined,
): node is TableNode {
return node instanceof TableNode;
}
export function $createTableNode(rows: Rows): TableNode {
return new TableNode(rows);
}
export function $createTableNodeWithDimensions(
rowCount: number,
columnCount: number,
includeHeaders = true,
): TableNode {
const rows: Rows = [];
for (let y = 0; y < columnCount; y++) {
const row: Row = createRow();
rows.push(row);
for (let x = 0; x < rowCount; x++) {
row.cells.push(
createCell(
includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal',
),
);
}
}
return new TableNode(rows);
}
@@ -0,0 +1,228 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType,
LexicalEditor,
LexicalNode,
NodeKey,
Spread,
} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
import {
DecoratorBlockNode,
SerializedDecoratorBlockNode,
} from '@lexical/react/LexicalDecoratorBlockNode';
import {useCallback, useEffect, useRef, useState} from 'react';
const WIDGET_SCRIPT_URL = 'https://platform.twitter.com/widgets.js';
type TweetComponentProps = Readonly<{
className: Readonly<{
base: string;
focus: string;
}>;
format: ElementFormatType | null;
loadingComponent?: JSX.Element | string;
nodeKey: NodeKey;
onError?: (error: string) => void;
onLoad?: () => void;
tweetID: string;
}>;
function convertTweetElement(
domNode: HTMLDivElement,
): DOMConversionOutput | null {
const id = domNode.getAttribute('data-lexical-tweet-id');
if (id) {
const node = $createTweetNode(id);
return {node};
}
return null;
}
let isTwitterScriptLoading = true;
function TweetComponent({
className,
format,
loadingComponent,
nodeKey,
onError,
onLoad,
tweetID,
}: TweetComponentProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const previousTweetIDRef = useRef<string>('');
const [isTweetLoading, setIsTweetLoading] = useState(false);
const createTweet = useCallback(async () => {
try {
// @ts-expect-error Twitter is attached to the window.
await window.twttr.widgets.createTweet(tweetID, containerRef.current);
setIsTweetLoading(false);
isTwitterScriptLoading = false;
if (onLoad) {
onLoad();
}
} catch (error) {
if (onError) {
onError(String(error));
}
}
}, [onError, onLoad, tweetID]);
useEffect(() => {
if (tweetID !== previousTweetIDRef.current) {
setIsTweetLoading(true);
if (isTwitterScriptLoading) {
const script = document.createElement('script');
script.src = WIDGET_SCRIPT_URL;
script.async = true;
document.body?.appendChild(script);
script.onload = createTweet;
if (onError) {
script.onerror = onError as OnErrorEventHandler;
}
} else {
createTweet();
}
if (previousTweetIDRef) {
previousTweetIDRef.current = tweetID;
}
}
}, [createTweet, onError, tweetID]);
return (
<BlockWithAlignableContents
className={className}
format={format}
nodeKey={nodeKey}>
{isTweetLoading ? loadingComponent : null}
<div
style={{display: 'inline-block', width: '550px'}}
ref={containerRef}
/>
</BlockWithAlignableContents>
);
}
export type SerializedTweetNode = Spread<
{
id: string;
type: 'tweet';
version: 1;
},
SerializedDecoratorBlockNode
>;
export class TweetNode extends DecoratorBlockNode {
__id: string;
static getType(): string {
return 'tweet';
}
static clone(node: TweetNode): TweetNode {
return new TweetNode(node.__id, node.__format, node.__key);
}
static importJSON(serializedNode: SerializedTweetNode): TweetNode {
const node = $createTweetNode(serializedNode.id);
node.setFormat(serializedNode.format);
return node;
}
exportJSON(): SerializedTweetNode {
return {
...super.exportJSON(),
id: this.getId(),
type: 'tweet',
version: 1,
};
}
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
return {
div: (domNode: HTMLDivElement) => {
if (!domNode.hasAttribute('data-lexical-tweet-id')) {
return null;
}
return {
conversion: convertTweetElement,
priority: 2,
};
},
};
}
exportDOM(): DOMExportOutput {
const element = document.createElement('div');
element.setAttribute('data-lexical-tweet-id', this.__id);
const text = document.createTextNode(this.getTextContent());
element.append(text);
return {element};
}
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key);
this.__id = id;
}
getId(): string {
return this.__id;
}
getTextContent(
_includeInert?: boolean | undefined,
_includeDirectionless?: false | undefined,
): string {
return `https://twitter.com/i/web/status/${this.__id}`;
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
const embedBlockTheme = config.theme.embedBlock || {};
const className = {
base: embedBlockTheme.base || '',
focus: embedBlockTheme.focus || '',
};
return (
<TweetComponent
className={className}
format={this.__format}
loadingComponent="Loading..."
nodeKey={this.getKey()}
tweetID={this.__id}
/>
);
}
isInline(): false {
return false;
}
}
export function $createTweetNode(tweetID: string): TweetNode {
return new TweetNode(tweetID);
}
export function $isTweetNode(
node: TweetNode | LexicalNode | null | undefined,
): node is TweetNode {
return node instanceof TweetNode;
}
@@ -0,0 +1,142 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
EditorConfig,
ElementFormatType,
LexicalEditor,
LexicalNode,
NodeKey,
Spread,
} from 'lexical';
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
import {
DecoratorBlockNode,
SerializedDecoratorBlockNode,
} from '@lexical/react/LexicalDecoratorBlockNode';
type YouTubeComponentProps = Readonly<{
className: Readonly<{
base: string;
focus: string;
}>;
format: ElementFormatType | null;
nodeKey: NodeKey;
videoID: string;
}>;
function YouTubeComponent({
className,
format,
nodeKey,
videoID,
}: YouTubeComponentProps) {
return (
<BlockWithAlignableContents
className={className}
format={format}
nodeKey={nodeKey}>
<iframe
width="560"
height="315"
src={`https://www.youtube.com/embed/${videoID}`}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen={true}
title="YouTube video"
/>
</BlockWithAlignableContents>
);
}
export type SerializedYouTubeNode = Spread<
{
videoID: string;
type: 'youtube';
version: 1;
},
SerializedDecoratorBlockNode
>;
export class YouTubeNode extends DecoratorBlockNode {
__id: string;
static getType(): string {
return 'youtube';
}
static clone(node: YouTubeNode): YouTubeNode {
return new YouTubeNode(node.__id, node.__format, node.__key);
}
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
const node = $createYouTubeNode(serializedNode.videoID);
node.setFormat(serializedNode.format);
return node;
}
exportJSON(): SerializedYouTubeNode {
return {
...super.exportJSON(),
type: 'youtube',
version: 1,
videoID: this.__id,
};
}
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
super(format, key);
this.__id = id;
}
updateDOM(): false {
return false;
}
getId(): string {
return this.__id;
}
getTextContent(
_includeInert?: boolean | undefined,
_includeDirectionless?: false | undefined,
): string {
return `https://www.youtube.com/watch?v=${this.__id}`;
}
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
const embedBlockTheme = config.theme.embedBlock || {};
const className = {
base: embedBlockTheme.base || '',
focus: embedBlockTheme.focus || '',
};
return (
<YouTubeComponent
className={className}
format={this.__format}
nodeKey={this.getKey()}
videoID={this.__id}
/>
);
}
isInline(): false {
return false;
}
}
export function $createYouTubeNode(videoID: string): YouTubeNode {
return new YouTubeNode(videoID);
}
export function $isYouTubeNode(
node: YouTubeNode | LexicalNode | null | undefined,
): node is YouTubeNode {
return node instanceof YouTubeNode;
}
@@ -0,0 +1,289 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {LexicalEditor} from 'lexical';
import {
AutoEmbedOption,
EmbedConfig,
EmbedMatchResult,
LexicalAutoEmbedPlugin,
URL_MATCHER,
} from '@lexical/react/LexicalAutoEmbedPlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useState} from 'react';
import * as ReactDOM from 'react-dom';
import useModal from '../../Hooks/useModal';
import Button from '../../UI/Button';
import {DialogActions} from '../../UI/Dialog';
import {INSERT_TWEET_COMMAND} from '../TwitterPlugin';
import {INSERT_YOUTUBE_COMMAND} from '../YouTubePlugin';
interface PlaygroundEmbedConfig extends EmbedConfig {
// Human readable name of the embeded content e.g. Tweet or Google Map.
contentName: string;
// Icon for display.
icon?: JSX.Element;
iconName: string;
// An example of a matching url https://twitter.com/jack/status/20
exampleUrl: string;
// For extra searching.
keywords: Array<string>;
// Embed a Figma Project.
description?: string;
}
export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
contentName: 'Youtube Video',
exampleUrl: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
// Icon for display.
icon: <i className="icon youtube" />,
iconName: 'youtube',
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id);
},
keywords: ['youtube', 'video'],
// Determine if a given URL is a match and return url data.
parseUrl: (url: string) => {
const match =
/^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url);
const id = match ? (match?.[2].length === 11 ? match[2] : null) : null;
if (id != null) {
return {
id,
url,
};
}
return null;
},
type: 'youtube-video',
};
export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
// e.g. Tweet or Google Map.
contentName: 'Tweet',
exampleUrl: 'https://twitter.com/jack/status/20',
// Icon for display.
icon: <i className="icon tweet" />,
iconName: 'tweet',
// Create the Lexical embed node from the url data.
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id);
},
// For extra searching.
keywords: ['tweet', 'twitter'],
// Determine if a given URL is a match and return url data.
parseUrl: (text: string) => {
const match =
/^https:\/\/twitter\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)$/.exec(text);
if (match != null) {
return {
id: match[4],
url: match[0],
};
}
return null;
},
type: 'tweet',
};
export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig];
function AutoEmbedMenuItem({
index,
isSelected,
onClick,
onMouseEnter,
option,
}: {
index: number;
isSelected: boolean;
onClick: () => void;
onMouseEnter: () => void;
option: AutoEmbedOption;
}) {
let className = 'item';
if (isSelected) {
className += ' selected';
}
return (
<li
key={option.key}
tabIndex={-1}
className={className}
ref={option.setRefElement}
role="option"
aria-selected={isSelected}
id={'typeahead-item-' + index}
onMouseEnter={onMouseEnter}
onClick={onClick}>
<span className="text">{option.title}</span>
</li>
);
}
function AutoEmbedMenu({
options,
selectedItemIndex,
onOptionClick,
onOptionMouseEnter,
}: {
selectedItemIndex: number | null;
onOptionClick: (option: AutoEmbedOption, index: number) => void;
onOptionMouseEnter: (index: number) => void;
options: Array<AutoEmbedOption>;
}) {
return (
<div className="typeahead-popover">
<ul>
{options.map((option: AutoEmbedOption, i: number) => (
<AutoEmbedMenuItem
index={i}
isSelected={selectedItemIndex === i}
onClick={() => onOptionClick(option, i)}
onMouseEnter={() => onOptionMouseEnter(i)}
key={option.key}
option={option}
/>
))}
</ul>
</div>
);
}
export function AutoEmbedDialog({
embedConfig,
onClose,
}: {
embedConfig: PlaygroundEmbedConfig;
onClose: () => void;
}): JSX.Element {
const [text, setText] = useState('');
const [editor] = useLexicalComposerContext();
const urlMatch = URL_MATCHER.exec(text);
const embedResult =
text != null && urlMatch != null ? embedConfig.parseUrl(text) : null;
const onClick = () => {
if (embedResult != null) {
embedConfig.insertNode(editor, embedResult);
onClose();
}
};
return (
<div style={{width: '600px'}}>
<div className="Input__wrapper">
<input
type="text"
className="Input__input"
placeholder={embedConfig.exampleUrl}
value={text}
data-test-id={`${embedConfig.type}-embed-modal-url`}
onChange={(e) => {
setText(e.target.value);
}}
/>
</div>
<DialogActions>
<Button
disabled={!embedResult}
onClick={onClick}
data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}>
Embed
</Button>
</DialogActions>
</div>
);
}
export default function AutoEmbedPlugin(): JSX.Element {
const [modal, showModal] = useModal();
const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
<AutoEmbedDialog embedConfig={embedConfig} onClose={onClose} />
));
};
const getMenuOptions = (
activeEmbedConfig: PlaygroundEmbedConfig,
embedFn: () => void,
dismissFn: () => void,
) => {
return [
new AutoEmbedOption('Dismiss', {
onSelect: dismissFn,
}),
new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
onSelect: embedFn,
}),
];
};
return (
<>
{modal}
<LexicalAutoEmbedPlugin<PlaygroundEmbedConfig>
embedConfigs={EmbedConfigs}
onOpenEmbedModalForConfig={openEmbedModal}
getMenuOptions={getMenuOptions}
menuRenderFn={(
anchorElementRef,
{selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex},
) =>
anchorElementRef.current
? ReactDOM.createPortal(
<div
className="typeahead-popover auto-embed-menu"
style={{
marginLeft: anchorElementRef.current.style.width,
}}>
<AutoEmbedMenu
options={options}
selectedItemIndex={selectedIndex}
onOptionClick={(option: AutoEmbedOption, index: number) => {
setHighlightedIndex(index);
selectOptionAndCleanUp(option);
}}
onOptionMouseEnter={(index: number) => {
setHighlightedIndex(index);
}}
/>
</div>,
anchorElementRef.current,
)
: null
}
/>
</>
);
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {registerCodeHighlighting} from '@lexical/code';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useEffect} from 'react';
export default function CodeHighlightPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return registerCodeHighlighting(editor);
}, [editor]);
return null;
}
@@ -0,0 +1,57 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
.Collapsible__container {
background: var(--sn-stylekit-contrast-background-color);
border: 1px solid var(--sn-stylekit-contrast-border-color);
border-radius: 10px;
margin-bottom: 8px;
}
.Collapsible__title {
cursor: pointer;
padding: 5px 5px 5px 20px;
position: relative;
font-weight: bold;
list-style: none;
outline: none;
}
.Collapsible__title::marker,
.Collapsible__title::-webkit-details-marker {
display: none;
}
.Collapsible__title:before {
border-style: solid;
border-color: transparent;
border-width: 4px 6px 4px 6px;
border-left-color: #000;
display: block;
content: '';
position: absolute;
left: 7px;
top: 50%;
transform: translateY(-50%);
}
.Collapsible__container[open] .Collapsible__title:before {
border-color: transparent;
border-width: 6px 4px 0 4px;
border-top-color: var(--sn-stylekit-contrast-color);
}
.Collapsible__content {
padding: 0 5px 5px 20px;
}
.Collapsible__collapsed .Collapsible__content {
display: none;
user-select: none;
}
@@ -0,0 +1,102 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
DOMConversionMap,
EditorConfig,
ElementNode,
LexicalNode,
NodeKey,
SerializedElementNode,
Spread,
} from 'lexical';
type SerializedCollapsibleContainerNode = Spread<
{
type: 'collapsible-container';
version: 1;
},
SerializedElementNode
>;
export class CollapsibleContainerNode extends ElementNode {
__open: boolean;
constructor(open: boolean, key?: NodeKey) {
super(key);
this.__open = open;
}
static getType(): string {
return 'collapsible-container';
}
static clone(node: CollapsibleContainerNode): CollapsibleContainerNode {
return new CollapsibleContainerNode(node.__open, node.__key);
}
createDOM(config: EditorConfig): HTMLElement {
const dom = document.createElement('details');
dom.classList.add('Collapsible__container');
dom.open = this.__open;
return dom;
}
updateDOM(
prevNode: CollapsibleContainerNode,
dom: HTMLDetailsElement,
): boolean {
if (prevNode.__open !== this.__open) {
dom.open = this.__open;
}
return false;
}
static importDOM(): DOMConversionMap | null {
return {};
}
static importJSON(
serializedNode: SerializedCollapsibleContainerNode,
): CollapsibleContainerNode {
const node = $createCollapsibleContainerNode();
return node;
}
exportJSON(): SerializedCollapsibleContainerNode {
return {
...super.exportJSON(),
type: 'collapsible-container',
version: 1,
};
}
setOpen(open: boolean): void {
const writable = this.getWritable();
writable.__open = open;
}
getOpen(): boolean {
return this.__open;
}
toggleOpen(): void {
this.setOpen(!this.getOpen());
}
}
export function $createCollapsibleContainerNode(): CollapsibleContainerNode {
return new CollapsibleContainerNode(true);
}
export function $isCollapsibleContainerNode(
node: LexicalNode | null | undefined,
): node is CollapsibleContainerNode {
return node instanceof CollapsibleContainerNode;
}
@@ -0,0 +1,76 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
DOMConversionMap,
EditorConfig,
ElementNode,
LexicalNode,
SerializedElementNode,
Spread,
} from 'lexical';
type SerializedCollapsibleContentNode = Spread<
{
type: 'collapsible-content';
version: 1;
},
SerializedElementNode
>;
export class CollapsibleContentNode extends ElementNode {
static getType(): string {
return 'collapsible-content';
}
static clone(node: CollapsibleContentNode): CollapsibleContentNode {
return new CollapsibleContentNode(node.__key);
}
createDOM(config: EditorConfig): HTMLElement {
const dom = document.createElement('div');
dom.classList.add('Collapsible__content');
return dom;
}
updateDOM(prevNode: CollapsibleContentNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {};
}
static importJSON(
serializedNode: SerializedCollapsibleContentNode,
): CollapsibleContentNode {
return $createCollapsibleContentNode();
}
isShadowRoot(): boolean {
return true;
}
exportJSON(): SerializedCollapsibleContentNode {
return {
...super.exportJSON(),
type: 'collapsible-content',
version: 1,
};
}
}
export function $createCollapsibleContentNode(): CollapsibleContentNode {
return new CollapsibleContentNode();
}
export function $isCollapsibleContentNode(
node: LexicalNode | null | undefined,
): node is CollapsibleContentNode {
return node instanceof CollapsibleContentNode;
}
@@ -0,0 +1,116 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
$createParagraphNode,
$isElementNode,
DOMConversionMap,
EditorConfig,
ElementNode,
LexicalEditor,
LexicalNode,
RangeSelection,
SerializedElementNode,
Spread,
} from 'lexical';
import {$isCollapsibleContainerNode} from './CollapsibleContainerNode';
import {$isCollapsibleContentNode} from './CollapsibleContentNode';
type SerializedCollapsibleTitleNode = Spread<
{
type: 'collapsible-title';
version: 1;
},
SerializedElementNode
>;
export class CollapsibleTitleNode extends ElementNode {
static getType(): string {
return 'collapsible-title';
}
static clone(node: CollapsibleTitleNode): CollapsibleTitleNode {
return new CollapsibleTitleNode(node.__key);
}
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
const dom = document.createElement('summary');
dom.classList.add('Collapsible__title');
return dom;
}
updateDOM(prevNode: CollapsibleTitleNode, dom: HTMLElement): boolean {
return false;
}
static importDOM(): DOMConversionMap | null {
return {};
}
static importJSON(
serializedNode: SerializedCollapsibleTitleNode,
): CollapsibleTitleNode {
return $createCollapsibleTitleNode();
}
exportJSON(): SerializedCollapsibleTitleNode {
return {
...super.exportJSON(),
type: 'collapsible-title',
version: 1,
};
}
collapseAtStart(_selection: RangeSelection): boolean {
this.getParentOrThrow().insertBefore(this);
return true;
}
insertNewAfter(): ElementNode {
const containerNode = this.getParentOrThrow();
if (!$isCollapsibleContainerNode(containerNode)) {
throw new Error(
'CollapsibleTitleNode expects to be child of CollapsibleContainerNode',
);
}
if (containerNode.getOpen()) {
const contentNode = this.getNextSibling();
if (!$isCollapsibleContentNode(contentNode)) {
throw new Error(
'CollapsibleTitleNode expects to have CollapsibleContentNode sibling',
);
}
const firstChild = contentNode.getFirstChild();
if ($isElementNode(firstChild)) {
return firstChild;
} else {
const paragraph = $createParagraphNode();
contentNode.append(paragraph);
return paragraph;
}
} else {
const paragraph = $createParagraphNode();
containerNode.insertAfter(paragraph);
return paragraph;
}
}
}
export function $createCollapsibleTitleNode(): CollapsibleTitleNode {
return new CollapsibleTitleNode();
}
export function $isCollapsibleTitleNode(
node: LexicalNode | null | undefined,
): node is CollapsibleTitleNode {
return node instanceof CollapsibleTitleNode;
}
@@ -0,0 +1,236 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './Collapsible.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
import {
$createParagraphNode,
$getNodeByKey,
$getPreviousSelection,
$getSelection,
$isElementNode,
$isRangeSelection,
$setSelection,
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_LOW,
createCommand,
DELETE_CHARACTER_COMMAND,
INSERT_PARAGRAPH_COMMAND,
KEY_ARROW_DOWN_COMMAND,
NodeKey,
} from 'lexical';
import {useEffect} from 'react';
import {
$createCollapsibleContainerNode,
$isCollapsibleContainerNode,
CollapsibleContainerNode,
} from './CollapsibleContainerNode';
import {
$createCollapsibleContentNode,
$isCollapsibleContentNode,
CollapsibleContentNode,
} from './CollapsibleContentNode';
import {
$createCollapsibleTitleNode,
$isCollapsibleTitleNode,
CollapsibleTitleNode,
} from './CollapsibleTitleNode';
export const INSERT_COLLAPSIBLE_COMMAND = createCommand<void>();
export const TOGGLE_COLLAPSIBLE_COMMAND = createCommand<NodeKey>();
export default function CollapsiblePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (
!editor.hasNodes([
CollapsibleContainerNode,
CollapsibleTitleNode,
CollapsibleContentNode,
])
) {
throw new Error(
'CollapsiblePlugin: CollapsibleContainerNode, CollapsibleTitleNode, or CollapsibleContentNode not registered on editor',
);
}
return mergeRegister(
// Structure enforcing transformers for each node type. In case nesting structure is not
// "Container > Title + Content" it'll unwrap nodes and convert it back
// to regular content.
editor.registerNodeTransform(CollapsibleContentNode, (node) => {
const parent = node.getParent();
if (!$isCollapsibleContainerNode(parent)) {
const children = node.getChildren();
for (const child of children) {
node.insertAfter(child);
}
node.remove();
}
}),
editor.registerNodeTransform(CollapsibleTitleNode, (node) => {
const parent = node.getParent();
if (!$isCollapsibleContainerNode(parent)) {
node.replace($createParagraphNode().append(...node.getChildren()));
}
}),
editor.registerNodeTransform(CollapsibleContainerNode, (node) => {
const children = node.getChildren();
if (
children.length !== 2 ||
!$isCollapsibleTitleNode(children[0]) ||
!$isCollapsibleContentNode(children[1])
) {
for (const child of children) {
node.insertAfter(child);
}
node.remove();
}
}),
// This handles the case when container is collapsed and we delete its previous sibling
// into it, it would cause collapsed content deleted (since it's display: none, and selection
// swallows it when deletes single char). Instead we expand container, which is although
// not perfect, but avoids bigger problem
editor.registerCommand(
DELETE_CHARACTER_COMMAND,
() => {
const selection = $getSelection();
if (
!$isRangeSelection(selection) ||
!selection.isCollapsed() ||
selection.anchor.offset !== 0
) {
return false;
}
const anchorNode = selection.anchor.getNode();
const topLevelElement = anchorNode.getTopLevelElement();
if (topLevelElement === null) {
return false;
}
const container = topLevelElement.getPreviousSibling();
if (!$isCollapsibleContainerNode(container) || container.getOpen()) {
return false;
}
container.setOpen(true);
return true;
},
COMMAND_PRIORITY_LOW,
),
// When collapsible is the last child pressing down arrow will insert paragraph
// below it to allow adding more content. It's similar what $insertBlockNode
// (mainly for decorators), except it'll always be possible to continue adding
// new content even if trailing paragraph is accidentally deleted
editor.registerCommand(
KEY_ARROW_DOWN_COMMAND,
() => {
const selection = $getSelection();
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
return false;
}
const container = $findMatchingParent(
selection.anchor.getNode(),
$isCollapsibleContainerNode,
);
if (container === null) {
return false;
}
const parent = container.getParent();
if (parent !== null && parent.getLastChild() === container) {
parent.append($createParagraphNode());
}
return false;
},
COMMAND_PRIORITY_LOW,
),
// Handling CMD+Enter to toggle collapsible element collapsed state
editor.registerCommand(
INSERT_PARAGRAPH_COMMAND,
() => {
// @ts-ignore
const windowEvent: KeyboardEvent | undefined = editor._window?.event;
if (
windowEvent &&
(windowEvent.ctrlKey || windowEvent.metaKey) &&
windowEvent.key === 'Enter'
) {
const selection = $getPreviousSelection();
if ($isRangeSelection(selection) && selection.isCollapsed()) {
const parent = $findMatchingParent(
selection.anchor.getNode(),
(node) => $isElementNode(node) && !node.isInline(),
);
if ($isCollapsibleTitleNode(parent)) {
const container = parent.getParent();
if ($isCollapsibleContainerNode(container)) {
container.toggleOpen();
$setSelection(selection.clone());
return true;
}
}
}
}
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
INSERT_COLLAPSIBLE_COMMAND,
() => {
editor.update(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const title = $createCollapsibleTitleNode();
const content = $createCollapsibleContentNode().append(
$createParagraphNode(),
);
const container = $createCollapsibleContainerNode().append(
title,
content,
);
selection.insertNodes([container]);
title.selectStart();
});
return true;
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
TOGGLE_COLLAPSIBLE_COMMAND,
(key: NodeKey) => {
editor.update(() => {
const containerNode = $getNodeByKey(key);
if ($isCollapsibleContainerNode(containerNode)) {
containerNode.toggleOpen();
}
});
return true;
},
COMMAND_PRIORITY_EDITOR,
),
);
}, [editor]);
return null;
}
@@ -0,0 +1,36 @@
.draggable-block-menu {
border-radius: 4px;
padding: 2px 1px;
cursor: grab;
opacity: 0;
position: absolute;
left: 0;
top: 0;
will-change: transform;
}
.draggable-block-menu .icon {
width: 16px;
height: 16px;
opacity: 0.3;
background-image: url(#{$blocks-editor-icons-path}/draggable-block-menu.svg);
}
.draggable-block-menu:active {
cursor: grabbing;
}
.draggable-block-menu:hover {
background-color: #efefef;
}
.draggable-block-target-line {
pointer-events: none;
background: deepskyblue;
height: 4px;
position: absolute;
left: 0;
top: 0;
opacity: 0;
will-change: transform;
}
@@ -0,0 +1,365 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {eventFiles} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils';
import {
$getNearestNodeFromDOMNode,
$getNodeByKey,
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_LOW,
DRAGOVER_COMMAND,
DROP_COMMAND,
LexicalEditor,
} from 'lexical';
import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {isHTMLElement} from '../../Utils/guard';
import {Point} from '../../Utils/point';
import {Rect} from '../../Utils/rect';
const SPACE = 4;
const TARGET_LINE_HALF_HEIGHT = 2;
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
const TEXT_BOX_HORIZONTAL_PADDING = 28;
const Downward = 1;
const Upward = -1;
const Indeterminate = 0;
let prevIndex = Infinity;
function getCurrentIndex(keysLength: number): number {
if (keysLength === 0) {
return Infinity;
}
if (prevIndex >= 0 && prevIndex < keysLength) {
return prevIndex;
}
return Math.floor(keysLength / 2);
}
function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
const root = editor.getEditorState()._nodeMap.get('root');
return root ? root.__children : [];
}
function getBlockElement(
anchorElem: HTMLElement,
editor: LexicalEditor,
event: MouseEvent,
): HTMLElement | null {
const anchorElementRect = anchorElem.getBoundingClientRect();
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
let blockElem: HTMLElement | null = null;
editor.getEditorState().read(() => {
let index = getCurrentIndex(topLevelNodeKeys.length);
let direction = Indeterminate;
while (index >= 0 && index < topLevelNodeKeys.length) {
const key = topLevelNodeKeys[index];
const elem = editor.getElementByKey(key);
if (elem === null) {
break;
}
const point = new Point(event.x, event.y);
const domRect = Rect.fromDOM(elem);
const {marginTop, marginBottom} = window.getComputedStyle(elem);
const rect = domRect.generateNewRect({
bottom: domRect.bottom + parseFloat(marginBottom),
left: anchorElementRect.left,
right: anchorElementRect.right,
top: domRect.top - parseFloat(marginTop),
});
const {
result,
reason: {isOnTopSide, isOnBottomSide},
} = rect.contains(point);
if (result) {
blockElem = elem;
prevIndex = index;
break;
}
if (direction === Indeterminate) {
if (isOnTopSide) {
direction = Upward;
} else if (isOnBottomSide) {
direction = Downward;
} else {
// stop search block element
direction = Infinity;
}
}
index += direction;
}
});
return blockElem;
}
function isOnMenu(element: HTMLElement): boolean {
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
}
function setMenuPosition(
targetElem: HTMLElement | null,
floatingElem: HTMLElement,
anchorElem: HTMLElement,
) {
if (!targetElem) {
floatingElem.style.opacity = '0';
floatingElem.style.transform = 'translate(-10000px, -10000px)';
return;
}
const targetRect = targetElem.getBoundingClientRect();
const targetStyle = window.getComputedStyle(targetElem);
const floatingElemRect = floatingElem.getBoundingClientRect();
const anchorElementRect = anchorElem.getBoundingClientRect();
const top =
targetRect.top +
(parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 -
anchorElementRect.top;
const left = SPACE;
floatingElem.style.opacity = '1';
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
}
function setDragImage(
dataTransfer: DataTransfer,
draggableBlockElem: HTMLElement,
) {
const {transform} = draggableBlockElem.style;
// Remove dragImage borders
draggableBlockElem.style.transform = 'translateZ(0)';
dataTransfer.setDragImage(draggableBlockElem, 0, 0);
setTimeout(() => {
draggableBlockElem.style.transform = transform;
});
}
function setTargetLine(
targetLineElem: HTMLElement,
targetBlockElem: HTMLElement,
mouseY: number,
anchorElem: HTMLElement,
) {
const targetStyle = window.getComputedStyle(targetBlockElem);
const {top: targetBlockElemTop, height: targetBlockElemHeight} =
targetBlockElem.getBoundingClientRect();
const {top: anchorTop, width: anchorWidth} =
anchorElem.getBoundingClientRect();
let lineTop = targetBlockElemTop;
// At the bottom of the target
if (mouseY - targetBlockElemTop > targetBlockElemHeight / 2) {
lineTop += targetBlockElemHeight + parseFloat(targetStyle.marginBottom);
} else {
lineTop -= parseFloat(targetStyle.marginTop);
}
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
targetLineElem.style.width = `${
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
}px`;
targetLineElem.style.opacity = '.4';
}
function hideTargetLine(targetLineElem: HTMLElement | null) {
if (targetLineElem) {
targetLineElem.style.opacity = '0';
targetLineElem.style.transform = 'translate(-10000px, -10000px)';
}
}
function useDraggableBlockMenu(
editor: LexicalEditor,
anchorElem: HTMLElement,
isEditable: boolean,
): JSX.Element {
const scrollerElem = anchorElem.parentElement;
const menuRef = useRef<HTMLDivElement>(null);
const targetLineRef = useRef<HTMLDivElement>(null);
const [draggableBlockElem, setDraggableBlockElem] =
useState<HTMLElement | null>(null);
useEffect(() => {
function onMouseMove(event: MouseEvent) {
const target = event.target;
if (!isHTMLElement(target)) {
setDraggableBlockElem(null);
return;
}
if (isOnMenu(target)) {
return;
}
const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
setDraggableBlockElem(_draggableBlockElem);
}
function onMouseLeave() {
setDraggableBlockElem(null);
}
scrollerElem?.addEventListener('mousemove', onMouseMove);
scrollerElem?.addEventListener('mouseleave', onMouseLeave);
return () => {
scrollerElem?.removeEventListener('mousemove', onMouseMove);
scrollerElem?.removeEventListener('mouseleave', onMouseLeave);
};
}, [scrollerElem, anchorElem, editor]);
useEffect(() => {
if (menuRef.current) {
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem);
}
}, [anchorElem, draggableBlockElem]);
useEffect(() => {
function onDragover(event: DragEvent): boolean {
const [isFileTransfer] = eventFiles(event);
if (isFileTransfer) {
return false;
}
const {pageY, target} = event;
if (!isHTMLElement(target)) {
return false;
}
const targetBlockElem = getBlockElement(anchorElem, editor, event);
const targetLineElem = targetLineRef.current;
if (targetBlockElem === null || targetLineElem === null) {
return false;
}
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
// Prevent default event to be able to trigger onDrop events
event.preventDefault();
return true;
}
function onDrop(event: DragEvent): boolean {
const [isFileTransfer] = eventFiles(event);
if (isFileTransfer) {
return false;
}
const {target, dataTransfer, pageY} = event;
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '';
const draggedNode = $getNodeByKey(dragData);
if (!draggedNode) {
return false;
}
if (!isHTMLElement(target)) {
return false;
}
const targetBlockElem = getBlockElement(anchorElem, editor, event);
if (!targetBlockElem) {
return false;
}
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
if (!targetNode) {
return false;
}
if (targetNode === draggedNode) {
return true;
}
const {top, height} = targetBlockElem.getBoundingClientRect();
const shouldInsertAfter = pageY - top > height / 2;
if (shouldInsertAfter) {
targetNode.insertAfter(draggedNode);
} else {
targetNode.insertBefore(draggedNode);
}
setDraggableBlockElem(null);
return true;
}
return mergeRegister(
editor.registerCommand(
DRAGOVER_COMMAND,
(event) => {
return onDragover(event);
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
DROP_COMMAND,
(event) => {
return onDrop(event);
},
COMMAND_PRIORITY_HIGH,
),
);
}, [anchorElem, editor]);
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
const dataTransfer = event.dataTransfer;
if (!dataTransfer || !draggableBlockElem) {
return;
}
setDragImage(dataTransfer, draggableBlockElem);
let nodeKey = '';
editor.update(() => {
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
if (node) {
nodeKey = node.getKey();
}
});
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
}
function onDragEnd(): void {
hideTargetLine(targetLineRef.current);
}
return createPortal(
<>
<div
className="icon draggable-block-menu"
ref={menuRef}
draggable={true}
onDragStart={onDragStart}
onDragEnd={onDragEnd}>
<div className={isEditable ? 'icon' : ''} />
</div>
<div className="draggable-block-target-line" ref={targetLineRef} />
</>,
anchorElem,
);
}
export default function DraggableBlockPlugin({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
return useDraggableBlockMenu(editor, anchorElem, editor._editable);
}
@@ -0,0 +1,261 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {$isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_LOW,
GridSelection,
LexicalEditor,
NodeSelection,
RangeSelection,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import LinkPreview from '../../UI/LinkPreview';
import {getSelectedNode} from '../../Utils/getSelectedNode';
import {sanitizeUrl} from '../../Utils/sanitizeUrl';
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
import {LexicalPencilFill} from '@standardnotes/icons';
import {IconComponent} from '../../../Lexical/Theme/IconComponent';
function FloatingLinkEditor({
editor,
anchorElem,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
}): JSX.Element {
const editorRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [linkUrl, setLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState<
RangeSelection | GridSelection | NodeSelection | null
>(null);
const updateLinkEditor = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const parent = node.getParent();
if ($isLinkNode(parent)) {
setLinkUrl(parent.getURL());
} else if ($isLinkNode(node)) {
setLinkUrl(node.getURL());
} else {
setLinkUrl('');
}
}
const editorElem = editorRef.current;
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (editorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
nativeSelection !== null &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const domRange = nativeSelection.getRangeAt(0);
let rect;
if (nativeSelection.anchorNode === rootElement) {
let inner = rootElement;
while (inner.firstElementChild != null) {
inner = inner.firstElementChild as HTMLElement;
}
rect = inner.getBoundingClientRect();
} else {
rect = domRange.getBoundingClientRect();
}
setFloatingElemPosition(rect, editorElem, anchorElem);
setLastSelection(selection);
} else if (!activeElement || activeElement.className !== 'link-input') {
if (rootElement !== null) {
setFloatingElemPosition(null, editorElem, anchorElem);
}
setLastSelection(null);
setEditMode(false);
setLinkUrl('');
}
return true;
}, [anchorElem, editor]);
useEffect(() => {
const scrollerElem = anchorElem.parentElement;
const update = () => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
};
window.addEventListener('resize', update);
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update);
}
};
}, [anchorElem.parentElement, editor, updateLinkEditor]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(() => {
updateLinkEditor();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateLinkEditor();
return true;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, updateLinkEditor]);
useEffect(() => {
editor.getEditorState().read(() => {
updateLinkEditor();
});
}, [editor, updateLinkEditor]);
useEffect(() => {
if (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
return (
<div ref={editorRef} className="link-editor">
{isEditMode ? (
<input
ref={inputRef}
className="link-input"
value={linkUrl}
onChange={(event) => {
setLinkUrl(event.target.value);
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault();
if (lastSelection !== null) {
if (linkUrl !== '') {
editor.dispatchCommand(
TOGGLE_LINK_COMMAND,
sanitizeUrl(linkUrl),
);
}
setEditMode(false);
}
} else if (event.key === 'Escape') {
event.preventDefault();
setEditMode(false);
}
}}
/>
) : (
<>
<div className="link-input">
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
{linkUrl}
</a>
<div
className="link-edit"
role="button"
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setEditMode(true);
}}>
<IconComponent size={15}>
<LexicalPencilFill />
</IconComponent>
</div>
</div>
<LinkPreview url={linkUrl} />
</>
)}
</div>
);
}
function useFloatingLinkEditorToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null {
const [activeEditor, setActiveEditor] = useState(editor);
const [isLink, setIsLink] = useState(false);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const node = getSelectedNode(selection);
const linkParent = $findMatchingParent(node, $isLinkNode);
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode);
// We don't want this menu to open for auto links.
if (linkParent != null && autoLinkParent == null) {
setIsLink(true);
} else {
setIsLink(false);
}
}
}, []);
useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
updateToolbar();
setActiveEditor(newEditor);
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, updateToolbar]);
return isLink
? createPortal(
<FloatingLinkEditor editor={activeEditor} anchorElem={anchorElem} />,
anchorElem,
)
: null;
}
export default function FloatingLinkEditorPlugin({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
return useFloatingLinkEditorToolbar(editor, anchorElem);
}
@@ -0,0 +1,128 @@
.floating-text-format-popup {
display: flex;
vertical-align: middle;
position: absolute;
top: 0;
left: 0;
z-index: 10;
opacity: 0;
background-color: var(--sn-stylekit-contrast-background-color);
box-shadow: 0px 5px 10px var(--sn-stylekit-shadow-color);
border-radius: 8px;
transition: opacity 0.5s;
will-change: transform;
}
.floating-text-format-popup button.popup-item {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 12px;
cursor: pointer;
vertical-align: middle;
}
.floating-text-format-popup button.popup-item:disabled {
cursor: not-allowed;
}
.floating-text-format-popup button.popup-item.spaced {
margin-right: 2px;
}
.floating-text-format-popup button.popup-item i.format {
background-size: contain;
display: inline-block;
height: 18px;
width: 18px;
margin-top: 2px;
vertical-align: -0.25em;
display: flex;
opacity: 0.6;
}
.floating-text-format-popup button.popup-item:disabled i.format {
opacity: 0.2;
}
.floating-text-format-popup button.popup-item.active {
background-color: rgba(223, 232, 250, 0.3);
}
.floating-text-format-popup button.popup-item.active i {
opacity: 1;
}
.floating-text-format-popup .popup-item:hover:not([disabled]) {
background-color: var(--sn-stylekit-info-color);
color: var(--sn-stylekit-info-contrast-color);
}
.floating-text-format-popup select.popup-item {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 8px;
vertical-align: middle;
-webkit-appearance: none;
-moz-appearance: none;
width: 70px;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
}
.floating-text-format-popup select.code-language {
text-transform: capitalize;
width: 130px;
}
.floating-text-format-popup .popup-item .text {
display: flex;
line-height: 20px;
width: 200px;
vertical-align: middle;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.floating-text-format-popup .popup-item .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
.floating-text-format-popup i.chevron-down {
margin-top: 3px;
width: 16px;
height: 16px;
display: flex;
user-select: none;
}
.floating-text-format-popup i.chevron-down.inside {
width: 16px;
height: 16px;
display: flex;
margin-left: -25px;
margin-top: 11px;
margin-right: 10px;
pointer-events: none;
}
.floating-text-format-popup .divider {
width: 1px;
background-color: #eee;
margin: 0 4px;
}
@@ -0,0 +1,351 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import './index.css';
import {$isCodeHighlightNode} from '@lexical/code';
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
$isTextNode,
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import {getDOMRangeRect} from '../../Utils/getDOMRangeRect';
import {getSelectedNode} from '../../Utils/getSelectedNode';
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
import {
BoldIcon,
ItalicIcon,
UnderlineIcon,
StrikethroughIcon,
CodeIcon,
LinkIcon,
SuperscriptIcon,
SubscriptIcon,
} from '@standardnotes/icons';
import {IconComponent} from '../../Theme/IconComponent';
const IconSize = 15;
function TextFormatFloatingToolbar({
editor,
anchorElem,
isLink,
isBold,
isItalic,
isUnderline,
isCode,
isStrikethrough,
isSubscript,
isSuperscript,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
isBold: boolean;
isCode: boolean;
isItalic: boolean;
isLink: boolean;
isStrikethrough: boolean;
isSubscript: boolean;
isSuperscript: boolean;
isUnderline: boolean;
}): JSX.Element {
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
const insertLink = useCallback(() => {
if (!isLink) {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
} else {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}
}, [editor, isLink]);
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection();
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
const nativeSelection = window.getSelection();
if (popupCharStylesEditorElem === null) {
return;
}
const rootElement = editor.getRootElement();
if (
selection !== null &&
nativeSelection !== null &&
!nativeSelection.isCollapsed &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem);
}
}, [editor, anchorElem]);
useEffect(() => {
const scrollerElem = anchorElem.parentElement;
const update = () => {
editor.getEditorState().read(() => {
updateTextFormatFloatingToolbar();
});
};
window.addEventListener('resize', update);
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update);
}
return () => {
window.removeEventListener('resize', update);
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update);
}
};
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
useEffect(() => {
editor.getEditorState().read(() => {
updateTextFormatFloatingToolbar();
});
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(() => {
updateTextFormatFloatingToolbar();
});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateTextFormatFloatingToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, updateTextFormatFloatingToolbar]);
return (
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
{editor.isEditable() && (
<>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={'popup-item spaced ' + (isBold ? 'active' : '')}
aria-label="Format text as bold">
<IconComponent size={IconSize}>
<BoldIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
aria-label="Format text as italics">
<IconComponent size={IconSize}>
<ItalicIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
aria-label="Format text to underlined">
<IconComponent size={IconSize + 1}>
<UnderlineIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
aria-label="Format text with a strikethrough">
<IconComponent size={IconSize}>
<StrikethroughIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
}}
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
title="Subscript"
aria-label="Format Subscript">
<IconComponent paddingTop={4} size={IconSize - 2}>
<SubscriptIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
}}
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
title="Superscript"
aria-label="Format Superscript">
<IconComponent paddingTop={1} size={IconSize - 2}>
<SuperscriptIcon />
</IconComponent>
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
}}
className={'popup-item spaced ' + (isCode ? 'active' : '')}
aria-label="Insert code block">
<IconComponent size={IconSize}>
<CodeIcon />
</IconComponent>
</button>
<button
onClick={insertLink}
className={'popup-item spaced ' + (isLink ? 'active' : '')}
aria-label="Insert link">
<IconComponent size={IconSize}>
<LinkIcon />
</IconComponent>
</button>
</>
)}
</div>
);
}
function useFloatingTextFormatToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null {
const [isText, setIsText] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const [isCode, setIsCode] = useState(false);
const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
// Should not to pop up the floating toolbar when using IME input
if (editor.isComposing()) {
return;
}
const selection = $getSelection();
const nativeSelection = window.getSelection();
const rootElement = editor.getRootElement();
if (
nativeSelection !== null &&
(!$isRangeSelection(selection) ||
rootElement === null ||
!rootElement.contains(nativeSelection.anchorNode))
) {
setIsText(false);
return;
}
if (!$isRangeSelection(selection)) {
return;
}
const node = getSelectedNode(selection);
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsSubscript(selection.hasFormat('subscript'));
setIsSuperscript(selection.hasFormat('superscript'));
setIsCode(selection.hasFormat('code'));
// Update links
const parent = node.getParent();
if ($isLinkNode(parent) || $isLinkNode(node)) {
setIsLink(true);
} else {
setIsLink(false);
}
if (
!$isCodeHighlightNode(selection.anchor.getNode()) &&
selection.getTextContent() !== ''
) {
setIsText($isTextNode(node));
} else {
setIsText(false);
}
});
}, [editor]);
useEffect(() => {
document.addEventListener('selectionchange', updatePopup);
return () => {
document.removeEventListener('selectionchange', updatePopup);
};
}, [updatePopup]);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(() => {
updatePopup();
}),
editor.registerRootListener(() => {
if (editor.getRootElement() === null) {
setIsText(false);
}
}),
);
}, [editor, updatePopup]);
if (!isText || isLink) {
return null;
}
return createPortal(
<TextFormatFloatingToolbar
editor={editor}
anchorElem={anchorElem}
isLink={isLink}
isBold={isBold}
isItalic={isItalic}
isStrikethrough={isStrikethrough}
isSubscript={isSubscript}
isSuperscript={isSuperscript}
isUnderline={isUnderline}
isCode={isCode}
/>,
anchorElem,
);
}
export default function FloatingTextFormatToolbarPlugin({
anchorElem = document.body,
}: {
anchorElem?: HTMLElement;
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
return useFloatingTextFormatToolbar(editor, anchorElem);
}
@@ -0,0 +1,51 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$createHorizontalRuleNode,
INSERT_HORIZONTAL_RULE_COMMAND,
} from '@lexical/react/LexicalHorizontalRuleNode';
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_EDITOR,
} from 'lexical';
import {useEffect} from 'react';
export default function HorizontalRulePlugin(): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
INSERT_HORIZONTAL_RULE_COMMAND,
(type) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const focusNode = selection.focus.getNode();
if (focusNode !== null) {
const horizontalRuleNode = $createHorizontalRuleNode();
selection.focus
.getNode()
.getTopLevelElementOrThrow()
.insertBefore(horizontalRuleNode);
}
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);
return null;
}
@@ -0,0 +1,77 @@
import {$getListDepth, $isListItemNode, $isListNode} from '@lexical/list';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
RangeSelection,
$getSelection,
$isElementNode,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
ElementNode,
INDENT_CONTENT_COMMAND,
} from 'lexical';
import {useEffect} from 'react';
type Props = Readonly<{
maxDepth: number | null | undefined;
}>;
function getElementNodesInSelection(
selection: RangeSelection,
): Set<ElementNode> {
const nodesInSelection = selection.getNodes();
if (nodesInSelection.length === 0) {
return new Set([
selection.anchor.getNode().getParentOrThrow(),
selection.focus.getNode().getParentOrThrow(),
]);
}
return new Set(
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())),
);
}
function isIndentPermitted(maxDepth: number): boolean {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}
const elementNodesInSelection: Set<ElementNode> =
getElementNodesInSelection(selection);
let totalDepth = 0;
for (const elementNode of elementNodesInSelection) {
if ($isListNode(elementNode)) {
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
} else if ($isListItemNode(elementNode)) {
const parent = elementNode.getParent();
if (!$isListNode(parent)) {
throw new Error(
'ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent.',
);
}
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
}
}
return totalDepth <= maxDepth;
}
export default function ListMaxIndentLevelPlugin({maxDepth}: Props): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
return editor.registerCommand(
INDENT_CONTENT_COMMAND,
() => !isIndentPermitted(maxDepth ?? 7),
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, maxDepth]);
return null;
}
@@ -0,0 +1,212 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {INSERT_TABLE_COMMAND} from '@lexical/table';
import {
$createNodeSelection,
$createParagraphNode,
$getSelection,
$isRangeSelection,
$isRootOrShadowRoot,
$setSelection,
COMMAND_PRIORITY_EDITOR,
createCommand,
EditorThemeClasses,
Klass,
LexicalCommand,
LexicalEditor,
LexicalNode,
} from 'lexical';
import {createContext, useContext, useEffect, useMemo, useState} from 'react';
import * as React from 'react';
import invariant from '../Shared/invariant';
import {$createTableNodeWithDimensions, TableNode} from '../Nodes/TableNode';
import Button from '../UI/Button';
import {DialogActions} from '../UI/Dialog';
import TextInput from '../UI/TextInput';
export type InsertTableCommandPayload = Readonly<{
columns: string;
rows: string;
includeHeaders?: boolean;
}>;
export type CellContextShape = {
cellEditorConfig: null | CellEditorConfig;
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>;
set: (
cellEditorConfig: null | CellEditorConfig,
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>,
) => void;
};
export type CellEditorConfig = Readonly<{
namespace: string;
nodes?: ReadonlyArray<Klass<LexicalNode>>;
onError: (error: Error, editor: LexicalEditor) => void;
readOnly?: boolean;
theme?: EditorThemeClasses;
}>;
export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
createCommand('INSERT_NEW_TABLE_COMMAND');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: not sure why TS doesn't like using null as the value?
export const CellContext: React.Context<CellContextShape> = createContext({
cellEditorConfig: null,
cellEditorPlugins: null,
set: () => {
// Empty
},
});
export function TableContext({children}: {children: JSX.Element}) {
const [contextValue, setContextValue] = useState<{
cellEditorConfig: null | CellEditorConfig;
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>;
}>({
cellEditorConfig: null,
cellEditorPlugins: null,
});
return (
<CellContext.Provider
value={useMemo(
() => ({
cellEditorConfig: contextValue.cellEditorConfig,
cellEditorPlugins: contextValue.cellEditorPlugins,
set: (cellEditorConfig, cellEditorPlugins) => {
setContextValue({cellEditorConfig, cellEditorPlugins});
},
}),
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
)}>
{children}
</CellContext.Provider>
);
}
export function InsertTableDialog({
activeEditor,
onClose,
}: {
activeEditor: LexicalEditor;
onClose: () => void;
}): JSX.Element {
const [rows, setRows] = useState('5');
const [columns, setColumns] = useState('5');
const onClick = () => {
activeEditor.dispatchCommand(INSERT_TABLE_COMMAND, {columns, rows});
onClose();
};
return (
<>
<TextInput label="No of rows" onChange={setRows} value={rows} />
<TextInput label="No of columns" onChange={setColumns} value={columns} />
<DialogActions data-test-id="table-model-confirm-insert">
<Button onClick={onClick}>Confirm</Button>
</DialogActions>
</>
);
}
export function InsertNewTableDialog({
activeEditor,
onClose,
}: {
activeEditor: LexicalEditor;
onClose: () => void;
}): JSX.Element {
const [rows, setRows] = useState('5');
const [columns, setColumns] = useState('5');
const onClick = () => {
activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, {columns, rows});
onClose();
};
return (
<>
<TextInput label="No of rows" onChange={setRows} value={rows} />
<TextInput label="No of columns" onChange={setColumns} value={columns} />
<DialogActions data-test-id="table-model-confirm-insert">
<Button onClick={onClick}>Confirm</Button>
</DialogActions>
</>
);
}
export function TablePlugin({
cellEditorConfig,
children,
}: {
cellEditorConfig: CellEditorConfig;
children: JSX.Element | Array<JSX.Element>;
}): JSX.Element | null {
const [editor] = useLexicalComposerContext();
const cellContext = useContext(CellContext);
useEffect(() => {
if (!editor.hasNodes([TableNode])) {
invariant(false, 'TablePlugin: TableNode is not registered on editor');
}
cellContext.set(cellEditorConfig, children);
return editor.registerCommand<InsertTableCommandPayload>(
INSERT_TABLE_COMMAND,
({columns, rows, includeHeaders}) => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return true;
}
const focus = selection.focus;
const focusNode = focus.getNode();
if (focusNode !== null) {
const tableNode = $createTableNodeWithDimensions(
Number(rows),
Number(columns),
includeHeaders,
);
if ($isRootOrShadowRoot(focusNode)) {
const target = focusNode.getChildAtIndex(focus.offset);
if (target !== null) {
target.insertBefore(tableNode);
} else {
focusNode.append(tableNode);
}
tableNode.insertBefore($createParagraphNode());
} else {
const topLevelNode = focusNode.getTopLevelElementOrThrow();
topLevelNode.insertAfter(tableNode);
}
tableNode.insertAfter($createParagraphNode());
const nodeSelection = $createNodeSelection();
nodeSelection.add(tableNode.getKey());
$setSelection(nodeSelection);
}
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [cellContext, cellEditorConfig, children, editor]);
return null;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$insertNodeToNearestRoot} from '@lexical/utils';
import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
import {useEffect} from 'react';
import {$createTweetNode, TweetNode} from '../../Nodes/TweetNode';
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand(
'INSERT_TWEET_COMMAND',
);
export default function TwitterPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([TweetNode])) {
throw new Error('TwitterPlugin: TweetNode not registered on editor');
}
return editor.registerCommand<string>(
INSERT_TWEET_COMMAND,
(payload) => {
const tweetNode = $createTweetNode(payload);
$insertNodeToNearestRoot(tweetNode);
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);
return null;
}
@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {$insertNodeToNearestRoot} from '@lexical/utils';
import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
import {useEffect} from 'react';
import {$createYouTubeNode, YouTubeNode} from '../../Nodes/YouTubeNode';
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand(
'INSERT_YOUTUBE_COMMAND',
);
export default function YouTubePlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([YouTubeNode])) {
throw new Error('YouTubePlugin: YouTubeNode not registered on editor');
}
return editor.registerCommand<string>(
INSERT_YOUTUBE_COMMAND,
(payload) => {
const youTubeNode = $createYouTubeNode(payload);
$insertNodeToNearestRoot(youTubeNode);
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);
return null;
}
@@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export const CAN_USE_DOM: boolean =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined' &&
typeof window.document.createElement !== 'undefined';
@@ -0,0 +1,46 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {CAN_USE_DOM} from './canUseDOM';
declare global {
interface Document {
documentMode?: string;
}
interface Window {
MSStream?: unknown;
}
}
const documentMode =
CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
export const IS_APPLE: boolean =
CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const IS_FIREFOX: boolean =
CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
export const CAN_USE_BEFORE_INPUT: boolean =
CAN_USE_DOM && 'InputEvent' in window && !documentMode
? 'getTargetRanges' in new window.InputEvent('input')
: false;
export const IS_SAFARI: boolean =
CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
export const IS_IOS: boolean =
CAN_USE_DOM &&
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
!window.MSStream;
// Keep these in case we need to use them in the future.
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
// export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent);
// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode;
@@ -0,0 +1,17 @@
// invariant(condition, message) will refine types based on "condition", and
// if "condition" is false will throw an error. This function is special-cased
// in flow itself, so we can't name it anything else.
export default function invariant(
cond?: boolean,
message?: string,
...args: string[]
): asserts cond {
if (cond) {
return;
}
throw new Error(
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
'time. There is no runtime version.',
);
}
@@ -0,0 +1,15 @@
export const IconComponent = ({
children,
size = 20,
paddingTop = 0,
}: {
children: React.ReactNode;
size?: number;
paddingTop?: number;
}) => {
return (
<span className="svg-icon" style={{width: size, height: size, paddingTop}}>
{children}
</span>
);
};
@@ -0,0 +1,101 @@
import type {EditorThemeClasses} from 'lexical';
const BlocksEditorTheme: EditorThemeClasses = {
characterLimit: 'Lexical__characterLimit',
code: 'Lexical__code',
codeHighlight: {
atrule: 'Lexical__tokenAttr',
attr: 'Lexical__tokenAttr',
boolean: 'Lexical__tokenProperty',
builtin: 'Lexical__tokenSelector',
cdata: 'Lexical__tokenComment',
char: 'Lexical__tokenSelector',
class: 'Lexical__tokenFunction',
'class-name': 'Lexical__tokenFunction',
comment: 'Lexical__tokenComment',
constant: 'Lexical__tokenProperty',
deleted: 'Lexical__tokenProperty',
doctype: 'Lexical__tokenComment',
entity: 'Lexical__tokenOperator',
function: 'Lexical__tokenFunction',
important: 'Lexical__tokenVariable',
inserted: 'Lexical__tokenSelector',
keyword: 'Lexical__tokenAttr',
namespace: 'Lexical__tokenVariable',
number: 'Lexical__tokenProperty',
operator: 'Lexical__tokenOperator',
prolog: 'Lexical__tokenComment',
property: 'Lexical__tokenProperty',
punctuation: 'Lexical__tokenPunctuation',
regex: 'Lexical__tokenVariable',
selector: 'Lexical__tokenSelector',
string: 'Lexical__tokenSelector',
symbol: 'Lexical__tokenProperty',
tag: 'Lexical__tokenProperty',
url: 'Lexical__tokenOperator',
variable: 'Lexical__tokenVariable',
},
embedBlock: {
base: 'Lexical__embedBlock',
focus: 'Lexical__embedBlockFocus',
},
hashtag: 'Lexical__hashtag',
heading: {
h1: 'Lexical__h1',
h2: 'Lexical__h2',
h3: 'Lexical__h3',
h4: 'Lexical__h4',
h5: 'Lexical__h5',
h6: 'Lexical__h6',
},
image: 'editor-image',
link: 'Lexical__link',
list: {
listitem: 'Lexical__listItem',
listitemChecked: 'Lexical__listItemChecked',
listitemUnchecked: 'Lexical__listItemUnchecked',
nested: {
listitem: 'Lexical__nestedListItem',
},
olDepth: [
'Lexical__ol1',
'Lexical__ol2',
'Lexical__ol3',
'Lexical__ol4',
'Lexical__ol5',
],
ul: 'Lexical__ul',
},
ltr: 'Lexical__ltr',
mark: 'Lexical__mark',
markOverlap: 'Lexical__markOverlap',
paragraph: 'Lexical__paragraph',
quote: 'Lexical__quote',
rtl: 'Lexical__rtl',
table: 'Lexical__table',
tableAddColumns: 'Lexical__tableAddColumns',
tableAddRows: 'Lexical__tableAddRows',
tableCell: 'Lexical__tableCell',
tableCellActionButton: 'Lexical__tableCellActionButton',
tableCellActionButtonContainer: 'Lexical__tableCellActionButtonContainer',
tableCellEditing: 'Lexical__tableCellEditing',
tableCellHeader: 'Lexical__tableCellHeader',
tableCellPrimarySelected: 'Lexical__tableCellPrimarySelected',
tableCellResizer: 'Lexical__tableCellResizer',
tableCellSelected: 'Lexical__tableCellSelected',
tableCellSortedIndicator: 'Lexical__tableCellSortedIndicator',
tableResizeRuler: 'Lexical__tableCellResizeRuler',
tableSelected: 'Lexical__tableSelected',
text: {
bold: 'Lexical__textBold',
code: 'Lexical__textCode',
italic: 'Lexical__textItalic',
strikethrough: 'Lexical__textStrikethrough',
subscript: 'Lexical__textSubscript',
superscript: 'Lexical__textSuperscript',
underline: 'Lexical__textUnderline',
underlineStrikethrough: 'Lexical__textUnderlineStrikethrough',
},
};
export default BlocksEditorTheme;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
@import '../Plugins//DraggableBlockPlugin/index.scss';
#typeahead-menu {
z-index: 10000;
}
#blocks-editor {
p {
color: var(--sn-stylekit-editor-foreground-color);
}
}
@@ -0,0 +1,427 @@
.Lexical__ltr {
text-align: left;
}
.Lexical__rtl {
text-align: right;
}
.Lexical__paragraph {
margin: 0;
position: relative;
}
.Lexical__quote {
margin: 0;
margin-left: 20px;
margin-bottom: 10px;
font-size: 15px;
color: rgb(101, 103, 107);
border-left-color: rgb(206, 208, 212);
border-left-width: 4px;
border-left-style: solid;
padding-left: 16px;
}
.Lexical__h1 {
font-size: 24px;
color: var(--sn-stylekit-editor-foreground-color);
font-weight: 400;
margin: 0;
}
.Lexical__h2 {
font-size: 15px;
color: var(--sn-stylekit-editor-foreground-color);
font-weight: 700;
margin: 0;
text-transform: uppercase;
}
.Lexical__h3 {
font-size: 12px;
margin: 0;
text-transform: uppercase;
}
.Lexical__textBold {
font-weight: bold;
}
.Lexical__textItalic {
font-style: italic;
}
.Lexical__textUnderline {
text-decoration: underline;
}
.Lexical__textStrikethrough {
text-decoration: line-through;
}
.Lexical__textUnderlineStrikethrough {
text-decoration: underline line-through;
}
.Lexical__textSubscript {
font-size: 0.8em;
vertical-align: sub !important;
}
.Lexical__textSuperscript {
font-size: 0.8em;
vertical-align: super;
}
.Lexical__textCode {
background-color: var(--sn-stylekit-secondary-background-color);
color: var(--sn-stylekit-info-color);
padding: 5px;
border-radius: 5px;
font-family: Menlo, Consolas, Monaco, monospace;
font-size: 85%;
}
.Lexical__hashtag {
background-color: rgba(88, 144, 255, 0.15);
border-bottom: 1px solid rgba(88, 144, 255, 0.3);
}
.Lexical__link {
color: var(--sn-stylekit-info-color);
text-decoration: none;
}
.Lexical__link:hover {
text-decoration: underline;
}
.Lexical__code {
background-color: var(--sn-stylekit-contrast-background-color);
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 8px 8px 52px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
tab-size: 2;
/* white-space: pre; */
overflow-x: auto;
position: relative;
}
.Lexical__code:before {
content: attr(data-gutter);
position: absolute;
background-color: var(--sn-stylekit-secondary-background-color);
left: 0;
top: 0;
border-right: 1px solid var(--sn-stylekit-contrast-border-color);
padding: 8px;
color: var(--sn-stylekit-info-color);
white-space: pre-wrap;
text-align: right;
min-width: 25px;
}
.Lexical__table {
border-collapse: collapse;
border-spacing: 0;
max-width: 100%;
overflow-y: scroll;
table-layout: fixed;
width: calc(100% - 25px);
margin: 30px 0;
color: var(--sn-stylekit-contrast-foreground-color);
}
.Lexical__tableSelected {
outline: 2px solid rgb(60, 132, 244);
}
.Lexical__tableCell {
border: 1px solid var(--sn-stylekit-border-color);
min-width: 75px;
vertical-align: top;
text-align: start;
padding: 6px 8px;
position: relative;
cursor: default;
outline: none;
}
.Lexical__tableCellSortedIndicator {
display: block;
opacity: 0.5;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: #999;
}
.Lexical__tableCellResizer {
position: absolute;
right: -4px;
height: 100%;
width: 8px;
cursor: ew-resize;
z-index: 10;
top: 0;
}
.Lexical__tableCellHeader {
background-color: var(--sn-stylekit-contrast-background-color);
border-color: var(--sn-stylekit-contrast-border-color);
text-align: start;
}
.Lexical__tableCellSelected {
background-color: #c9dbf0;
}
.Lexical__tableCellPrimarySelected {
border: 2px solid rgb(60, 132, 244);
display: block;
height: calc(100% - 2px);
position: absolute;
width: calc(100% - 2px);
left: -1px;
top: -1px;
z-index: 2;
}
.Lexical__tableCellEditing {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
border-radius: 3px;
}
.Lexical__tableAddColumns {
position: absolute;
top: 0;
width: 20px;
background-color: #eee;
height: 100%;
right: 0;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
}
.Lexical__tableAddColumns:after {
background-image: url(#{$blocks-editor-icons-path}/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
.Lexical__tableAddColumns:hover {
background-color: #c9dbf0;
}
.Lexical__tableAddRows {
position: absolute;
bottom: -25px;
width: calc(100% - 25px);
background-color: #eee;
height: 20px;
left: 0;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
}
.Lexical__tableAddRows:after {
background-image: url(#{$blocks-editor-icons-path}/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
.Lexical__tableAddRows:hover {
background-color: #c9dbf0;
}
@keyframes table-controls {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.Lexical__tableCellResizeRuler {
display: block;
position: absolute;
width: 1px;
background-color: rgb(60, 132, 244);
height: 100%;
top: 0;
}
.Lexical__tableCellActionButtonContainer {
display: block;
right: 5px;
top: 6px;
position: absolute;
z-index: 4;
width: 20px;
height: 20px;
}
.Lexical__tableCellActionButton {
background-color: #eee;
display: block;
border: 0;
border-radius: 20px;
width: 20px;
height: 20px;
color: #222;
cursor: pointer;
}
.Lexical__tableCellActionButton:hover {
background-color: #ddd;
}
.Lexical__characterLimit {
display: inline;
background-color: #ffbbbb !important;
}
.Lexical__ol1 {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-position: inside;
}
.Lexical__ol2 {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: upper-alpha;
list-style-position: inside;
}
.Lexical__ol3 {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: lower-alpha;
list-style-position: inside;
}
.Lexical__ol4 {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: upper-roman;
list-style-position: inside;
}
.Lexical__ol5 {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-type: lower-roman;
list-style-position: inside;
}
.Lexical__ul {
padding: 0;
margin: 0;
margin-left: 16px;
list-style-position: inside;
}
.Lexical__listItem {
margin: 0 0px;
}
.Lexical__listItemChecked,
.Lexical__listItemUnchecked {
position: relative;
margin-left: 8px;
margin-right: 8px;
padding-left: 24px;
padding-right: 24px;
list-style-type: none;
outline: none;
vertical-align: middle;
}
.Lexical__listItemChecked {
text-decoration: line-through;
}
.Lexical__listItemUnchecked:before,
.Lexical__listItemChecked:before {
content: '';
width: 16px;
height: 16px;
left: 0;
top: 5px;
cursor: pointer;
background-size: cover;
position: absolute;
}
.Lexical__listItemUnchecked[dir='rtl']:before,
.Lexical__listItemChecked[dir='rtl']:before {
left: auto;
right: 0;
}
.Lexical__listItemUnchecked:focus:before,
.Lexical__listItemChecked:focus:before {
box-shadow: 0 0 0 2px #a6cdfe;
border-radius: 2px;
}
.Lexical__listItemUnchecked:before {
border: 1px solid #999;
border-radius: 2px;
}
.Lexical__listItemChecked:before {
border: 1px solid var(--sn-stylekit-info-color);
border-radius: 2px;
background-color: var(--sn-stylekit-info-color);
background-repeat: no-repeat;
}
.Lexical__listItemChecked:after {
content: '';
cursor: pointer;
border-color: var(--sn-stylekit-info-contrast-color);
border-style: solid;
position: absolute;
display: block;
top: 7px;
width: 5px;
left: 6px;
height: 10px;
transform: rotate(45deg);
border-width: 0 2px 2px 0;
}
.Lexical__nestedListItem {
list-style-type: none;
}
.Lexical__nestedListItem:before,
.Lexical__nestedListItem:after {
display: none;
}
.Lexical__tokenComment {
color: slategray;
}
.Lexical__tokenPunctuation {
color: #999;
}
.Lexical__tokenProperty {
color: #905;
}
.Lexical__tokenSelector {
color: #690;
}
.Lexical__tokenOperator {
color: #9a6e3a;
}
.Lexical__tokenAttr {
color: #07a;
}
.Lexical__tokenVariable {
color: #e90;
}
.Lexical__tokenFunction {
color: #dd4a68;
}
.Lexical__mark {
background: rgba(255, 212, 0, 0.14);
border-bottom: 2px solid rgba(255, 212, 0, 0.3);
padding-bottom: 2px;
}
.Lexical__markOverlap {
background: rgba(255, 212, 0, 0.3);
border-bottom: 2px solid rgba(255, 212, 0, 0.7);
}
.Lexical__mark.selected {
background: rgba(255, 212, 0, 0.5);
border-bottom: 2px solid rgba(255, 212, 0, 1);
}
.Lexical__markOverlap.selected {
background: rgba(255, 212, 0, 0.7);
border-bottom: 2px solid rgba(255, 212, 0, 0.7);
}
.Lexical__embedBlock {
user-select: none;
}
.Lexical__embedBlockFocus {
outline: 2px solid rgb(60, 132, 244);
}
@@ -0,0 +1,117 @@
.icon.plus {
background-image: url(#{$blocks-editor-icons-path}/plus.svg);
}
/**
.icon.dropdown-more {
background-image: url(#{$blocks-editor-icons-path}/dropdown-more.svg);
}
.icon.font-color {
background-image: url(#{$blocks-editor-icons-path}/font-color.svg);
}
.icon.font-family {
background-image: url(#{$blocks-editor-icons-path}/font-family.svg);
}
.icon.bg-color {
background-image: url(#{$blocks-editor-icons-path}/bg-color.svg);
}
i.palette {
background-image: url(#{$blocks-editor-icons-path}/palette.svg);
}
i.bucket {
background-image: url(#{$blocks-editor-icons-path}/paint-bucket.svg);
}
i.clear {
background-image: url(#{$blocks-editor-icons-path}/trash.svg);
}
i.image {
background-image: url(#{$blocks-editor-icons-path}/file-image.svg);
}
i.close {
background-image: url(#{$blocks-editor-icons-path}/close.svg);
}
i.figma {
background-image: url(#{$blocks-editor-icons-path}/figma.svg);
}
i.poll {
background-image: url(#{$blocks-editor-icons-path}/card-checklist.svg);
}
i.indent {
background-image: url(#{$blocks-editor-icons-path}/indent.svg);
}
i.markdown {
background-image: url(#{$blocks-editor-icons-path}/markdown.svg);
}
i.outdent {
background-image: url(#{$blocks-editor-icons-path}/outdent.svg);
}
i.undo {
background-image: url(#{$blocks-editor-icons-path}/arrow-counterclockwise.svg);
}
i.redo {
background-image: url(#{$blocks-editor-icons-path}/arrow-clockwise.svg);
}
i.sticky {
background-image: url(#{$blocks-editor-icons-path}/sticky.svg);
}
i.mic {
background-image: url(#{$blocks-editor-icons-path}/mic.svg);
}
i.import {
background-image: url(#{$blocks-editor-icons-path}/upload.svg);
}
i.export {
background-image: url(#{$blocks-editor-icons-path}/download.svg);
}
i.diagram-2 {
background-image: url(#{$blocks-editor-icons-path}/diagram-2.svg);
}
i.user {
background-image: url(#{$blocks-editor-icons-path}/user.svg);
}
i.equation {
background-image: url(#{$blocks-editor-icons-path}/plus-slash-minus.svg);
}
i.gif {
background-image: url(#{$blocks-editor-icons-path}/filetype-gif.svg);
}
i.copy {
background-image: url(#{$blocks-editor-icons-path}/copy.svg);
}
i.success {
background-image: url(#{$blocks-editor-icons-path}/success.svg);
}
i.prettier {
background-image: url(#{$blocks-editor-icons-path}/prettier.svg);
}
i.prettier-error {
background-image: url(#{$blocks-editor-icons-path}/prettier-error.svg);
}
*/
@@ -0,0 +1,5 @@
@import 'base';
@import 'custom';
@import 'editor';
@import 'icons';
@import 'links';
@@ -0,0 +1,110 @@
.button.active,
.toolbar .button.active {
background-color: var(--sn-stylekit-contrast-background-color);
}
.link-editor {
background-color: var(--sn-stylekit-secondary-background-color);
border-radius: 8px;
border: 0;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
box-sizing: border-box;
display: absolute;
font-family: inherit;
font-size: 15px;
left: 0;
margin: 8px 12px;
max-width: 400px;
opacity: 0;
outline: 0;
padding: 8px 12px;
position: absolute;
top: 0;
transition: opacity 0.5s;
width: 100%;
will-change: transform;
z-index: 10;
}
.link-input {
background-color: var(--sn-stylekit-background-color);
border-radius: 15px;
border: 0;
box-sizing: border-box;
color: var(--sn-stylekit-contrast-color);
display: absolute;
font-family: inherit;
font-size: 15px;
margin: 8px 12px;
outline: 0;
padding: 8px 12px;
position: relative;
width: calc(100% - 24px);
}
.link-editor div.link-edit {
display: flex;
align-items: center;
background-size: 16px;
background-position: center;
background-repeat: no-repeat;
vertical-align: -0.25em;
position: absolute;
right: 12px;
top: 0;
bottom: 0;
cursor: pointer;
}
.link-editor .link-input a {
color: rgb(33, 111, 219);
text-decoration: none;
display: block;
white-space: nowrap;
overflow: hidden;
margin-right: 30px;
text-overflow: ellipsis;
}
.link-editor .link-input a:hover {
text-decoration: underline;
}
.link-editor .font-size-wrapper,
.link-editor .font-family-wrapper {
display: flex;
margin: 0 4px;
}
.link-editor select {
padding: 6px;
border: none;
background-color: rgba(0, 0, 0, 0.075);
border-radius: 4px;
}
.link-editor .button {
width: 20px;
height: 20px;
display: inline-block;
padding: 6px;
border-radius: 8px;
cursor: pointer;
margin: 0 2px;
}
.link-editor .button.hovered {
width: 20px;
height: 20px;
display: inline-block;
background-color: #eee;
}
.link-editor .button i,
.actions i {
background-size: contain;
display: inline-block;
height: 20px;
width: 20px;
vertical-align: -0.25em;
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*
*/
.Button__root {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 15px;
padding-right: 15px;
border: 0px;
background-color: #eee;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.Button__root:hover {
background-color: #ddd;
}
.Button__small {
padding-top: 5px;
padding-bottom: 5px;
padding-left: 10px;
padding-right: 10px;
font-size: 13px;
}
.Button__disabled {
cursor: not-allowed;
}
.Button__disabled:hover {
background-color: #eee;
}

Some files were not shown because too many files have changed in this diff Show More