commit
39c3ca3ef0
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write"
|
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
|
||||||
|
"package.json": "sort-package-json"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,6 @@
|
||||||
"custom-properties",
|
"custom-properties",
|
||||||
"declarations"
|
"declarations"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"defaultSeverity": "warning"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
[0.6.0]
|
||||||
|
[+] editor enabled
|
||||||
|
|
||||||
[0.5.1]
|
[0.5.1]
|
||||||
[+] nanostores-base global store
|
[+] nanostores-base global store
|
||||||
[-] Root.tsx components
|
[-] Root.tsx components
|
||||||
|
|
|
@ -39,41 +39,31 @@ const astroConfig: AstroUserConfig = {
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
plugins: [visualizer()],
|
plugins: [visualizer()],
|
||||||
output: {
|
// output: {
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// manualChunks(id) {
|
||||||
manualChunks(id) {
|
// if (id.includes('p2p')) return 'p2p'
|
||||||
if (id.includes('node_modules')) {
|
// if (id.includes('editor') || id.includes('Editor')) return 'editor'
|
||||||
// FIXME: doesn't work in production
|
// if (id.includes('node_modules')) {
|
||||||
// if (id.includes('solid')) {
|
// let chunkid
|
||||||
// chunkid = 'solid'
|
// if (id.includes('solid')) chunkid = 'solid'
|
||||||
// }
|
// if (id.includes('swiper')) chunkid = 'swiper'
|
||||||
// if (id.includes('acorn')) {
|
// if (id.includes('acorn')) chunkid = 'acorn'
|
||||||
// chunkid = 'acorn'
|
// if (id.includes('prosemirror')) chunkid = 'editor'
|
||||||
// }
|
// if (id.includes('markdown') || id.includes('mdurl') || id.includes('yjs')) {
|
||||||
// if (id.includes('simple-peer')) {
|
// chunkid = 'codecs'
|
||||||
// chunkid = 'simple-peer'
|
|
||||||
// }
|
|
||||||
// if (id.includes('prosemirror')) {
|
|
||||||
// chunkid = 'prosemirror'
|
|
||||||
// }
|
|
||||||
// if (id.includes('markdown') || id.includes('mdurl')) {
|
|
||||||
// chunkid = 'markdown'
|
|
||||||
// }
|
|
||||||
// if (id.includes('swiper')) {
|
|
||||||
// chunkid = 'swiper'
|
|
||||||
// }
|
// }
|
||||||
// if (
|
// if (
|
||||||
// id.includes('yjs') ||
|
// id.includes('p2p') ||
|
||||||
// id.includes('y-prosemirror') ||
|
|
||||||
// id.includes('y-protocols') ||
|
// id.includes('y-protocols') ||
|
||||||
// id.includes('y-webrtc')
|
// id.includes('y-webrtc') ||
|
||||||
|
// id.includes('simple-peer')
|
||||||
// ) {
|
// ) {
|
||||||
// chunkid = 'yjs'
|
// chunkid = 'p2p'
|
||||||
// }
|
// }
|
||||||
return 'vendor'
|
// return chunkid
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
},
|
// },
|
||||||
external: ['@aws-sdk/clients/s3']
|
external: ['@aws-sdk/clients/s3']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -50,7 +50,7 @@ Solid -> Store: loadArticleComments
|
||||||
activate Store
|
activate Store
|
||||||
Store -> apiClient: getArticleComments
|
Store -> apiClient: getArticleComments
|
||||||
activate apiClient
|
activate apiClient
|
||||||
apiClient -> DB: query: articleReactions
|
apiClient -> DB: query: getReactionsForShouts
|
||||||
activate DB
|
activate DB
|
||||||
DB --> apiClient: response
|
DB --> apiClient: response
|
||||||
deactivate DB
|
deactivate DB
|
||||||
|
|
70
package.json
70
package.json
|
@ -18,8 +18,8 @@
|
||||||
"lint:styles": "stylelint **/*.{scss,css}",
|
"lint:styles": "stylelint **/*.{scss,css}",
|
||||||
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
"lint:styles:fix": "stylelint **/*.{scss,css} --fix",
|
||||||
"pre-commit": "lint-staged",
|
"pre-commit": "lint-staged",
|
||||||
"pre-push": "",
|
|
||||||
"pre-commit-old": "lint-staged",
|
"pre-commit-old": "lint-staged",
|
||||||
|
"pre-push": "",
|
||||||
"pre-push-old": "npm run typecheck",
|
"pre-push-old": "npm run typecheck",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
|
@ -31,23 +31,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.178.0",
|
"@aws-sdk/client-s3": "^3.178.0",
|
||||||
"@nanostores/persistent": "^0.7.0",
|
"mailgun.js": "^8.0.1"
|
||||||
"@nanostores/router": "^0.7.0",
|
|
||||||
"@nanostores/solid": "^0.3.0",
|
|
||||||
"@solid-primitives/memo": "^1.0.2",
|
|
||||||
"loglevel": "^1.8.0",
|
|
||||||
"loglevel-plugin-prefix": "^0.8.4",
|
|
||||||
"mailgun.js": "^8.0.1",
|
|
||||||
"markdown-it": "^13.0.1",
|
|
||||||
"markdown-it-container": "^3.0.0",
|
|
||||||
"markdown-it-implicit-figures": "^0.10.0",
|
|
||||||
"markdown-it-mark": "^3.0.1",
|
|
||||||
"markdown-it-replace-link": "^1.1.0",
|
|
||||||
"nanostores": "^0.7.0",
|
|
||||||
"postcss-modules": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/language-server": "^0.27.0",
|
|
||||||
"@astrojs/solid-js": "^1.1.0",
|
"@astrojs/solid-js": "^1.1.0",
|
||||||
"@astrojs/vercel": "^2.1.0",
|
"@astrojs/vercel": "^2.1.0",
|
||||||
"@babel/core": "^7.18.13",
|
"@babel/core": "^7.18.13",
|
||||||
|
@ -58,9 +44,13 @@
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||||
"@graphql-tools/url-loader": "^7.16.4",
|
"@graphql-tools/url-loader": "^7.16.4",
|
||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
|
"@nanostores/persistent": "^0.7.0",
|
||||||
|
"@nanostores/router": "^0.7.0",
|
||||||
|
"@nanostores/solid": "^0.3.0",
|
||||||
"@popperjs/core": "^2.11.6",
|
"@popperjs/core": "^2.11.6",
|
||||||
"@solid-devtools/debugger": "^0.9.1",
|
"@solid-devtools/debugger": "^0.13.1",
|
||||||
"@solid-devtools/logger": "^0.4.7",
|
"@solid-devtools/logger": "^0.4.9",
|
||||||
|
"@solid-primitives/memo": "^1.0.2",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/node": "^18.7.19",
|
"@types/node": "^18.7.19",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
@ -71,33 +61,41 @@
|
||||||
"@urql/exchange-auth": "^1.0.0",
|
"@urql/exchange-auth": "^1.0.0",
|
||||||
"@urql/exchange-graphcache": "^5.0.0",
|
"@urql/exchange-graphcache": "^5.0.0",
|
||||||
"astro": "^1.1.1",
|
"astro": "^1.1.1",
|
||||||
"astro-eslint-parser": "^0.6.1",
|
"astro-eslint-parser": "^0.9.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bootstrap": "5.1.3",
|
"bootstrap": "5.1.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
"cookie-signature": "^1.2.0",
|
"cookie-signature": "^1.2.0",
|
||||||
"eslint": "8.22.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-stylelint": "^16.0.0",
|
"eslint-config-stylelint": "^17.0.0",
|
||||||
"eslint-import-resolver-typescript": "^3.5.0",
|
"eslint-import-resolver-typescript": "^3.5.0",
|
||||||
"eslint-mdx": "^2.0.2",
|
"eslint-plugin-astro": "^0.21.0",
|
||||||
"eslint-plugin-astro": "^0.19.0",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-mdx": "^2.0.2",
|
|
||||||
"eslint-plugin-promise": "^6.0.1",
|
"eslint-plugin-promise": "^6.0.1",
|
||||||
"eslint-plugin-solid": "^0.7.1",
|
"eslint-plugin-solid": "^0.7.3",
|
||||||
"eslint-plugin-sonarjs": "^0.15.0",
|
"eslint-plugin-sonarjs": "^0.16.0",
|
||||||
"eslint-plugin-unicorn": "^43.0.2",
|
"eslint-plugin-unicorn": "^44.0.2",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"graphql-ws": "^5.11.2",
|
"graphql-ws": "^5.11.2",
|
||||||
"hast-util-select": "^5.0.2",
|
"hast-util-select": "^5.0.2",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.1",
|
||||||
"idb": "^7.0.1",
|
"idb": "^7.1.0",
|
||||||
"jest": "^29.0.1",
|
"jest": "^29.2.1",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
|
"loglevel": "^1.8.0",
|
||||||
|
"loglevel-plugin-prefix": "^0.8.4",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-container": "^3.0.0",
|
||||||
|
"markdown-it-implicit-figures": "^0.10.0",
|
||||||
|
"markdown-it-mark": "^3.0.1",
|
||||||
|
"markdown-it-replace-link": "^1.1.0",
|
||||||
|
"nanostores": "^0.7.0",
|
||||||
|
"orderedmap": "^2.1.0",
|
||||||
"postcss": "^8.4.16",
|
"postcss": "^8.4.16",
|
||||||
|
"postcss-modules": "^5.0.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-eslint": "^15.0.1",
|
"prettier-eslint": "^15.0.1",
|
||||||
"prosemirror-commands": "^1.3.1",
|
"prosemirror-commands": "^1.3.1",
|
||||||
|
@ -114,15 +112,11 @@
|
||||||
"prosemirror-schema-list": "^1.2.2",
|
"prosemirror-schema-list": "^1.2.2",
|
||||||
"prosemirror-state": "^1.4.1",
|
"prosemirror-state": "^1.4.1",
|
||||||
"prosemirror-view": "^1.28.1",
|
"prosemirror-view": "^1.28.1",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
|
||||||
"rehype-slug": "^5.0.1",
|
|
||||||
"rehype-toc": "^3.0.2",
|
|
||||||
"remark-code-titles": "^0.1.2",
|
|
||||||
"rollup": "~2.79.1",
|
"rollup": "~2.79.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.2",
|
"rollup-plugin-visualizer": "^5.8.2",
|
||||||
"sass": "^1.55.0",
|
"sass": "^1.55.0",
|
||||||
"solid-devtools": "^0.16.2",
|
"solid-devtools": "^0.20.1",
|
||||||
"solid-js": "^1.5.6",
|
"solid-js": "^1.6.0",
|
||||||
"solid-js-form": "^0.1.5",
|
"solid-js-form": "^0.1.5",
|
||||||
"solid-jsx": "^0.9.1",
|
"solid-jsx": "^0.9.1",
|
||||||
"solid-social": "^0.9.0",
|
"solid-social": "^0.9.0",
|
||||||
|
@ -131,18 +125,18 @@
|
||||||
"stylelint": "^14.12.1",
|
"stylelint": "^14.12.1",
|
||||||
"stylelint-config-css-modules": "^4.1.0",
|
"stylelint-config-css-modules": "^4.1.0",
|
||||||
"stylelint-config-prettier-scss": "^0.0.1",
|
"stylelint-config-prettier-scss": "^0.0.1",
|
||||||
"stylelint-config-standard-scss": "^5.0.0",
|
"stylelint-config-standard-scss": "^6.0.0",
|
||||||
"stylelint-order": "^5.0.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"stylelint-scss": "^4.3.0",
|
"stylelint-scss": "^4.3.0",
|
||||||
"swiper": "^8.4.2",
|
"swiper": "^8.4.2",
|
||||||
"ts-debounce": "^4.0.0",
|
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.8.3",
|
"typescript": "^4.8.3",
|
||||||
"undici": "^5.10.0",
|
"undici": "^5.10.0",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite": "^3.1.3",
|
"vite": "^3.1.3",
|
||||||
"y-prosemirror": "^1.1.3",
|
"ws": "^8.9.0",
|
||||||
|
"y-prosemirror": "^1.2.0",
|
||||||
"y-protocols": "^1.0.5",
|
"y-protocols": "^1.0.5",
|
||||||
"y-webrtc": "^10.2.3",
|
"y-webrtc": "^10.2.3",
|
||||||
"yjs": "^13.5.41"
|
"yjs": "^13.5.41"
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M2 1H18V-1H2V1ZM19 2V18H21V2H19ZM18 19H2V21H18V19ZM1 18V2H-1V18H1ZM2 19C1.44772 19 1 18.5523 1 18H-1C-1 19.6569 0.343147 21 2 21V19ZM19 18C19 18.5523 18.5523 19 18 19V21C19.6569 21 21 19.6569 21 18H19ZM18 1C18.5523 1 19 1.44772 19 2H21C21 0.343146 19.6569 -1 18 -1V1ZM2 -1C0.343146 -1 -1 0.343147 -1 2H1C1 1.44772 1.44772 1 2 1V-1Z" fill="#141414"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 462 B |
3
src/assets/handle.svg
Normal file
3
src/assets/handle.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg viewBox="0 0 10 10" height="14" width="14">
|
||||||
|
<path d="M3 2a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm4-8a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2zm0 4a1 1 0 110-2 1 1 0 010 2z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 245 B |
|
@ -1,10 +1,9 @@
|
||||||
import './Comment.scss'
|
import './Comment.scss'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { Show } from 'solid-js/web'
|
import { Show, createMemo } from 'solid-js'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import type { Author, Reaction as Point } from '../../graphql/types.gen'
|
import type { Author, Reaction as Point } from '../../graphql/types.gen'
|
||||||
import { createMemo } from 'solid-js'
|
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
|
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import './Tooltip.scss'
|
import './Tooltip.scss'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, JSX } from 'solid-js'
|
||||||
|
|
||||||
export const Tooltip: (p: any) => any = (props: any) => {
|
interface TooltipProps {
|
||||||
|
children?: JSX.Element
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = (props: TooltipProps) => {
|
||||||
const [isShown, setShowed] = createSignal(false)
|
const [isShown, setShowed] = createSignal(false)
|
||||||
const show = () => setShowed(true)
|
const show = () => setShowed(true)
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { For, Show } from 'solid-js/web'
|
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import Userpic from './Userpic'
|
import Userpic from './Userpic'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import style from './Card.module.scss'
|
import style from './Card.module.scss'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Show } from 'solid-js'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import style from './Userpic.module.scss'
|
import style from './Userpic.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
|
@ -1,29 +1,33 @@
|
||||||
|
import '../../styles/help.scss'
|
||||||
import { createSignal, onMount } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
import { showModal, warn } from '../../stores/ui'
|
import { showModal, warn } from '../../stores/ui'
|
||||||
import '../../styles/help.scss'
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
export const Donate = () => {
|
export const Donate = () => {
|
||||||
const once = ''
|
const once = ''
|
||||||
const monthly = 'Monthly'
|
const monthly = 'Monthly'
|
||||||
const cpOptions = {
|
const cpOptions = {
|
||||||
publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed',
|
publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed',
|
||||||
description: 'Поддержка журнала и развитие Дискурса',
|
description: t('Help discours to grow'),
|
||||||
currency: 'RUB'
|
currency: 'RUB'
|
||||||
}
|
}
|
||||||
|
|
||||||
let amountSwitchElement: HTMLDivElement | undefined
|
let amountSwitchElement: HTMLDivElement | undefined
|
||||||
let customAmountElement: HTMLInputElement | undefined
|
let customAmountElement: HTMLInputElement | undefined
|
||||||
let CustomerReciept: any
|
const [widget, setWidget] = createSignal()
|
||||||
let widget: any
|
const [customerReciept, setCustomerReciept] = createSignal({})
|
||||||
|
|
||||||
const [showingPayment, setShowingPayment] = createSignal<boolean>()
|
const [showingPayment, setShowingPayment] = createSignal<boolean>()
|
||||||
const [period, setPeriod] = createSignal(monthly)
|
const [period, setPeriod] = createSignal(monthly)
|
||||||
const [amount, setAmount] = createSignal(0)
|
const [amount, setAmount] = createSignal(0)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
widget = new (window as any).cp.CloudPayments() // Checkout(cpOptions)
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const {
|
||||||
|
cp: { CloudPayments }
|
||||||
|
} = window as any // Checkout(cpOptions)
|
||||||
|
setWidget(new CloudPayments())
|
||||||
console.log('[donate] payments initiated')
|
console.log('[donate] payments initiated')
|
||||||
CustomerReciept = {
|
setCustomerReciept({
|
||||||
Items: [
|
Items: [
|
||||||
//товарные позиции
|
//товарные позиции
|
||||||
{
|
{
|
||||||
|
@ -46,7 +50,7 @@ export const Donate = () => {
|
||||||
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
|
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
|
||||||
provision: 0 // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
|
provision: 0 // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const show = () => {
|
const show = () => {
|
||||||
|
@ -57,7 +61,8 @@ export const Donate = () => {
|
||||||
amountSwitchElement?.querySelector('input[type=radio]:checked')
|
amountSwitchElement?.querySelector('input[type=radio]:checked')
|
||||||
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
|
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
|
||||||
console.log('[donate] input amount ' + amount)
|
console.log('[donate] input amount ' + amount)
|
||||||
widget.charge(
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
;(widget() as any).charge(
|
||||||
{
|
{
|
||||||
// options
|
// options
|
||||||
...cpOptions,
|
...cpOptions,
|
||||||
|
@ -69,22 +74,22 @@ export const Donate = () => {
|
||||||
// accountId: 'user@example.com', //идентификатор плательщика (обязательно для создания подписки)
|
// accountId: 'user@example.com', //идентификатор плательщика (обязательно для создания подписки)
|
||||||
data: {
|
data: {
|
||||||
CloudPayments: {
|
CloudPayments: {
|
||||||
CustomerReciept,
|
CustomerReciept: customerReciept(),
|
||||||
recurrent: {
|
recurrent: {
|
||||||
interval: period(), // local solid's signal
|
interval: period(), // local solid's signal
|
||||||
period: 1, // internal widget's
|
period: 1, // internal widget's
|
||||||
CustomerReciept // чек для регулярных платежей
|
CustomerReciept: customerReciept() // чек для регулярных платежей
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(opts: any) => {
|
(opts) => {
|
||||||
// success
|
// success
|
||||||
// действие при успешной оплате
|
// действие при успешной оплате
|
||||||
console.debug('[donate] options', opts)
|
console.debug('[donate] options', opts)
|
||||||
showModal('thank')
|
showModal('thank')
|
||||||
},
|
},
|
||||||
function (reason: string, options: any) {
|
function (reason: string, options) {
|
||||||
// fail
|
// fail
|
||||||
// действие при неуспешной оплате
|
// действие при неуспешной оплате
|
||||||
console.debug('[donate] options', options)
|
console.debug('[donate] options', options)
|
||||||
|
@ -124,7 +129,7 @@ export const Donate = () => {
|
||||||
ref={customAmountElement}
|
ref={customAmountElement}
|
||||||
type="number"
|
type="number"
|
||||||
name="sum"
|
name="sum"
|
||||||
placeholder="Другая сумма"
|
placeholder={t('Another amount')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,7 +145,7 @@ export const Donate = () => {
|
||||||
checked={period() === once}
|
checked={period() === once}
|
||||||
/>
|
/>
|
||||||
<label for="once" class="btn payment-type" classList={{ active: period() === once }}>
|
<label for="once" class="btn payment-type" classList={{ active: period() === once }}>
|
||||||
Единоразово
|
{t('One time')}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
@ -151,14 +156,14 @@ export const Donate = () => {
|
||||||
checked={period() === monthly}
|
checked={period() === monthly}
|
||||||
/>
|
/>
|
||||||
<label for="monthly" class="btn payment-type" classList={{ active: period() === monthly }}>
|
<label for="monthly" class="btn payment-type" classList={{ active: period() === monthly }}>
|
||||||
Ежемесячно
|
{t('Every month')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<a href={''} class="btn send-btn donate" onClick={show}>
|
<a href={''} class="btn send-btn donate" onClick={show}>
|
||||||
Помочь журналу
|
{t('Help discours to grow')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import './ArticlesList.scss'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return (
|
|
||||||
<div class="articles-list">
|
|
||||||
<div class="articles-list__item article row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="article__status article__status--draft">Черновик</div>
|
|
||||||
<div class="article__title">
|
|
||||||
<strong>Поствыживание. Комплекс вины и кризис самооценки в дивном новом мире.</strong>{' '}
|
|
||||||
В летописи российского музыкального подполья остаётся множество лакун.
|
|
||||||
</div>
|
|
||||||
<time class="article__date">21 марта 2022</time>
|
|
||||||
</div>
|
|
||||||
<div class="article__controls col-md-5 offset-md-1">
|
|
||||||
<div class="article-control">Редактировать</div>
|
|
||||||
<div class="article-control">Опубликовать</div>
|
|
||||||
<div class="article-control article-control--remove">Удалить</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
.error {
|
|
||||||
button {
|
|
||||||
height: 50px;
|
|
||||||
padding: 0 20px;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
font-family: Muller;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
background: none;
|
|
||||||
color: var(--foreground);
|
|
||||||
border: 1px solid var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary {
|
|
||||||
color: var(--primary-foreground);
|
|
||||||
border: 0;
|
|
||||||
background: var(--primary-background);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
import type { Config } from './store'
|
|
||||||
import './Layout.scss'
|
|
||||||
|
|
||||||
export type Styled = {
|
|
||||||
children: any
|
|
||||||
config?: Config
|
|
||||||
'data-testid'?: string
|
|
||||||
onClick?: () => void
|
|
||||||
onMouseEnter?: (e: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Layout = (props: Styled) => {
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
<div onMouseEnter={props.onMouseEnter} class="layout layout--editor" data-testid={props['data-testid']}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,266 +0,0 @@
|
||||||
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
|
||||||
import { unwrap } from 'solid-js/store'
|
|
||||||
// import { undo, redo } from 'prosemirror-history'
|
|
||||||
import { File, useState /*, Config, PrettierConfig */ } from './store'
|
|
||||||
import { clsx } from 'clsx'
|
|
||||||
import type { Styled } from './Layout'
|
|
||||||
// import type { EditorState } from 'prosemirror-state'
|
|
||||||
// import { serialize } from './prosemirror/markdown'
|
|
||||||
// import { baseUrl } from '../../graphql/client'
|
|
||||||
// import { isServer } from 'solid-js/web'
|
|
||||||
|
|
||||||
// const copy = async (text: string): Promise<void> => navigator.clipboard.writeText(text)
|
|
||||||
// const copyAllAsMarkdown = async (state: EditorState): Promise<void> =>
|
|
||||||
// !isServer && navigator.clipboard.writeText(serialize(state))
|
|
||||||
|
|
||||||
const Off = (props: any) => <div class="sidebar-off">{props.children}</div>
|
|
||||||
|
|
||||||
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
|
|
||||||
|
|
||||||
const Link = (
|
|
||||||
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
|
||||||
) => (
|
|
||||||
<button
|
|
||||||
class={clsx('sidebar-link', props.className)}
|
|
||||||
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
|
|
||||||
onClick={props.onClick}
|
|
||||||
disabled={props.disabled}
|
|
||||||
title={props.title}
|
|
||||||
data-testid={props['data-testid']}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileLinkProps = {
|
|
||||||
file: File
|
|
||||||
onOpenFile: (file: File) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileLink = (props: FileLinkProps) => {
|
|
||||||
const length = 100
|
|
||||||
let content = ''
|
|
||||||
const getContent = (node: any) => {
|
|
||||||
if (node.text) {
|
|
||||||
content += node.text
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content.length > length) {
|
|
||||||
content = `${content.slice(0, Math.max(0, length))}...`
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.content) {
|
|
||||||
for (const child of node.content) {
|
|
||||||
if (content.length >= length) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
content = getContent(child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = () =>
|
|
||||||
props.file.path
|
|
||||||
? props.file.path.slice(Math.max(0, props.file.path.length - length))
|
|
||||||
: getContent(props.file.text?.doc)
|
|
||||||
|
|
||||||
return (
|
|
||||||
// eslint-disable-next-line solid/no-react-specific-props
|
|
||||||
<Link className="file" onClick={() => props.onOpenFile(props.file)} data-testid="open">
|
|
||||||
{text()} {props.file.path && '📎'}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Sidebar = () => {
|
|
||||||
const [store, ctrl] = useState()
|
|
||||||
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
|
||||||
const toggleTheme = () => {
|
|
||||||
document.body.classList.toggle('dark')
|
|
||||||
ctrl.updateConfig({ theme: document.body.className })
|
|
||||||
}
|
|
||||||
|
|
||||||
// const collabText = () => (store.collab?.started ? 'Stop' : store.collab?.error ? 'Restart 🚨' : 'Start')
|
|
||||||
const editorView = () => unwrap(store.editorView)
|
|
||||||
// const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
|
||||||
const onOpenFile = (file: File) => ctrl.openFile(unwrap(file))
|
|
||||||
// const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
|
||||||
// const onUndo = () => undo(editorView().state, editorView().dispatch)
|
|
||||||
// const onRedo = () => redo(editorView().state, editorView().dispatch)
|
|
||||||
// const onCopyAllAsMd = () => copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
|
||||||
// const onToggleAlwaysOnTop = () => ctrl.updateConfig({ alwaysOnTop: !store.config.alwaysOnTop })
|
|
||||||
// const onToggleFullscreen = () => ctrl.setFullscreen(!store.fullscreen)
|
|
||||||
// const onNew = () => ctrl.newFile()
|
|
||||||
// const onDiscard = () => ctrl.discard()
|
|
||||||
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
setIsHidden(!isHidden())
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSidebar()
|
|
||||||
|
|
||||||
// const onSaveAs = async () => {
|
|
||||||
// const path = 'test' // TODO: save filename await remote.save(editorView().state)
|
|
||||||
//
|
|
||||||
// if (path) ctrl.updatePath(path)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const onCollab = () => {
|
|
||||||
// const state = unwrap(store)
|
|
||||||
//
|
|
||||||
// store.collab?.started ? ctrl.stopCollab(state) : console.log(state)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const onOpenInApp = () => {
|
|
||||||
// // if (isTauri) return
|
|
||||||
//
|
|
||||||
// if (store.collab?.started) {
|
|
||||||
// window.open(`discoursio://main?room=${store.collab?.room}`, '_self')
|
|
||||||
// } else {
|
|
||||||
// const text = window.btoa(JSON.stringify(editorView().state.toJSON()))
|
|
||||||
//
|
|
||||||
// window.open(`discoursio://main?text=${text}`, '_self')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const onCopyCollabLink = () => {
|
|
||||||
// copy(`${baseUrl}/collab/${store.collab?.room}`).then(() => {
|
|
||||||
// editorView().focus()
|
|
||||||
// setLastAction('copy-collab-link')
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const onCopyCollabAppLink = () => {
|
|
||||||
// copy(`discoursio://${store.collab?.room}`).then(() => {
|
|
||||||
// editorView().focus()
|
|
||||||
// setLastAction('copy-collab-app-link')
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const Keys = (props: { keys: string[] }) => (
|
|
||||||
// <span>
|
|
||||||
// <For each={props.keys}>{(k: string) => <i>{k}</i>}</For>
|
|
||||||
// </span>
|
|
||||||
// )
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setLastAction()
|
|
||||||
}, store.lastModified)
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!lastAction()) return
|
|
||||||
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
setLastAction()
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
onCleanup(() => clearTimeout(id))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={`sidebar-container${isHidden() ? ' sidebar-container--hidden' : ''}`}>
|
|
||||||
<span class="sidebar-opener" onClick={toggleSidebar}>
|
|
||||||
Советы и предложения
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Off onClick={() => editorView().focus()} data-tauri-drag-region="true">
|
|
||||||
<div class="sidebar-closer" onClick={toggleSidebar} />
|
|
||||||
<Show when={true}>
|
|
||||||
<div>
|
|
||||||
<Show when={store.path}>
|
|
||||||
<Label>
|
|
||||||
<i>({store.path?.slice(Math.max(0, store.path?.length - 24))})</i>
|
|
||||||
</Label>
|
|
||||||
</Show>
|
|
||||||
<Link>Пригласить соавторов</Link>
|
|
||||||
<Link>Настройки публикации</Link>
|
|
||||||
<Link>История правок</Link>
|
|
||||||
|
|
||||||
<div class="theme-switcher">
|
|
||||||
Ночная тема
|
|
||||||
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
|
||||||
<label for="theme">Ночная тема</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
<Show when={isTauri && !store.path}>
|
|
||||||
<Link onClick={onSaveAs}>
|
|
||||||
Save to file <Keys keys={[mod, 's']} />
|
|
||||||
</Link>
|
|
||||||
</Show>
|
|
||||||
<Link onClick={onNew} data-testid='new'>
|
|
||||||
New <Keys keys={[mod, 'n']} />
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
onClick={onDiscard}
|
|
||||||
disabled={!store.path && store.files?.length === 0 && isEmpty(store.text)}
|
|
||||||
data-testid='discard'
|
|
||||||
>
|
|
||||||
{store.path ? 'Close' : store.files?.length > 0 && isEmpty(store.text) ? 'Delete ⚠️' : 'Clear'}{' '}
|
|
||||||
<Keys keys={[mod, 'w']} />
|
|
||||||
</Link>
|
|
||||||
<Show when={isTauri}>
|
|
||||||
<Link onClick={onToggleFullscreen}>
|
|
||||||
Fullscreen {store.fullscreen && '✅'} <Keys keys={[alt, 'Enter']} />
|
|
||||||
</Link>
|
|
||||||
</Show>
|
|
||||||
<Link onClick={onUndo}>
|
|
||||||
Undo <Keys keys={[mod, 'z']} />
|
|
||||||
</Link>
|
|
||||||
<Link onClick={onRedo}>
|
|
||||||
Redo <Keys keys={[mod, ...(isMac ? ['Shift', 'z'] : ['y'])]} />
|
|
||||||
</Link>
|
|
||||||
<Show when={isTauri}>
|
|
||||||
<Link onClick={onToggleAlwaysOnTop}>Always on Top {store.config.alwaysOnTop && '✅'}</Link>
|
|
||||||
</Show>
|
|
||||||
<Show when={!isTauri && false}>
|
|
||||||
<Link onClick={onOpenInApp}>Open in App ⚡</Link>
|
|
||||||
</Show>
|
|
||||||
<Link onClick={onToggleMarkdown} data-testid='markdown'>
|
|
||||||
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
|
||||||
</Link>
|
|
||||||
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
|
||||||
*/}
|
|
||||||
<Show when={store.files?.length > 0}>
|
|
||||||
<h4>Files:</h4>
|
|
||||||
<p>
|
|
||||||
<For each={store.files}>{(file) => <FileLink file={file} onOpenFile={onOpenFile} />}</For>
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
|
||||||
Collab {collabText()}
|
|
||||||
</Link>
|
|
||||||
<Show when={collabUsers() > 0}>
|
|
||||||
<Link onClick={onCopyCollabLink}>
|
|
||||||
Copy Link {lastAction() === 'copy-collab-link' && '📋'}
|
|
||||||
</Link>
|
|
||||||
<Show when={false}>
|
|
||||||
<Link onClick={onCopyCollabAppLink}>
|
|
||||||
Copy App Link {lastAction() === 'copy-collab-app-link' && '📋'}
|
|
||||||
</Link>
|
|
||||||
</Show>
|
|
||||||
<span>
|
|
||||||
{collabUsers()} {collabUsers() === 1 ? 'user' : 'users'} connected
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={isTauri}>
|
|
||||||
<Link onClick={() => remote.quit()}>
|
|
||||||
Quit <Keys keys={[mod, 'q']} />
|
|
||||||
</Link>
|
|
||||||
</Show>
|
|
||||||
*/}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Off>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
7
src/components/Editor/components/Editor.module.scss
Normal file
7
src/components/Editor/components/Editor.module.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.error {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
|
@ -1,31 +1,23 @@
|
||||||
import './Editor.scss'
|
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import type { EditorState } from 'prosemirror-state'
|
||||||
import { useState } from './store'
|
import { useState } from '../store/context'
|
||||||
import { ProseMirror } from './prosemirror'
|
import { ProseMirror } from './ProseMirror'
|
||||||
|
import '../styles/Editor.scss'
|
||||||
|
import styles from './Editor.module.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
export default () => {
|
export const Editor = () => {
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
const onInit = (text: EditorState, editorView: EditorView) => ctrl.setState({ editorView, text })
|
||||||
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
const onReconfigure = (text: EditorState) => ctrl.setState({ text })
|
||||||
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
const onChange = (text: EditorState) => ctrl.setState({ text, lastModified: new Date() })
|
||||||
// const editorCss = (config) => css``
|
|
||||||
const style = () => {
|
|
||||||
if (store.error) {
|
|
||||||
return `display: none;`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.markdown) {
|
|
||||||
return `white-space: pre-wrap;`
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProseMirror
|
<ProseMirror
|
||||||
class="editor"
|
cssClass={clsx('editor', 'col-md-6', 'shift-content', {
|
||||||
style={style()}
|
[styles.error]: store.error,
|
||||||
|
[styles.markdown]: store.markdown
|
||||||
|
})}
|
||||||
editorView={store.editorView}
|
editorView={store.editorView}
|
||||||
text={store.text}
|
text={store.text}
|
||||||
extensions={store.extensions}
|
extensions={store.extensions}
|
|
@ -1,12 +1,30 @@
|
||||||
import { Switch, Match, createMemo } from 'solid-js'
|
import { Switch, Match } from 'solid-js'
|
||||||
import { ErrorObject, useState } from './store'
|
import { useState } from '../store/context'
|
||||||
|
import '../styles/Button.scss'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const [store] = useState()
|
||||||
|
return (
|
||||||
|
<Switch fallback={<Other />}>
|
||||||
|
<Match when={store.error.id === 'invalid_state'}>
|
||||||
|
<InvalidState title="Invalid State" />
|
||||||
|
</Match>
|
||||||
|
<Match when={store.error.id === 'invalid_config'}>
|
||||||
|
<InvalidState title="Invalid Config" />
|
||||||
|
</Match>
|
||||||
|
<Match when={store.error.id === 'invalid_draft'}>
|
||||||
|
<InvalidState title="Invalid Draft" />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const InvalidState = (props: { title: string }) => {
|
const InvalidState = (props: { title: string }) => {
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const onClick = () => ctrl.clean()
|
const onClick = () => ctrl.clean()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error" data-tauri-drag-region="true">
|
<div class="error">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
<p>
|
<p>
|
||||||
|
@ -15,7 +33,7 @@ const InvalidState = (props: { title: string }) => {
|
||||||
you can copy important notes from below, clean the state and paste it again.
|
you can copy important notes from below, clean the state and paste it again.
|
||||||
</p>
|
</p>
|
||||||
<pre>
|
<pre>
|
||||||
<code>{JSON.stringify(store.error)}</code>
|
<code>{JSON.stringify(store.error.props)}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<button class="primary" onClick={onClick}>
|
<button class="primary" onClick={onClick}>
|
||||||
Clean
|
Clean
|
||||||
|
@ -28,10 +46,14 @@ const InvalidState = (props: { title: string }) => {
|
||||||
const Other = () => {
|
const Other = () => {
|
||||||
const [store, ctrl] = useState()
|
const [store, ctrl] = useState()
|
||||||
const onClick = () => ctrl.discard()
|
const onClick = () => ctrl.discard()
|
||||||
const getMessage = createMemo<ErrorObject['message']>(() => store.error.message)
|
|
||||||
|
const getMessage = () => {
|
||||||
|
const err = (store.error.props as any).error
|
||||||
|
return typeof err === 'string' ? err : err.message
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="error" data-tauri-drag-region="true">
|
<div class="error">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>An error occurred.</h1>
|
<h1>An error occurred.</h1>
|
||||||
<pre>
|
<pre>
|
||||||
|
@ -44,21 +66,3 @@ const Other = () => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const [store] = useState()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch fallback={<Other />}>
|
|
||||||
<Match when={store.error?.id === 'invalid_state'}>
|
|
||||||
<InvalidState title="Invalid State" />
|
|
||||||
</Match>
|
|
||||||
<Match when={store.error?.id === 'invalid_config'}>
|
|
||||||
<InvalidState title="Invalid Config" />
|
|
||||||
</Match>
|
|
||||||
<Match when={store.error?.id === 'invalid_file'}>
|
|
||||||
<InvalidState title="Invalid File" />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
}
|
|
19
src/components/Editor/components/Layout.tsx
Normal file
19
src/components/Editor/components/Layout.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
import type { Config } from '../store/context'
|
||||||
|
import '../styles/Layout.scss'
|
||||||
|
|
||||||
|
export type Styled = {
|
||||||
|
children: JSX.Element
|
||||||
|
config?: Config
|
||||||
|
'data-testid'?: string
|
||||||
|
onClick?: () => void
|
||||||
|
onMouseEnter?: (e: MouseEvent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Layout = (props: Styled) => {
|
||||||
|
return (
|
||||||
|
<div onMouseEnter={props.onMouseEnter} class="layout container" data-testid={props['data-testid']}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
import { createEffect, untrack } from 'solid-js'
|
import { createEffect, untrack } from 'solid-js'
|
||||||
import { Store, unwrap } from 'solid-js/store'
|
import { Store, unwrap } from 'solid-js/store'
|
||||||
import { EditorState, Plugin, Transaction } from 'prosemirror-state'
|
import { EditorState, EditorStateConfig, Transaction } from 'prosemirror-state'
|
||||||
import { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import { Schema } from 'prosemirror-model'
|
import { Schema } from 'prosemirror-model'
|
||||||
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from './state'
|
import type { NodeViewFn, ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
|
|
||||||
interface Props {
|
interface ProseMirrorProps {
|
||||||
style?: string
|
cssClass?: string
|
||||||
class?: string
|
|
||||||
text?: Store<ProseMirrorState>
|
text?: Store<ProseMirrorState>
|
||||||
editorView?: Store<EditorView>
|
editorView?: Store<EditorView>
|
||||||
extensions?: Store<ProseMirrorExtension[]>
|
extensions?: Store<ProseMirrorExtension[]>
|
||||||
|
@ -16,6 +15,53 @@ interface Props {
|
||||||
onChange: (s: EditorState) => void
|
onChange: (s: EditorState) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ProseMirror = (props: ProseMirrorProps) => {
|
||||||
|
let editorRef: HTMLDivElement
|
||||||
|
const editorView = () => untrack(() => unwrap(props.editorView))
|
||||||
|
|
||||||
|
const dispatchTransaction = (tr: Transaction) => {
|
||||||
|
if (!editorView()) return
|
||||||
|
const newState = editorView().state.apply(tr)
|
||||||
|
editorView().updateState(newState)
|
||||||
|
if (!tr.docChanged) return
|
||||||
|
props.onChange(newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
(payload: [EditorState, ProseMirrorExtension[]]) => {
|
||||||
|
const [prevText, prevExtensions] = payload
|
||||||
|
const text = unwrap(props.text)
|
||||||
|
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
||||||
|
if (!text || !extensions?.length) {
|
||||||
|
return [text, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.editorView) {
|
||||||
|
const { editorState, nodeViews } = createEditorState(text, extensions)
|
||||||
|
const view = new EditorView(editorRef, { state: editorState, nodeViews, dispatchTransaction })
|
||||||
|
view.focus()
|
||||||
|
props.onInit(editorState, view)
|
||||||
|
return [editorState, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
||||||
|
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
||||||
|
if (!editorState) return
|
||||||
|
editorView().updateState(editorState)
|
||||||
|
editorView().setProps({ nodeViews, dispatchTransaction })
|
||||||
|
props.onReconfigure(editorState)
|
||||||
|
editorView().focus()
|
||||||
|
return [editorState, extensions]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [text, extensions]
|
||||||
|
},
|
||||||
|
[props.text, props.extensions]
|
||||||
|
)
|
||||||
|
|
||||||
|
return <div ref={editorRef} class={props.cssClass} spell-check={false} />
|
||||||
|
}
|
||||||
|
|
||||||
const createEditorState = (
|
const createEditorState = (
|
||||||
text: ProseMirrorState,
|
text: ProseMirrorState,
|
||||||
extensions: ProseMirrorExtension[],
|
extensions: ProseMirrorExtension[],
|
||||||
|
@ -27,7 +73,7 @@ const createEditorState = (
|
||||||
const reconfigure = text instanceof EditorState && prevText?.schema
|
const reconfigure = text instanceof EditorState && prevText?.schema
|
||||||
let schemaSpec = { nodes: {} }
|
let schemaSpec = { nodes: {} }
|
||||||
let nodeViews = {}
|
let nodeViews = {}
|
||||||
let plugins: Plugin<any>[] = []
|
let plugins = []
|
||||||
|
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
if (extension.schema) {
|
if (extension.schema) {
|
||||||
|
@ -40,72 +86,21 @@ const createEditorState = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = reconfigure ? prevText.schema : new Schema(schemaSpec)
|
const schema = reconfigure ? prevText.schema : new Schema(schemaSpec)
|
||||||
|
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
if (extension.plugins) {
|
if (extension.plugins) {
|
||||||
plugins = extension.plugins(plugins, schema)
|
plugins = extension.plugins(plugins, schema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorState: EditorState = reconfigure
|
let editorState: EditorState
|
||||||
? text.reconfigure({ plugins })
|
if (reconfigure) {
|
||||||
: EditorState.fromJSON({ schema, plugins }, text as { [key: string]: any })
|
editorState = text.reconfigure({ schema, plugins } as EditorStateConfig)
|
||||||
|
} else if (text instanceof EditorState) {
|
||||||
|
editorState = EditorState.fromJSON({ schema, plugins }, text.toJSON())
|
||||||
|
} else if (text) {
|
||||||
|
console.debug(text)
|
||||||
|
editorState = EditorState.fromJSON({ schema, plugins }, text)
|
||||||
|
}
|
||||||
|
|
||||||
return { editorState, nodeViews }
|
return { editorState, nodeViews }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProseMirror = (props: Props) => {
|
|
||||||
let editorRef: HTMLDivElement
|
|
||||||
const editorView = () => untrack(() => unwrap(props.editorView))
|
|
||||||
|
|
||||||
const dispatchTransaction = (tr: Transaction) => {
|
|
||||||
if (!editorView()) return
|
|
||||||
const newState = editorView().state.apply(tr)
|
|
||||||
editorView().updateState(newState)
|
|
||||||
if (!tr.docChanged) return
|
|
||||||
props.onChange(newState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
createEffect(
|
|
||||||
(state: [EditorState, ProseMirrorExtension[]]) => {
|
|
||||||
const [prevText, prevExtensions] = state
|
|
||||||
const text = unwrap(props.text) as EditorState
|
|
||||||
const extensions: ProseMirrorExtension[] = unwrap(props.extensions)
|
|
||||||
|
|
||||||
if (!text || !extensions?.length) return [text, extensions]
|
|
||||||
|
|
||||||
if (!props.editorView) {
|
|
||||||
const { editorState, nodeViews } = createEditorState(text, extensions)
|
|
||||||
const view = new EditorView(editorRef, {
|
|
||||||
state: editorState,
|
|
||||||
nodeViews,
|
|
||||||
dispatchTransaction
|
|
||||||
})
|
|
||||||
|
|
||||||
view.focus()
|
|
||||||
props.onInit(editorState, view)
|
|
||||||
|
|
||||||
return [editorState, extensions]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extensions !== prevExtensions || (!(text instanceof EditorState) && text !== prevText)) {
|
|
||||||
const { editorState, nodeViews } = createEditorState(text, extensions, prevText)
|
|
||||||
|
|
||||||
if (!editorState) return
|
|
||||||
|
|
||||||
editorView().updateState(editorState)
|
|
||||||
editorView().setProps({ nodeViews, dispatchTransaction })
|
|
||||||
props.onReconfigure(editorState)
|
|
||||||
editorView().focus()
|
|
||||||
|
|
||||||
return [editorState, extensions]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [text, extensions]
|
|
||||||
},
|
|
||||||
[props.text, props.extensions]
|
|
||||||
)
|
|
||||||
|
|
||||||
return <div style={props.style} ref={editorRef} class={props.class} spell-check={false} />
|
|
||||||
}
|
|
3
src/components/Editor/components/Sidebar.module.scss
Normal file
3
src/components/Editor/components/Sidebar.module.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.withMargin {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
204
src/components/Editor/components/Sidebar.tsx
Normal file
204
src/components/Editor/components/Sidebar.tsx
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import { For, Show, createEffect, createSignal, onCleanup } from 'solid-js'
|
||||||
|
import { unwrap } from 'solid-js/store'
|
||||||
|
import { undo, redo } from 'prosemirror-history'
|
||||||
|
import { Draft, useState } from '../store/context'
|
||||||
|
import { mod } from '../env'
|
||||||
|
import * as remote from '../remote'
|
||||||
|
import { isEmpty } from '../prosemirror/helpers'
|
||||||
|
import type { Styled } from './Layout'
|
||||||
|
import '../styles/Sidebar.scss'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './Sidebar.module.scss'
|
||||||
|
|
||||||
|
const Off = (props) => <div class="sidebar-off">{props.children}</div>
|
||||||
|
|
||||||
|
const Label = (props: Styled) => <h3 class="sidebar-label">{props.children}</h3>
|
||||||
|
|
||||||
|
const Link = (
|
||||||
|
props: Styled & { withMargin?: boolean; disabled?: boolean; title?: string; className?: string }
|
||||||
|
) => (
|
||||||
|
<button
|
||||||
|
class={clsx('sidebar-link', props.className, {
|
||||||
|
[styles.withMargin]: props.withMargin
|
||||||
|
})}
|
||||||
|
onClick={props.onClick}
|
||||||
|
disabled={props.disabled}
|
||||||
|
title={props.title}
|
||||||
|
data-testid={props['data-testid']}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Sidebar = () => {
|
||||||
|
const [store, ctrl] = useState()
|
||||||
|
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||||
|
const toggleTheme = () => {
|
||||||
|
document.body.classList.toggle('dark')
|
||||||
|
ctrl.updateConfig({ theme: document.body.className })
|
||||||
|
}
|
||||||
|
const collabText = () => {
|
||||||
|
if (store.collab?.started) {
|
||||||
|
return 'Stop'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.collab?.error) {
|
||||||
|
return 'Restart 🚨'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Start'
|
||||||
|
}
|
||||||
|
|
||||||
|
const discardText = () => {
|
||||||
|
if (store.path) {
|
||||||
|
return 'Close'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.drafts.length > 0 && isEmpty(store.text)) {
|
||||||
|
return 'Delete ⚠️'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Clear'
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorView = () => unwrap(store.editorView)
|
||||||
|
const onToggleMarkdown = () => ctrl.toggleMarkdown()
|
||||||
|
const onOpenDraft = (draft: Draft) => ctrl.openDraft(unwrap(draft))
|
||||||
|
const collabUsers = () => store.collab?.y?.provider.awareness.meta.size ?? 0
|
||||||
|
const onUndo = () => undo(editorView().state, editorView().dispatch)
|
||||||
|
const onRedo = () => redo(editorView().state, editorView().dispatch)
|
||||||
|
const onCopyAllAsMd = () =>
|
||||||
|
remote.copyAllAsMarkdown(editorView().state).then(() => setLastAction('copy-md'))
|
||||||
|
const onDiscard = () => ctrl.discard()
|
||||||
|
const [isHidden, setIsHidden] = createSignal<boolean | false>()
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
setIsHidden(!isHidden())
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSidebar()
|
||||||
|
|
||||||
|
const onCollab = () => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
store.collab?.started ? ctrl.stopCollab(state) : ctrl.startCollab(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const DraftLink = (p: { draft: Draft }) => {
|
||||||
|
const length = 100
|
||||||
|
let content = ''
|
||||||
|
const getContent = (node: any) => {
|
||||||
|
if (node.text) {
|
||||||
|
content += node.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > length) {
|
||||||
|
content = content.slice(0, Math.max(0, length)) + '...'
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.content) {
|
||||||
|
for (const child of node.content) {
|
||||||
|
if (content.length >= length) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
content = getContent(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = () =>
|
||||||
|
p.draft.path
|
||||||
|
? p.draft.path.slice(Math.max(0, p.draft.path.length - length))
|
||||||
|
: getContent(p.draft.text?.doc)
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line solid/no-react-specific-props
|
||||||
|
<Link className="draft" onClick={() => onOpenDraft(p.draft)} data-testid="open">
|
||||||
|
{text()} {p.draft.path && '📎'}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Keys = (props) => (
|
||||||
|
<span>
|
||||||
|
<For each={props.keys}>{(k: Element) => <i>{k}</i>}</For>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
setLastAction()
|
||||||
|
}, store.lastModified)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!lastAction()) return
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
setLastAction()
|
||||||
|
}, 1000)
|
||||||
|
onCleanup(() => clearTimeout(id))
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'sidebar-container' + (isHidden() ? ' sidebar-container--hidden' : '')}>
|
||||||
|
<span class="sidebar-opener" onClick={toggleSidebar}>
|
||||||
|
Советы и предложения
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Off onClick={() => editorView().focus()}>
|
||||||
|
<div class="sidebar-closer" onClick={toggleSidebar} />
|
||||||
|
<Show when={true}>
|
||||||
|
<div>
|
||||||
|
{store.path && (
|
||||||
|
<Label>
|
||||||
|
<i>({store.path.slice(Math.max(0, store.path.length - 24))})</i>
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
<Link>Пригласить соавторов</Link>
|
||||||
|
<Link>Настройки публикации</Link>
|
||||||
|
<Link>История правок</Link>
|
||||||
|
|
||||||
|
<div class="theme-switcher">
|
||||||
|
Ночная тема
|
||||||
|
<input type="checkbox" name="theme" id="theme" onClick={toggleTheme} />
|
||||||
|
<label for="theme">Ночная тема</label>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
onClick={onDiscard}
|
||||||
|
disabled={!store.path && store.drafts.length === 0 && isEmpty(store.text)}
|
||||||
|
data-testid="discard"
|
||||||
|
>
|
||||||
|
{discardText()} <Keys keys={[mod, 'w']} />
|
||||||
|
</Link>
|
||||||
|
<Link onClick={onUndo}>
|
||||||
|
Undo <Keys keys={[mod, 'z']} />
|
||||||
|
</Link>
|
||||||
|
<Link onClick={onRedo}>
|
||||||
|
Redo <Keys keys={[mod, 'Shift', 'z']} />
|
||||||
|
</Link>
|
||||||
|
<Link onClick={onToggleMarkdown} data-testid="markdown">
|
||||||
|
Markdown mode {store.markdown && '✅'} <Keys keys={[mod, 'm']} />
|
||||||
|
</Link>
|
||||||
|
<Link onClick={onCopyAllAsMd}>Copy all as MD {lastAction() === 'copy-md' && '📋'}</Link>
|
||||||
|
<Show when={store.drafts.length > 0}>
|
||||||
|
<h4>Drafts:</h4>
|
||||||
|
<p>
|
||||||
|
<For each={store.drafts}>{(draft) => <DraftLink draft={draft} />}</For>
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<Link onClick={onCollab} title={store.collab?.error ? 'Connection error' : ''}>
|
||||||
|
Collab {collabText()}
|
||||||
|
</Link>
|
||||||
|
<Show when={collabUsers() > 0}>
|
||||||
|
<span>
|
||||||
|
{collabUsers()} {collabUsers() === 1 ? 'user' : 'users'} connected
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Off>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,30 +1,31 @@
|
||||||
import { openDB } from 'idb'
|
const dbPromise = async () => {
|
||||||
|
const { openDB } = await import('idb')
|
||||||
const dbPromise = openDB('discours.io', 2, {
|
return openDB('discours.io', 2, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
db.createObjectStore('keyval')
|
db.createObjectStore('keyval')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async get(key: string) {
|
async get(key: string) {
|
||||||
const result = await dbPromise
|
const result = await dbPromise()
|
||||||
return result.get('keyval', key)
|
return result.get('keyval', key)
|
||||||
},
|
},
|
||||||
async set(key: string, val: string) {
|
async set(key: string, val: string) {
|
||||||
const result = await dbPromise
|
const result = await dbPromise()
|
||||||
return result.put('keyval', val, key)
|
return result.put('keyval', val, key)
|
||||||
},
|
},
|
||||||
async delete(key: string) {
|
async delete(key: string) {
|
||||||
const result = await dbPromise
|
const result = await dbPromise()
|
||||||
return result.delete('keyval', key)
|
return result.delete('keyval', key)
|
||||||
},
|
},
|
||||||
async clear() {
|
async clear() {
|
||||||
const result = await dbPromise
|
const result = await dbPromise()
|
||||||
return result.clear('keyval')
|
return result.clear('keyval')
|
||||||
},
|
},
|
||||||
async keys() {
|
async keys() {
|
||||||
const result = await dbPromise
|
const result = await dbPromise()
|
||||||
return result.getAllKeys('keyval')
|
return result.getAllKeys('keyval')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
src/components/Editor/env.ts
Normal file
3
src/components/Editor/env.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const isDark = () => (window as any).matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
export const mod = 'Ctrl'
|
||||||
|
export const alt = 'Alt'
|
|
@ -1,18 +1,29 @@
|
||||||
import markdownit from 'markdown-it'
|
import markdownit from 'markdown-it'
|
||||||
import type Token from 'markdown-it/lib/token'
|
import {
|
||||||
import { MarkdownSerializer, MarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown'
|
MarkdownSerializer,
|
||||||
|
MarkdownParser,
|
||||||
|
defaultMarkdownSerializer,
|
||||||
|
MarkdownSerializerState
|
||||||
|
} from 'prosemirror-markdown'
|
||||||
import type { Node, Schema } from 'prosemirror-model'
|
import type { Node, Schema } from 'prosemirror-model'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import type { EditorState } from 'prosemirror-state'
|
||||||
|
|
||||||
|
export const serialize = (state: EditorState) => {
|
||||||
|
let text = markdownSerializer.serialize(state.doc)
|
||||||
|
if (text.charAt(text.length - 1) !== '\n') {
|
||||||
|
text += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
function findAlignment(cell: Node): string | null {
|
function findAlignment(cell: Node): string | null {
|
||||||
const alignment = cell.attrs.style as string
|
const alignment = cell.attrs.style as string
|
||||||
|
|
||||||
if (!alignment) {
|
if (!alignment) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = alignment.match(/text-align: ?(left|right|center)/)
|
const match = alignment.match(/text-align: ?(left|right|center)/)
|
||||||
|
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
return match[1]
|
return match[1]
|
||||||
}
|
}
|
||||||
|
@ -23,41 +34,34 @@ function findAlignment(cell: Node): string | null {
|
||||||
export const markdownSerializer = new MarkdownSerializer(
|
export const markdownSerializer = new MarkdownSerializer(
|
||||||
{
|
{
|
||||||
...defaultMarkdownSerializer.nodes,
|
...defaultMarkdownSerializer.nodes,
|
||||||
image(state, node) {
|
image(state: MarkdownSerializerState, node: Node) {
|
||||||
const alt = state.esc(node.attrs.alt || '')
|
const alt = state.esc(node.attrs.alt || '')
|
||||||
const src = node.attrs.path ?? node.attrs.src
|
const src = node.attrs.path ?? node.attrs.src
|
||||||
|
const title = node.attrs.title ? `"${node.attrs.title}"` : undefined
|
||||||
// FIXME !!!!!!!!!
|
state.write(`\n`)
|
||||||
// const title = node.attrs.title ? state.quote(node.attrs.title) : undefined
|
/*  */
|
||||||
const title = node.attrs.title
|
|
||||||
|
|
||||||
state.write(`\n`)
|
|
||||||
},
|
},
|
||||||
code_block(state, node) {
|
code_block(state, node) {
|
||||||
const src = node.attrs.params.src
|
const src = node.attrs.params.src
|
||||||
|
|
||||||
if (src) {
|
if (src) {
|
||||||
const title = state.esc(node.attrs.params.title || '')
|
const title = state.esc(node.attrs.params.title || '')
|
||||||
|
|
||||||
state.write(`\n`)
|
state.write(`\n`)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
state.write(`\`\`\`${node.attrs.params.lang || ''}\n`)
|
state.write('```' + (node.attrs.params.lang || '') + '\n')
|
||||||
state.text(node.textContent, false)
|
state.text(node.textContent, false)
|
||||||
state.ensureNewLine()
|
state.ensureNewLine()
|
||||||
state.write('```')
|
state.write('```')
|
||||||
state.closeBlock(node)
|
state.closeBlock(node)
|
||||||
},
|
},
|
||||||
todo_item(state, node) {
|
todo_item(state, node) {
|
||||||
state.write(`${node.attrs.done ? '[x]' : '[ ]'} `)
|
state.write((node.attrs.done ? '[x]' : '[ ]') + ' ')
|
||||||
state.renderContent(node)
|
state.renderContent(node)
|
||||||
},
|
},
|
||||||
table(state, node) {
|
table(state, node) {
|
||||||
function serializeTableHead(head: Node) {
|
function serializeTableHead(head: Node) {
|
||||||
let columnAlignments: string[] = []
|
let columnAlignments: string[] = []
|
||||||
|
|
||||||
head.forEach((headRow) => {
|
head.forEach((headRow) => {
|
||||||
if (headRow.type.name === 'table_row') {
|
if (headRow.type.name === 'table_row') {
|
||||||
columnAlignments = serializeTableRow(headRow)
|
columnAlignments = serializeTableRow(headRow)
|
||||||
|
@ -71,7 +75,6 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
state.write('---')
|
state.write('---')
|
||||||
state.write(alignment === 'right' || alignment === 'center' ? ':' : ' ')
|
state.write(alignment === 'right' || alignment === 'center' ? ':' : ' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
state.write('|')
|
state.write('|')
|
||||||
state.ensureNewLine()
|
state.ensureNewLine()
|
||||||
}
|
}
|
||||||
|
@ -87,17 +90,14 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
|
|
||||||
function serializeTableRow(row: Node): string[] {
|
function serializeTableRow(row: Node): string[] {
|
||||||
const columnAlignment: string[] = []
|
const columnAlignment: string[] = []
|
||||||
|
|
||||||
row.forEach((cell) => {
|
row.forEach((cell) => {
|
||||||
if (cell.type.name === 'table_header' || cell.type.name === 'table_cell') {
|
if (cell.type.name === 'table_header' || cell.type.name === 'table_cell') {
|
||||||
const alignment = serializeTableCell(cell)
|
const alignment = serializeTableCell(cell)
|
||||||
|
|
||||||
columnAlignment.push(alignment)
|
columnAlignment.push(alignment)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
state.write('|')
|
state.write('|')
|
||||||
state.ensureNewLine()
|
state.ensureNewLine()
|
||||||
|
|
||||||
return columnAlignment
|
return columnAlignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,13 +105,11 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
state.write('| ')
|
state.write('| ')
|
||||||
state.renderInline(cell)
|
state.renderInline(cell)
|
||||||
state.write(' ')
|
state.write(' ')
|
||||||
|
|
||||||
return findAlignment(cell)
|
return findAlignment(cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
node.forEach((table_child) => {
|
node.forEach((table_child) => {
|
||||||
if (table_child.type.name === 'table_head') serializeTableHead(table_child)
|
if (table_child.type.name === 'table_head') serializeTableHead(table_child)
|
||||||
|
|
||||||
if (table_child.type.name === 'table_body') serializeTableBody(table_child)
|
if (table_child.type.name === 'table_body') serializeTableBody(table_child)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -130,24 +128,11 @@ export const markdownSerializer = new MarkdownSerializer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const serialize = (state: EditorState) => {
|
function listIsTight(tokens: any, idx: number) {
|
||||||
// eslint-disable-next-line no-use-before-define
|
let i = idx
|
||||||
let text = markdownSerializer.serialize(state.doc)
|
while (++i < tokens.length) {
|
||||||
|
if (tokens[i].type !== 'list_item_open') return tokens[i].hidden
|
||||||
if (text.charAt(text.length - 1) !== '\n') {
|
|
||||||
text += '\n'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
function listIsTight(tokens: any[], i: number) {
|
|
||||||
for (let index = i + 1; i < tokens.length; index++) {
|
|
||||||
if (tokens[index].type !== 'list_item_open') {
|
|
||||||
return tokens[i].hidden
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,20 +161,18 @@ export const createMarkdownParser = (schema: Schema) =>
|
||||||
list_item: { block: 'list_item' },
|
list_item: { block: 'list_item' },
|
||||||
bullet_list: {
|
bullet_list: {
|
||||||
block: 'bullet_list',
|
block: 'bullet_list',
|
||||||
getAttrs: (_: Token, tokens: Token[], i: number): Record<string, any> => ({
|
getAttrs: (_, tokens, i) => ({ tight: listIsTight(tokens, i) })
|
||||||
tight: listIsTight(tokens, i)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
ordered_list: {
|
ordered_list: {
|
||||||
block: 'ordered_list',
|
block: 'ordered_list',
|
||||||
getAttrs: (tok: Token, tokens: Token[], i: number): Record<string, any> => ({
|
getAttrs: (tok, tokens, i) => ({
|
||||||
order: Number(tok.attrGet('start')) || 1,
|
order: +tok.attrGet('start') || 1,
|
||||||
tight: listIsTight(tokens, i)
|
tight: listIsTight(tokens, i)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
heading: {
|
heading: {
|
||||||
block: 'heading',
|
block: 'heading',
|
||||||
getAttrs: (tok) => ({ level: Number(tok.tag.slice(1)) })
|
getAttrs: (tok) => ({ level: +tok.tag.slice(1) })
|
||||||
},
|
},
|
||||||
code_block: {
|
code_block: {
|
||||||
block: 'code_block',
|
block: 'code_block',
|
||||||
|
@ -203,7 +186,7 @@ export const createMarkdownParser = (schema: Schema) =>
|
||||||
hr: { node: 'horizontal_rule' },
|
hr: { node: 'horizontal_rule' },
|
||||||
image: {
|
image: {
|
||||||
node: 'image',
|
node: 'image',
|
||||||
getAttrs: (tok: any) => ({
|
getAttrs: (tok) => ({
|
||||||
src: tok.attrGet('src'),
|
src: tok.attrGet('src'),
|
||||||
title: tok.attrGet('title') || null,
|
title: tok.attrGet('title') || null,
|
||||||
alt: (tok.children[0] && tok.children[0].content) || null
|
alt: (tok.children[0] && tok.children[0].content) || null
|
|
@ -1,12 +1,13 @@
|
||||||
import { schema as markdownSchema } from 'prosemirror-markdown'
|
import { schema as markdownSchema } from 'prosemirror-markdown'
|
||||||
import { Schema } from 'prosemirror-model'
|
import { NodeSpec, Schema } from 'prosemirror-model'
|
||||||
import { baseKeymap } from 'prosemirror-commands'
|
import { baseKeymap } from 'prosemirror-commands'
|
||||||
import { sinkListItem, liftListItem } from 'prosemirror-schema-list'
|
import { sinkListItem, liftListItem } from 'prosemirror-schema-list'
|
||||||
import { history } from 'prosemirror-history'
|
import { history } from 'prosemirror-history'
|
||||||
import { dropCursor } from 'prosemirror-dropcursor'
|
import { dropCursor } from 'prosemirror-dropcursor'
|
||||||
import { buildKeymap } from 'prosemirror-example-setup'
|
import { buildKeymap } from 'prosemirror-example-setup'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
import type OrderedMap from 'orderedmap'
|
||||||
|
|
||||||
const plainSchema = new Schema({
|
const plainSchema = new Schema({
|
||||||
nodes: {
|
nodes: {
|
||||||
|
@ -29,7 +30,7 @@ const blockquoteSchema = {
|
||||||
content: 'block+',
|
content: 'block+',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
toDOM: () => ['div', ['blockquote', 0]]
|
toDOM: () => ['div', ['blockquote', 0]]
|
||||||
}
|
} as NodeSpec
|
||||||
|
|
||||||
export default (plain = false): ProseMirrorExtension => ({
|
export default (plain = false): ProseMirrorExtension => ({
|
||||||
schema: () =>
|
schema: () =>
|
||||||
|
@ -39,7 +40,7 @@ export default (plain = false): ProseMirrorExtension => ({
|
||||||
marks: plainSchema.spec.marks
|
marks: plainSchema.spec.marks
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
nodes: (markdownSchema.spec.nodes as any).update('blockquote', blockquoteSchema),
|
nodes: (markdownSchema.spec.nodes as OrderedMap<NodeSpec>).update('blockquote', blockquoteSchema),
|
||||||
marks: markdownSchema.spec.marks
|
marks: markdownSchema.spec.marks
|
||||||
},
|
},
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { EditorState, Transaction } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import { markInputRule } from './mark-input-rule'
|
import { markInputRule } from './mark-input-rule'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const blank = '\u00A0'
|
const blank = '\u00A0'
|
||||||
|
|
||||||
|
@ -12,21 +12,18 @@ const onArrow =
|
||||||
(dir: 'left' | 'right') =>
|
(dir: 'left' | 'right') =>
|
||||||
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
(state: EditorState, dispatch: (tr: Transaction) => void, editorView: EditorView) => {
|
||||||
if (!state.selection.empty) return false
|
if (!state.selection.empty) return false
|
||||||
|
|
||||||
const $pos = state.selection.$head
|
const $pos = state.selection.$head
|
||||||
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
const isCode = $pos.marks().find((m: Mark) => m.type.name === 'code')
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
|
|
||||||
if (dir === 'left') {
|
if (dir === 'left') {
|
||||||
const up = editorView.endOfTextblock('up')
|
const up = editorView.endOfTextblock('up')
|
||||||
|
|
||||||
if (!$pos.nodeBefore && up && isCode) {
|
if (!$pos.nodeBefore && up && isCode) {
|
||||||
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
tr.insertText(blank, $pos.pos - 1, $pos.pos)
|
||||||
dispatch(tr)
|
dispatch(tr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const down = editorView.endOfTextblock('down')
|
const down = editorView.endOfTextblock('down')
|
||||||
|
|
||||||
if (!$pos.nodeAfter && down && isCode) {
|
if (!$pos.nodeAfter && down && isCode) {
|
||||||
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
tr.insertText(blank, $pos.pos, $pos.pos + 1)
|
||||||
dispatch(tr)
|
dispatch(tr)
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
import { ySyncPlugin, yCursorPlugin, yUndoPlugin } from 'y-prosemirror'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type { YOptions } from '../../store'
|
import type { YOptions } from '../../store/context'
|
||||||
|
|
||||||
export const cursorBuilder = (user: any): HTMLElement => {
|
interface YUser {
|
||||||
|
background: string
|
||||||
|
foreground: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cursorBuilder = (user: YUser): HTMLElement => {
|
||||||
const cursor = document.createElement('span')
|
const cursor = document.createElement('span')
|
||||||
|
|
||||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||||
cursor.setAttribute('style', `border-color: ${user.background}`)
|
cursor.setAttribute('style', `border-color: ${user.background}`)
|
||||||
const userDiv = document.createElement('span')
|
const userDiv = document.createElement('span')
|
||||||
|
|
||||||
userDiv.setAttribute('style', `background-color: ${user.background}; color: ${user.foreground}`)
|
userDiv.setAttribute('style', `background-color: ${user.background}; color: ${user.foreground}`)
|
||||||
userDiv.textContent = user.name
|
userDiv.textContent = user.name
|
||||||
cursor.append(userDiv)
|
cursor.append(userDiv)
|
||||||
|
|
||||||
return cursor
|
return cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +25,6 @@ export default (y: YOptions): ProseMirrorExtension => ({
|
||||||
? [
|
? [
|
||||||
...prev,
|
...prev,
|
||||||
ySyncPlugin(y.type),
|
ySyncPlugin(y.type),
|
||||||
// FIXME
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
yCursorPlugin(y.provider.awareness, { cursorBuilder }),
|
||||||
yUndoPlugin()
|
yUndoPlugin()
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Plugin, NodeSelection } from 'prosemirror-state'
|
import { Plugin, NodeSelection } from 'prosemirror-state'
|
||||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const handleIcon = `
|
const handleIcon = `
|
||||||
<svg viewBox="0 0 10 10" height="14" width="14">
|
<svg viewBox="0 0 10 10" height="14" width="14">
|
||||||
|
@ -9,14 +9,11 @@ const handleIcon = `
|
||||||
|
|
||||||
const createDragHandle = () => {
|
const createDragHandle = () => {
|
||||||
const handle = document.createElement('span')
|
const handle = document.createElement('span')
|
||||||
|
|
||||||
handle.setAttribute('contenteditable', 'false')
|
handle.setAttribute('contenteditable', 'false')
|
||||||
const icon = document.createElement('span')
|
const icon = document.createElement('span')
|
||||||
|
|
||||||
icon.innerHTML = handleIcon
|
icon.innerHTML = handleIcon
|
||||||
handle.append(icon)
|
handle.appendChild(icon)
|
||||||
handle.classList.add('handle')
|
handle.classList.add('handle')
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,13 +21,10 @@ const handlePlugin = new Plugin({
|
||||||
props: {
|
props: {
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
const decos = []
|
const decos = []
|
||||||
|
|
||||||
state.doc.forEach((node, pos) => {
|
state.doc.forEach((node, pos) => {
|
||||||
decos.push(
|
decos.push(
|
||||||
Decoration.widget(pos + 1, createDragHandle),
|
Decoration.widget(pos + 1, createDragHandle),
|
||||||
Decoration.node(pos, pos + node.nodeSize, {
|
Decoration.node(pos, pos + node.nodeSize, { class: 'draggable' })
|
||||||
class: 'draggable'
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,15 +33,12 @@ const handlePlugin = new Plugin({
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
mousedown: (editorView, event) => {
|
mousedown: (editorView, event) => {
|
||||||
const target = event.target as Element
|
const target = event.target as Element
|
||||||
|
|
||||||
if (target.classList.contains('handle')) {
|
if (target.classList.contains('handle')) {
|
||||||
const pos = editorView.posAtCoords({ left: event.x, top: event.y })
|
const pos = editorView.posAtCoords({ left: event.x, top: event.y })
|
||||||
const resolved = editorView.state.doc.resolve(pos.pos)
|
const resolved = editorView.state.doc.resolve(pos.pos)
|
||||||
const tr = editorView.state.tr
|
const tr = editorView.state.tr
|
||||||
|
|
||||||
tr.setSelection(NodeSelection.create(editorView.state.doc, resolved.before()))
|
tr.setSelection(NodeSelection.create(editorView.state.doc, resolved.before()))
|
||||||
editorView.dispatch(tr)
|
editorView.dispatch(tr)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { Node, Schema } from 'prosemirror-model'
|
import type { Node, Schema } from 'prosemirror-model'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
// import { convertFileSrc } from '@tauri-apps/api/tauri'
|
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||||
// import { resolvePath, dirname } from '../../remote'
|
import type OrderedMap from 'orderedmap'
|
||||||
// import { isTauri } from '../../env'
|
|
||||||
import type { ProseMirrorExtension } from '../state'
|
|
||||||
|
|
||||||
const REGEX = /^!\[([^[\]]*)]\((.+?)\)\s+/
|
const REGEX = /^!\[([^[\]]*?)]\((.+?)\)\s+/
|
||||||
const MAX_MATCH = 500
|
const MAX_MATCH = 500
|
||||||
|
|
||||||
const isUrl = (str: string) => {
|
const isUrl = (str: string) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(str)
|
const url = new URL(str)
|
||||||
|
|
||||||
return url.protocol === 'http:' || url.protocol === 'https:'
|
return url.protocol === 'http:' || url.protocol === 'https:'
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
@ -20,64 +17,35 @@ const isUrl = (str: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
const isBlank = (text: string) => text === ' ' || text === '\u00A0'
|
||||||
/*
|
|
||||||
export const getImagePath = async (src: string, path?: string) => {
|
|
||||||
let paths = [src]
|
|
||||||
|
|
||||||
if (path) paths = [await dirname(path), src]
|
const imageInput = (schema: Schema, path?: string) =>
|
||||||
|
|
||||||
const absolutePath = await resolvePath(paths)
|
|
||||||
|
|
||||||
return convertFileSrc(absolutePath)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const imageInput = (schema: Schema, _path?: string) =>
|
|
||||||
new Plugin({
|
new Plugin({
|
||||||
props: {
|
props: {
|
||||||
handleTextInput(view, from, to, text) {
|
handleTextInput(view, from, to, text) {
|
||||||
if (view.composing || !isBlank(text)) return false
|
if (view.composing || !isBlank(text)) return false
|
||||||
|
|
||||||
const $from = view.state.doc.resolve(from)
|
const $from = view.state.doc.resolve(from)
|
||||||
|
|
||||||
if ($from.parent.type.spec.code) return false
|
if ($from.parent.type.spec.code) return false
|
||||||
|
|
||||||
const textBefore =
|
const textBefore =
|
||||||
$from.parent.textBetween(
|
$from.parent.textBetween(
|
||||||
Math.max(0, $from.parentOffset - MAX_MATCH),
|
Math.max(0, $from.parentOffset - MAX_MATCH),
|
||||||
$from.parentOffset,
|
$from.parentOffset,
|
||||||
undefined,
|
null,
|
||||||
'\uFFFC'
|
'\uFFFC'
|
||||||
) + text
|
) + text
|
||||||
|
|
||||||
const match = REGEX.exec(textBefore)
|
const match = REGEX.exec(textBefore)
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const [, title, src] = match
|
const [, title, src] = match
|
||||||
|
|
||||||
if (isUrl(src)) {
|
if (isUrl(src)) {
|
||||||
const node = schema.node('image', { src, title })
|
const node = schema.node('image', { src, title })
|
||||||
const start = from - (match[0].length - text.length)
|
const start = from - (match[0].length - text.length)
|
||||||
const tr = view.state.tr
|
const tr = view.state.tr
|
||||||
|
|
||||||
tr.delete(start, to)
|
tr.delete(start, to)
|
||||||
tr.insert(start, node)
|
tr.insert(start, node)
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!isTauri) return false
|
|
||||||
/*
|
|
||||||
getImagePath(src, path).then((p) => {
|
|
||||||
const node = schema.node('image', { src: p, title, path: src })
|
|
||||||
const start = from - (match[0].length - text.length)
|
|
||||||
const tr = view.state.tr
|
|
||||||
|
|
||||||
tr.delete(start, to)
|
|
||||||
tr.insert(start, node)
|
|
||||||
view.dispatch(tr)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,14 +89,10 @@ export const insertImage = (view: EditorView, src: string, left: number, top: nu
|
||||||
const state = view.state
|
const state = view.state
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
const node = state.schema.nodes.image.create({ src })
|
const node = state.schema.nodes.image.create({ src })
|
||||||
|
|
||||||
if (view) {
|
|
||||||
const pos = view.posAtCoords({ left, top }).pos
|
const pos = view.posAtCoords({ left, top }).pos
|
||||||
|
|
||||||
tr.insert(pos, node)
|
tr.insert(pos, node)
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class ImageView {
|
class ImageView {
|
||||||
node: Node
|
node: Node
|
||||||
|
@ -144,7 +108,7 @@ class ImageView {
|
||||||
width: number
|
width: number
|
||||||
updating: number
|
updating: number
|
||||||
|
|
||||||
constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, _path: string) {
|
constructor(node: Node, view: EditorView, getPos: () => number, schema: Schema, path: string) {
|
||||||
this.node = node
|
this.node = node
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos
|
this.getPos = getPos
|
||||||
|
@ -154,23 +118,11 @@ class ImageView {
|
||||||
|
|
||||||
this.container = document.createElement('span')
|
this.container = document.createElement('span')
|
||||||
this.container.className = 'image-container'
|
this.container.className = 'image-container'
|
||||||
|
|
||||||
if (node.attrs.width) this.setWidth(node.attrs.width)
|
if (node.attrs.width) this.setWidth(node.attrs.width)
|
||||||
|
|
||||||
const image = document.createElement('img')
|
const image = document.createElement('img')
|
||||||
|
|
||||||
image.setAttribute('title', node.attrs.title ?? '')
|
image.setAttribute('title', node.attrs.title ?? '')
|
||||||
|
|
||||||
if (
|
|
||||||
// isTauri &&
|
|
||||||
!node.attrs.src.startsWith('asset:') &&
|
|
||||||
!node.attrs.src.startsWith('data:') &&
|
|
||||||
!isUrl(node.attrs.src)
|
|
||||||
) {
|
|
||||||
// getImagePath(node.attrs.src, path).then((p) => image.setAttribute('src', p))
|
|
||||||
} else {
|
|
||||||
image.setAttribute('src', node.attrs.src)
|
image.setAttribute('src', node.attrs.src)
|
||||||
}
|
|
||||||
|
|
||||||
this.handle = document.createElement('span')
|
this.handle = document.createElement('span')
|
||||||
this.handle.className = 'resize-handle'
|
this.handle.className = 'resize-handle'
|
||||||
|
@ -180,8 +132,8 @@ class ImageView {
|
||||||
window.addEventListener('mouseup', this.onResizeEndFn)
|
window.addEventListener('mouseup', this.onResizeEndFn)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.container.append(image)
|
this.container.appendChild(image)
|
||||||
this.container.append(this.handle)
|
this.container.appendChild(this.handle)
|
||||||
this.dom = this.container
|
this.dom = this.container
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,12 +144,9 @@ class ImageView {
|
||||||
|
|
||||||
onResizeEnd() {
|
onResizeEnd() {
|
||||||
window.removeEventListener('mousemove', this.onResizeFn)
|
window.removeEventListener('mousemove', this.onResizeFn)
|
||||||
|
|
||||||
if (this.updating === this.width) return
|
if (this.updating === this.width) return
|
||||||
|
|
||||||
this.updating = this.width
|
this.updating = this.width
|
||||||
const tr = this.view.state.tr
|
const tr = this.view.state.tr
|
||||||
|
|
||||||
tr.setNodeMarkup(this.getPos(), undefined, {
|
tr.setNodeMarkup(this.getPos(), undefined, {
|
||||||
...this.node.attrs,
|
...this.node.attrs,
|
||||||
width: this.width
|
width: this.width
|
||||||
|
@ -207,22 +156,19 @@ class ImageView {
|
||||||
}
|
}
|
||||||
|
|
||||||
setWidth(width: number) {
|
setWidth(width: number) {
|
||||||
this.container.style.width = `${width}px`
|
this.container.style.width = width + 'px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (path?: string): ProseMirrorExtension => ({
|
export default (path?: string): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
nodes: (prev.nodes as any).update('image', imageSchema)
|
nodes: (prev.nodes as OrderedMap<any>).update('image', imageSchema)
|
||||||
}),
|
}),
|
||||||
plugins: (prev, schema) => [...prev, imageInput(schema, path)],
|
plugins: (prev, schema) => [...prev, imageInput(schema, path)],
|
||||||
nodeViews: {
|
nodeViews: {
|
||||||
// FIXME something is not right
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
image: (node, view, getPos) => {
|
image: (node, view, getPos) => {
|
||||||
return new ImageView(node, view, getPos, view.state.schema, path)
|
return new ImageView(node, view, getPos, view.state.schema, path)
|
||||||
}
|
}
|
||||||
}
|
} as unknown as { [key: string]: NodeViewFn }
|
||||||
})
|
})
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'prosemirror-example-setup'
|
|
|
@ -1,16 +1,14 @@
|
||||||
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
import { Plugin, PluginKey, TextSelection, Transaction } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import type { Mark, Node, ResolvedPos, Schema } from 'prosemirror-model'
|
import type { Mark, Node, Schema } from 'prosemirror-model'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
const REGEX = /(^|\s)\[(.+)]\(([^ ]+)(?: "(.+)")?\)/
|
||||||
|
|
||||||
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
||||||
let markPos = { from: -1, to: -1 }
|
let markPos = { from: -1, to: -1 }
|
||||||
|
|
||||||
doc.nodesBetween(from, to, (node, pos) => {
|
doc.nodesBetween(from, to, (node, pos) => {
|
||||||
if (markPos.from > -1) return false
|
if (markPos.from > -1) return false
|
||||||
|
|
||||||
if (markPos.from === -1 && mark.isInSet(node.marks)) {
|
if (markPos.from === -1 && mark.isInSet(node.marks)) {
|
||||||
markPos = { from: pos, to: pos + Math.max(node.textContent.length, 1) }
|
markPos = { from: pos, to: pos + Math.max(node.textContent.length, 1) }
|
||||||
}
|
}
|
||||||
|
@ -21,6 +19,38 @@ const findMarkPosition = (mark: Mark, doc: Node, from: number, to: number) => {
|
||||||
|
|
||||||
const pluginKey = new PluginKey('markdown-links')
|
const pluginKey = new PluginKey('markdown-links')
|
||||||
|
|
||||||
|
const markdownLinks = (schema: Schema) =>
|
||||||
|
new Plugin({
|
||||||
|
key: pluginKey,
|
||||||
|
state: {
|
||||||
|
init() {
|
||||||
|
return { schema }
|
||||||
|
},
|
||||||
|
apply(tr, state: any) {
|
||||||
|
const action = tr.getMeta(this)
|
||||||
|
if (action?.pos) {
|
||||||
|
state.pos = action.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
handleDOMEvents: {
|
||||||
|
keyup: (view) => {
|
||||||
|
return handleMove(view)
|
||||||
|
},
|
||||||
|
click: (view, e) => {
|
||||||
|
if (handleMove(view)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const resolvePos = (view: EditorView, pos: number) => {
|
const resolvePos = (view: EditorView, pos: number) => {
|
||||||
try {
|
try {
|
||||||
return view.state.doc.resolve(pos)
|
return view.state.doc.resolve(pos)
|
||||||
|
@ -29,7 +59,6 @@ const resolvePos = (view: EditorView, pos: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const toLink = (view: EditorView, tr: Transaction) => {
|
const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
const sel = view.state.selection
|
const sel = view.state.selection
|
||||||
|
@ -38,7 +67,6 @@ const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
|
|
||||||
if (lastPos !== undefined) {
|
if (lastPos !== undefined) {
|
||||||
const $from = resolvePos(view, lastPos)
|
const $from = resolvePos(view, lastPos)
|
||||||
|
|
||||||
if (!$from || $from.depth === 0 || $from.parent.type.spec.code) {
|
if (!$from || $from.depth === 0 || $from.parent.type.spec.code) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -62,8 +90,7 @@ const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
|
|
||||||
// Do not convert md links if content has marks
|
// Do not convert md links if content has marks
|
||||||
const $startPos = resolvePos(view, start)
|
const $startPos = resolvePos(view, start)
|
||||||
|
if ($startPos.marks().length > 0) {
|
||||||
if (($startPos as ResolvedPos).marks().length > 0) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,15 +98,12 @@ const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
const textEnd = textStart + text.length
|
const textEnd = textStart + text.length
|
||||||
|
|
||||||
if (textEnd < end) tr.delete(textEnd, end)
|
if (textEnd < end) tr.delete(textEnd, end)
|
||||||
|
|
||||||
if (textStart > start) tr.delete(start, textStart)
|
if (textStart > start) tr.delete(start, textStart)
|
||||||
|
|
||||||
const to = start + text.length
|
const to = start + text.length
|
||||||
|
|
||||||
tr.addMark(start, to, state.schema.marks.link.create({ href }))
|
tr.addMark(start, to, state.schema.marks.link.create({ href }))
|
||||||
|
|
||||||
const sub = end - textEnd + textStart - start
|
const sub = end - textEnd + textStart - start
|
||||||
|
|
||||||
tr.setMeta(pluginKey, { pos: sel.$head.pos - sub })
|
tr.setMeta(pluginKey, { pos: sel.$head.pos - sub })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -92,7 +116,6 @@ const toLink = (view: EditorView, tr: Transaction) => {
|
||||||
const toMarkdown = (view: EditorView, tr: Transaction) => {
|
const toMarkdown = (view: EditorView, tr: Transaction) => {
|
||||||
const { schema } = pluginKey.getState(view.state)
|
const { schema } = pluginKey.getState(view.state)
|
||||||
const sel = view.state.selection
|
const sel = view.state.selection
|
||||||
|
|
||||||
if (sel.$head.depth === 0 || sel.$head.parent.type.spec.code) {
|
if (sel.$head.depth === 0 || sel.$head.parent.type.spec.code) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -105,11 +128,9 @@ const toMarkdown = (view: EditorView, tr: Transaction) => {
|
||||||
const { href } = mark.attrs
|
const { href } = mark.attrs
|
||||||
const range = findMarkPosition(mark, view.state.doc, textFrom, textTo)
|
const range = findMarkPosition(mark, view.state.doc, textFrom, textTo)
|
||||||
const text = view.state.doc.textBetween(range.from, range.to, '\0', '\0')
|
const text = view.state.doc.textBetween(range.from, range.to, '\0', '\0')
|
||||||
|
|
||||||
tr.replaceRangeWith(range.from, range.to, view.state.schema.text(`[${text}](${href})`))
|
tr.replaceRangeWith(range.from, range.to, view.state.schema.text(`[${text}](${href})`))
|
||||||
tr.setSelection(new TextSelection(tr.doc.resolve(sel.$head.pos + 1)))
|
tr.setSelection(new TextSelection(tr.doc.resolve(sel.$head.pos + 1)))
|
||||||
tr.setMeta(pluginKey, { pos: sel.$head.pos })
|
tr.setMeta(pluginKey, { pos: sel.$head.pos })
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,66 +139,25 @@ const toMarkdown = (view: EditorView, tr: Transaction) => {
|
||||||
|
|
||||||
const handleMove = (view: EditorView) => {
|
const handleMove = (view: EditorView) => {
|
||||||
const sel = view.state.selection
|
const sel = view.state.selection
|
||||||
|
|
||||||
if (!sel.empty || !sel.$head) return false
|
if (!sel.empty || !sel.$head) return false
|
||||||
|
|
||||||
const pos = sel.$head.pos
|
const pos = sel.$head.pos
|
||||||
const tr = view.state.tr
|
const tr = view.state.tr
|
||||||
|
|
||||||
if (toLink(view, tr)) {
|
if (toLink(view, tr)) {
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toMarkdown(view, tr)) {
|
if (toMarkdown(view, tr)) {
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.setMeta(pluginKey, { pos })
|
tr.setMeta(pluginKey, { pos })
|
||||||
view.dispatch(tr)
|
view.dispatch(tr)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const markdownLinks = (schema: Schema) =>
|
|
||||||
new Plugin({
|
|
||||||
key: pluginKey,
|
|
||||||
state: {
|
|
||||||
init() {
|
|
||||||
return { schema }
|
|
||||||
},
|
|
||||||
apply(tr, state) {
|
|
||||||
const action = tr.getMeta(this)
|
|
||||||
|
|
||||||
if (action?.pos) {
|
|
||||||
// FIXME
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
state.pos = action.pos
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
handleDOMEvents: {
|
|
||||||
keyup: (view) => {
|
|
||||||
return handleMove(view)
|
|
||||||
},
|
|
||||||
click: (view, e) => {
|
|
||||||
if (handleMove(view)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [...prev, markdownLinks(schema)]
|
plugins: (prev, schema) => [...prev, markdownLinks(schema)]
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,37 +2,26 @@ import { InputRule } from 'prosemirror-inputrules'
|
||||||
import type { EditorState } from 'prosemirror-state'
|
import type { EditorState } from 'prosemirror-state'
|
||||||
import type { MarkType } from 'prosemirror-model'
|
import type { MarkType } from 'prosemirror-model'
|
||||||
|
|
||||||
export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs?) =>
|
export const markInputRule = (regexp: RegExp, nodeType: MarkType, getAttrs = null) =>
|
||||||
// FIXME ?
|
new InputRule(regexp, (state: EditorState, match: string[], start: number, end: number) => {
|
||||||
new InputRule(regexp, (state: EditorState, match: string[], start: number, endArg: number) => {
|
let markEnd = end
|
||||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
let end = endArg
|
|
||||||
|
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
const textStart = start + match[0].indexOf(match[1])
|
const textStart = start + match[0].indexOf(match[1])
|
||||||
const textEnd = textStart + match[1].length
|
const textEnd = textStart + match[1].length
|
||||||
let hasMarks = false
|
let hasMarks = false
|
||||||
|
|
||||||
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
state.doc.nodesBetween(textStart, textEnd, (node) => {
|
||||||
if (node.marks.length > 0) {
|
hasMarks = node.marks.length > 0
|
||||||
hasMarks = true
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (hasMarks) {
|
if (hasMarks) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textEnd < end) tr.delete(textEnd, end)
|
if (textEnd < end) tr.delete(textEnd, end)
|
||||||
|
|
||||||
if (textStart > start) tr.delete(start, textStart)
|
if (textStart > start) tr.delete(start, textStart)
|
||||||
|
markEnd = start + match[1].length
|
||||||
end = start + match[1].length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.addMark(start, end, nodeType.create(attrs))
|
tr.addMark(start, markEnd, nodeType.create(attrs))
|
||||||
tr.removeStoredMark(nodeType)
|
tr.removeStoredMark(nodeType)
|
||||||
|
|
||||||
return tr
|
return tr
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
ellipsis
|
ellipsis
|
||||||
} from 'prosemirror-inputrules'
|
} from 'prosemirror-inputrules'
|
||||||
import type { NodeType, Schema } from 'prosemirror-model'
|
import type { NodeType, Schema } from 'prosemirror-model'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
const blockQuoteRule = (nodeType: NodeType) => wrappingInputRule(/^\s*>\s$/, nodeType)
|
||||||
|
|
||||||
|
@ -16,9 +16,7 @@ const orderedListRule = (nodeType: NodeType) =>
|
||||||
/^(\d+)\.\s$/,
|
/^(\d+)\.\s$/,
|
||||||
nodeType,
|
nodeType,
|
||||||
(match) => ({ order: +match[1] }),
|
(match) => ({ order: +match[1] }),
|
||||||
// FIXME
|
(match, node) => node.childCount + node.attrs.order === +match[1]
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
(match, node) => node.childCount + node.attrs.order == +match[1]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([*+-])\s$/, nodeType)
|
const bulletListRule = (nodeType: NodeType) => wrappingInputRule(/^\s*([*+-])\s$/, nodeType)
|
||||||
|
|
|
@ -13,18 +13,18 @@ import {
|
||||||
Dropdown
|
Dropdown
|
||||||
} from 'prosemirror-menu'
|
} from 'prosemirror-menu'
|
||||||
|
|
||||||
import type { MenuItemSpec, MenuElement } from 'prosemirror-menu'
|
|
||||||
|
|
||||||
import { wrapInList } from 'prosemirror-schema-list'
|
import { wrapInList } from 'prosemirror-schema-list'
|
||||||
import { NodeSelection } from 'prosemirror-state'
|
import type { NodeSelection } from 'prosemirror-state'
|
||||||
|
|
||||||
import { TextField, openPrompt } from './prompt'
|
import { TextField, openPrompt } from './prompt'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type { Schema } from 'prosemirror-model'
|
import type { Schema } from 'prosemirror-model'
|
||||||
|
|
||||||
// Helpers to create specific types of items
|
// Helpers to create specific types of items
|
||||||
|
|
||||||
function canInsert(state: { selection: { $from: any } }, nodeType: any) {
|
const cut = (something) => something.filter(Boolean)
|
||||||
|
|
||||||
|
function canInsert(state, nodeType) {
|
||||||
const $from = state.selection.$from
|
const $from = state.selection.$from
|
||||||
|
|
||||||
for (let d = $from.depth; d >= 0; d--) {
|
for (let d = $from.depth; d >= 0; d--) {
|
||||||
|
@ -36,31 +36,19 @@ function canInsert(state: { selection: { $from: any } }, nodeType: any) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) {
|
function insertImageItem(nodeType) {
|
||||||
return new MenuItem({
|
return new MenuItem({
|
||||||
icon: icons.image,
|
icon: icons.image,
|
||||||
label: 'image',
|
label: 'image',
|
||||||
enable(state: any) {
|
enable(state) {
|
||||||
return canInsert(state, nodeType)
|
return canInsert(state, nodeType)
|
||||||
},
|
},
|
||||||
run(
|
run(state, _, view) {
|
||||||
state: {
|
const {
|
||||||
selection: { node?: any; from?: any; to?: any }
|
from,
|
||||||
doc: { textBetween: (arg0: any, arg1: any, arg2: string) => any }
|
to,
|
||||||
},
|
node: { attrs }
|
||||||
_: any,
|
} = state.selection as NodeSelection
|
||||||
view: {
|
|
||||||
dispatch: (arg0: any) => void
|
|
||||||
state: { tr: { replaceSelectionWith: (arg0: any) => any } }
|
|
||||||
focus: () => void
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const { from, to, node } = state.selection
|
|
||||||
let attrs = null
|
|
||||||
|
|
||||||
if (state.selection instanceof NodeSelection && node.type === nodeType) {
|
|
||||||
attrs = node.attrs
|
|
||||||
}
|
|
||||||
|
|
||||||
openPrompt({
|
openPrompt({
|
||||||
title: 'Insert image',
|
title: 'Insert image',
|
||||||
|
@ -76,9 +64,8 @@ function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) {
|
||||||
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
value: attrs ? attrs.alt : state.doc.textBetween(from, to, ' ')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line no-shadow
|
callback(newAttrs) {
|
||||||
callback(attrs: any) {
|
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(newAttrs)))
|
||||||
view.dispatch(view.state.tr.replaceSelectionWith(nodeType.createAndFill(attrs)))
|
|
||||||
view.focus()
|
view.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -86,32 +73,22 @@ function insertImageItem(nodeType: { createAndFill: (arg0: any) => any }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmdItem(
|
function cmdItem(cmd, options) {
|
||||||
cmd: (arg0: any) => any,
|
|
||||||
options: { [x: string]: any; active?: (state: any) => any; enable?: any; title?: any; select?: any }
|
|
||||||
) {
|
|
||||||
const passedOptions = {
|
const passedOptions = {
|
||||||
label: options.title,
|
label: options.title,
|
||||||
run: cmd
|
run: cmd
|
||||||
} as { [key: string]: any }
|
}
|
||||||
|
|
||||||
Object.keys(options).forEach((prop) => (passedOptions[prop] = options[prop]))
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
if ((!options.enable || options.enable === true) && !options.select) {
|
if ((!options.enable || options.enable === true) && !options.select) {
|
||||||
passedOptions[options.enable ? 'enable' : 'select'] = (state: any) => cmd(state)
|
passedOptions[options.enable ? 'enable' : 'select'] = (state) => cmd(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MenuItem(passedOptions as MenuItemSpec)
|
return new MenuItem(passedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function markActive(
|
function markActive(state, type) {
|
||||||
state: {
|
|
||||||
selection: { from: any; $from: any; to: any; empty: any }
|
|
||||||
storedMarks: any
|
|
||||||
doc: { rangeHasMark: (arg0: any, arg1: any, arg2: any) => any }
|
|
||||||
},
|
|
||||||
type: { isInSet: (arg0: any) => any }
|
|
||||||
) {
|
|
||||||
const { from, $from, to, empty } = state.selection
|
const { from, $from, to, empty } = state.selection
|
||||||
|
|
||||||
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
if (empty) return type.isInSet(state.storedMarks || $from.marks())
|
||||||
|
@ -119,20 +96,20 @@ function markActive(
|
||||||
return state.doc.rangeHasMark(from, to, type)
|
return state.doc.rangeHasMark(from, to, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
function markItem(markType: any, options: { [x: string]: any; title?: string; icon?: any }) {
|
function markItem(markType, options) {
|
||||||
const passedOptions = {
|
const passedOptions = {
|
||||||
active(state: any) {
|
active(state) {
|
||||||
return markActive(state, markType)
|
return markActive(state, markType)
|
||||||
},
|
},
|
||||||
enable: true
|
enable: true
|
||||||
} as { [key: string]: any }
|
}
|
||||||
|
|
||||||
Object.keys(options).forEach((prop: string) => (passedOptions[prop] = options[prop]))
|
for (const prop in options) passedOptions[prop] = options[prop]
|
||||||
|
|
||||||
return cmdItem(toggleMark(markType), passedOptions)
|
return cmdItem(toggleMark(markType), passedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkItem(markType: any) {
|
function linkItem(markType) {
|
||||||
return new MenuItem({
|
return new MenuItem({
|
||||||
title: 'Add or remove link',
|
title: 'Add or remove link',
|
||||||
icon: {
|
icon: {
|
||||||
|
@ -140,13 +117,13 @@ function linkItem(markType: any) {
|
||||||
height: 18,
|
height: 18,
|
||||||
path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z'
|
path: 'M3.27177 14.7277C2.06258 13.5186 2.06258 11.5527 3.27177 10.3435L6.10029 7.51502L4.75675 6.17148L1.92823 9C-0.0234511 10.9517 -0.0234511 14.1196 1.92823 16.0713C3.87991 18.023 7.04785 18.023 8.99952 16.0713L11.828 13.2428L10.4845 11.8992L7.65598 14.7277C6.44679 15.9369 4.48097 15.9369 3.27177 14.7277ZM6.87756 12.536L12.5346 6.87895L11.1203 5.46469L5.4633 11.1217L6.87756 12.536ZM6.17055 4.75768L8.99907 1.92916C10.9507 -0.0225206 14.1187 -0.0225201 16.0704 1.92916C18.022 3.88084 18.022 7.04878 16.0704 9.00046L13.2418 11.829L11.8983 10.4854L14.7268 7.65691C15.936 6.44772 15.936 4.4819 14.7268 3.27271C13.5176 2.06351 11.5518 2.06351 10.3426 3.2727L7.51409 6.10122L6.17055 4.75768Z'
|
||||||
},
|
},
|
||||||
active(state: any) {
|
active(state) {
|
||||||
return markActive(state, markType)
|
return markActive(state, markType)
|
||||||
},
|
},
|
||||||
enable(state: { selection: { empty: any } }) {
|
enable(state) {
|
||||||
return !state.selection.empty
|
return !state.selection.empty
|
||||||
},
|
},
|
||||||
run(state: any, dispatch: any, view: { state: any; dispatch: any; focus: () => void }) {
|
run(state, dispatch, view) {
|
||||||
if (markActive(state, markType)) {
|
if (markActive(state, markType)) {
|
||||||
toggleMark(markType)(state, dispatch)
|
toggleMark(markType)(state, dispatch)
|
||||||
|
|
||||||
|
@ -160,7 +137,7 @@ function linkItem(markType: any) {
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
callback(attrs: any) {
|
callback(attrs) {
|
||||||
toggleMark(markType, attrs)(view.state, view.dispatch)
|
toggleMark(markType, attrs)(view.state, view.dispatch)
|
||||||
view.focus()
|
view.focus()
|
||||||
}
|
}
|
||||||
|
@ -169,14 +146,7 @@ function linkItem(markType: any) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapListItem(
|
function wrapListItem(nodeType, options) {
|
||||||
nodeType: any,
|
|
||||||
options: {
|
|
||||||
title?: string
|
|
||||||
icon?: { width: number; height: number; path: string } | { width: number; height: number; path: string }
|
|
||||||
attrs?: any
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
return cmdItem(wrapInList(nodeType, options.attrs), options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,24 +208,10 @@ function wrapListItem(
|
||||||
// **`fullMenu`**`: [[MenuElement]]`
|
// **`fullMenu`**`: [[MenuElement]]`
|
||||||
// : An array of arrays of menu elements for use as the full menu
|
// : An array of arrays of menu elements for use as the full menu
|
||||||
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
|
// for, for example the [menu bar](https://github.com/prosemirror/prosemirror-menu#user-content-menubar).
|
||||||
/*
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
type BuildSchema = {
|
export function buildMenuItems(schema: Schema<any, any>) {
|
||||||
marks: { strong: any; em: any; code: any; link: any; blockquote: any }
|
|
||||||
nodes: {
|
|
||||||
image: any
|
|
||||||
bullet_list: any
|
|
||||||
ordered_list: any
|
|
||||||
blockquote: any
|
|
||||||
paragraph: any
|
|
||||||
code_block: any
|
|
||||||
heading: any
|
|
||||||
horizontal_rule: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
export function buildMenuItems(schema: Schema) {
|
|
||||||
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
const r: { [key: string]: MenuItem | MenuItem[] } = {}
|
||||||
let type: any
|
let type
|
||||||
|
|
||||||
if ((type = schema.marks.strong)) {
|
if ((type = schema.marks.strong)) {
|
||||||
r.toggleStrong = markItem(type, {
|
r.toggleStrong = markItem(type, {
|
||||||
|
@ -360,41 +316,29 @@ export function buildMenuItems(schema: Schema) {
|
||||||
r.insertHorizontalRule = new MenuItem({
|
r.insertHorizontalRule = new MenuItem({
|
||||||
label: '---',
|
label: '---',
|
||||||
icon: icons.horizontal_rule,
|
icon: icons.horizontal_rule,
|
||||||
enable(state: any) {
|
enable(state) {
|
||||||
return canInsert(state, hr)
|
return canInsert(state, hr)
|
||||||
},
|
},
|
||||||
run(state: { tr: { replaceSelectionWith: (arg0: any) => any } }, dispatch: (arg0: any) => void) {
|
run(state, dispatch) {
|
||||||
dispatch(state.tr.replaceSelectionWith(hr.create()))
|
dispatch(state.tr.replaceSelectionWith(hr.create()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const tMenu = new Dropdown(
|
r.typeMenu = new Dropdown(cut([r.makeHead1, r.makeHead2, r.makeHead3, r.typeMenu, r.wrapBlockQuote]), {
|
||||||
[
|
|
||||||
r.makeHead1 as MenuElement,
|
|
||||||
r.makeHead2 as MenuElement,
|
|
||||||
r.makeHead3 as MenuElement,
|
|
||||||
r.typeMenu as MenuElement,
|
|
||||||
r.wrapBlockQuote as MenuElement
|
|
||||||
],
|
|
||||||
{
|
|
||||||
label: 'Тт',
|
label: 'Тт',
|
||||||
// FIXME !!!!!!!!!
|
class: 'editor-dropdown' // TODO: use this class
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// FIXME: icon svg code shouldn't be here
|
||||||
// @ts-ignore
|
// icon: {
|
||||||
icon: {
|
// width: 12,
|
||||||
width: 12,
|
// height: 12,
|
||||||
height: 12,
|
// path: "M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z"
|
||||||
path: 'M6.39999 3.19998V0H20.2666V3.19998H14.9333V15.9999H11.7333V3.19998H6.39999ZM3.19998 8.5334H0V5.33342H9.59994V8.5334H6.39996V16H3.19998V8.5334Z'
|
// }
|
||||||
}
|
}) as MenuItem
|
||||||
}
|
r.blockMenu = []
|
||||||
)
|
r.listMenu = [cut([r.wrapBulletList, r.wrapOrderedList])]
|
||||||
|
r.inlineMenu = [cut([r.toggleStrong, r.toggleEm, r.toggleMark])]
|
||||||
r.typeMenu = tMenu as MenuItem
|
r.fullMenu = r.inlineMenu.concat([cut([r.typeMenu])], r.listMenu)
|
||||||
// r.blockMenu = []
|
|
||||||
r.listMenu = [r.wrapBulletList as MenuItem, r.wrapOrderedList as MenuItem]
|
|
||||||
r.inlineMenu = [r.toggleStrong as MenuItem, r.toggleEm as MenuItem, r.toggleMark as MenuItem]
|
|
||||||
r.fullMenu = [...r.inlineMenu, r.typeMenu, ...r.listMenu]
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
@ -403,8 +347,8 @@ export default (): ProseMirrorExtension => ({
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
...prev,
|
...prev,
|
||||||
menuBar({
|
menuBar({
|
||||||
floating: true,
|
floating: false,
|
||||||
content: buildMenuItems(schema).fullMenu as any[]
|
content: buildMenuItems(schema).fullMenu as any
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,86 +1,74 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin, Transaction } from 'prosemirror-state'
|
||||||
// import { Fragment, Node, Schema } from 'prosemirror-model'
|
import { Fragment, Node, Schema, Slice } from 'prosemirror-model'
|
||||||
import type { Schema } from 'prosemirror-model'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import { createMarkdownParser } from '../../markdown'
|
||||||
// import { createMarkdownParser } from '../markdown'
|
// import { openPrompt } from './prompt'
|
||||||
|
|
||||||
// const URL_REGEX = /(ftp|http|https):\/\/(\w+(?::\w*)?@)?(\S+)(:\d+)?(\/|\/([\w!#%&+./:=?@-]))?/g
|
const URL_REGEX = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:\d+)?(\/|\/([\w!#%&+./:=?@-]))?/g
|
||||||
|
|
||||||
// const transform = (schema: Schema, fragment: Fragment) => {
|
const transform = (schema: Schema, fragment: Fragment) => {
|
||||||
// const nodes: Node[] = []
|
const nodes = []
|
||||||
//
|
fragment.forEach((child: Node) => {
|
||||||
// fragment.forEach((child: Node) => {
|
if (child.isText) {
|
||||||
// if (child.isText) {
|
let pos = 0
|
||||||
// let pos = 0
|
let match: RegExpExecArray
|
||||||
// let match: RegExpMatchArray | null
|
|
||||||
//
|
|
||||||
// while ((match = URL_REGEX.exec(child.text as string)) !== null) {
|
|
||||||
// const start = match.index as number
|
|
||||||
// const end = start + match[0].length
|
|
||||||
// const attrs = { href: match[0] }
|
|
||||||
//
|
|
||||||
// if (start > 0) {
|
|
||||||
// nodes.push(child.cut(pos, start))
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const node = child.cut(start, end).mark(schema.marks.link.create(attrs).addToSet(child.marks))
|
|
||||||
//
|
|
||||||
// nodes.push(node)
|
|
||||||
// pos = end
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (pos < (child.text as string).length) {
|
|
||||||
// nodes.push(child.cut(pos))
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// nodes.push(child.copy(transform(schema, child.content)))
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// return Fragment.fromArray(nodes)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let shiftKey = false
|
while ((match = URL_REGEX.exec(child.text)) !== null) {
|
||||||
|
const start = match.index
|
||||||
|
const end = start + match[0].length
|
||||||
|
const attrs = { href: match[0] }
|
||||||
|
|
||||||
const pasteMarkdown = (_schema: Schema) => {
|
if (start > 0) {
|
||||||
// const parser = createMarkdownParser(schema)
|
nodes.push(child.cut(pos, start))
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = child.cut(start, end).mark(schema.marks.link.create(attrs).addToSet(child.marks))
|
||||||
|
nodes.push(node)
|
||||||
|
pos = end
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < child.text.length) {
|
||||||
|
nodes.push(child.cut(pos))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nodes.push(child.copy(transform(schema, child.content)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Fragment.fromArray(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
let shiftKey = false
|
||||||
|
|
||||||
|
const pasteMarkdown = (schema: Schema) => {
|
||||||
|
const parser = createMarkdownParser(schema)
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
props: {
|
props: {
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keydown: (_, _event) => {
|
keydown: (_, event) => {
|
||||||
// shiftKey = event.shiftKey
|
shiftKey = event.shiftKey
|
||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
keyup: () => {
|
keyup: () => {
|
||||||
// shiftKey = false
|
shiftKey = false
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handlePaste: (view, event) => {
|
handlePaste: (view, event) => {
|
||||||
if (!event.clipboardData) return false
|
if (!event.clipboardData) return false
|
||||||
|
|
||||||
const text = event.clipboardData.getData('text/plain')
|
const text = event.clipboardData.getData('text/plain')
|
||||||
const html = event.clipboardData.getData('text/html')
|
const html = event.clipboardData.getData('text/html')
|
||||||
|
|
||||||
// otherwise, if we have html then fallback to the default HTML
|
// otherwise, if we have html then fallback to the default HTML
|
||||||
// parser behavior that comes with Prosemirror.
|
// parser behavior that comes with Prosemirror.
|
||||||
if (text.length === 0 || html) return false
|
if (text.length === 0 || html) return false
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
const node: Node = parser.parse(text)
|
||||||
|
const fragment = shiftKey ? node.content : transform(schema, node.content)
|
||||||
|
const openStart = 0 // FIXME
|
||||||
|
const openEnd = text.length // FIXME: detect real start and end cursor position
|
||||||
|
const tr: Transaction = view.state.tr.replaceSelection(new Slice(fragment, openStart, openEnd))
|
||||||
|
|
||||||
// const paste = parser.parse(text)
|
view.dispatch(tr)
|
||||||
|
|
||||||
// FIXME !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
// paste is Node why ...paste?
|
|
||||||
// const slice = [...paste]
|
|
||||||
// const fragment = shiftKey ? slice.content : transform(schema, slice.content)
|
|
||||||
// const tr = view.state.tr.replaceSelection(new Slice(fragment, slice.openStart, slice.openEnd))
|
|
||||||
//
|
|
||||||
// view.dispatch(tr)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import { DecorationSet, Decoration } from 'prosemirror-view'
|
import { DecorationSet, Decoration } from 'prosemirror-view'
|
||||||
import { ProseMirrorExtension, isEmpty } from '../state'
|
import { ProseMirrorExtension, isEmpty } from '../helpers'
|
||||||
|
|
||||||
const placeholder = (text: string) =>
|
const placeholder = (text: string) =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
|
@ -8,7 +8,6 @@ const placeholder = (text: string) =>
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
if (isEmpty(state)) {
|
if (isEmpty(state)) {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
div.setAttribute('contenteditable', 'false')
|
div.setAttribute('contenteditable', 'false')
|
||||||
div.classList.add('placeholder')
|
div.classList.add('placeholder')
|
||||||
div.textContent = text
|
div.textContent = text
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
const prefix = 'ProseMirror-prompt'
|
const prefix = 'ProseMirror-prompt'
|
||||||
|
|
||||||
// FIXME !!!
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export function openPrompt(options: any) {
|
export function openPrompt(options) {
|
||||||
const wrapper = document.body.appendChild(document.createElement('div'))
|
const wrapper = document.body.appendChild(document.createElement('div'))
|
||||||
wrapper.className = prefix
|
wrapper.className = prefix
|
||||||
|
|
||||||
const mouseOutside = (e: any) => {
|
const mouseOutside = (ev: MouseEvent) => {
|
||||||
if (!wrapper.contains(e.target)) close()
|
if (!wrapper.contains(ev.target as Node)) close()
|
||||||
}
|
}
|
||||||
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
setTimeout(() => window.addEventListener('mousedown', mouseOutside), 50)
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
@ -15,7 +14,7 @@ export function openPrompt(options: any) {
|
||||||
if (wrapper.parentNode) wrapper.remove()
|
if (wrapper.parentNode) wrapper.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
const domFields: any = []
|
const domFields: HTMLElement[] = []
|
||||||
options.fields.forEach((name) => {
|
options.fields.forEach((name) => {
|
||||||
domFields.push(options.fields[name].render())
|
domFields.push(options.fields[name].render())
|
||||||
})
|
})
|
||||||
|
@ -34,14 +33,14 @@ export function openPrompt(options: any) {
|
||||||
if (options.title) {
|
if (options.title) {
|
||||||
form.appendChild(document.createElement('h5')).textContent = options.title
|
form.appendChild(document.createElement('h5')).textContent = options.title
|
||||||
}
|
}
|
||||||
domFields.forEach((field: any) => {
|
domFields.forEach((field: HTMLElement) => {
|
||||||
form.appendChild(document.createElement('div')).append(field)
|
form.appendChild(document.createElement('div')).appendChild(field)
|
||||||
})
|
})
|
||||||
const buttons = form.appendChild(document.createElement('div'))
|
const buttons = form.appendChild(document.createElement('div'))
|
||||||
buttons.className = prefix + '-buttons'
|
buttons.className = prefix + '-buttons'
|
||||||
buttons.append(submitButton)
|
buttons.appendChild(submitButton)
|
||||||
buttons.append(document.createTextNode(' '))
|
buttons.appendChild(document.createTextNode(' '))
|
||||||
buttons.append(cancelButton)
|
buttons.appendChild(cancelButton)
|
||||||
|
|
||||||
const box = wrapper.getBoundingClientRect()
|
const box = wrapper.getBoundingClientRect()
|
||||||
wrapper.style.top = (window.innerHeight - box.height) / 2 + 'px'
|
wrapper.style.top = (window.innerHeight - box.height) / 2 + 'px'
|
||||||
|
@ -74,11 +73,11 @@ export function openPrompt(options: any) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const input: any = form.elements[0]
|
const input = form.elements[0] as HTMLInputElement
|
||||||
if (input) input.focus()
|
if (input) input.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValues(fields: any, domFields: any) {
|
function getValues(fields: any, domFields: HTMLElement[]) {
|
||||||
const result = Object.create(null)
|
const result = Object.create(null)
|
||||||
let i = 0
|
let i = 0
|
||||||
fields.forEarch((name) => {
|
fields.forEarch((name) => {
|
||||||
|
@ -95,14 +94,13 @@ function getValues(fields: any, domFields: any) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function reportInvalid(dom: any, message: any) {
|
function reportInvalid(dom: HTMLElement, message: string) {
|
||||||
const parent = dom.parentNode
|
const msg: HTMLElement = dom.parentNode.appendChild(document.createElement('div'))
|
||||||
const msg = parent.appendChild(document.createElement('div'))
|
|
||||||
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
msg.style.left = dom.offsetLeft + dom.offsetWidth + 2 + 'px'
|
||||||
msg.style.top = dom.offsetTop - 5 + 'px'
|
msg.style.top = dom.offsetTop - 5 + 'px'
|
||||||
msg.className = 'ProseMirror-invalid'
|
msg.className = 'ProseMirror-invalid'
|
||||||
msg.textContent = message
|
msg.textContent = message
|
||||||
setTimeout(() => msg.remove(), 1500)
|
setTimeout(msg.remove, 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Field {
|
export class Field {
|
||||||
|
@ -142,3 +140,16 @@ export class TextField extends Field {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SelectField extends Field {
|
||||||
|
render() {
|
||||||
|
const select = document.createElement('select')
|
||||||
|
this.options.options.forEach((o: { value: string; label: string }) => {
|
||||||
|
const opt = select.appendChild(document.createElement('option'))
|
||||||
|
opt.value = o.value
|
||||||
|
opt.selected = o.value === this.options.value
|
||||||
|
opt.label = o.label
|
||||||
|
})
|
||||||
|
return select
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
|
||||||
const scroll = (view: EditorView) => {
|
const scroll = (view: EditorView) => {
|
||||||
if (!view.state.selection.empty) return false
|
if (!view.state.selection.empty) return false
|
||||||
|
|
|
@ -1,50 +1,39 @@
|
||||||
import { /*MenuItem,*/ renderGrouped } from 'prosemirror-menu'
|
import { renderGrouped } from 'prosemirror-menu'
|
||||||
import type { Schema } from 'prosemirror-model'
|
|
||||||
import { Plugin } from 'prosemirror-state'
|
import { Plugin } from 'prosemirror-state'
|
||||||
// import { EditorView } from 'prosemirror-view'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
|
||||||
import { buildMenuItems } from './menu'
|
import { buildMenuItems } from './menu'
|
||||||
|
|
||||||
const cut = (arr: any[] | any) => arr.filter((a: any) => !!a)
|
|
||||||
|
|
||||||
export class SelectionTooltip {
|
export class SelectionTooltip {
|
||||||
tooltip: any
|
tooltip: any
|
||||||
|
|
||||||
constructor(view: any, schema: Schema) {
|
constructor(view: any, schema: any) {
|
||||||
this.tooltip = document.createElement('div')
|
this.tooltip = document.createElement('div')
|
||||||
this.tooltip.className = 'tooltip'
|
this.tooltip.className = 'tooltip'
|
||||||
view.dom.parentNode.append(this.tooltip)
|
view.dom.parentNode.appendChild(this.tooltip)
|
||||||
const content = cut((buildMenuItems(schema) as { [key: string]: any })?.fullMenu)
|
const { dom } = renderGrouped(view, buildMenuItems(schema).fullMenu as any)
|
||||||
|
this.tooltip.appendChild(dom)
|
||||||
console.debug(content)
|
|
||||||
const { dom } = renderGrouped(view, content)
|
|
||||||
|
|
||||||
this.tooltip.append(dom)
|
|
||||||
this.update(view, null)
|
this.update(view, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(view: any, lastState: any) {
|
update(view: any, lastState: any) {
|
||||||
const state = view.state
|
const state = view.state
|
||||||
|
|
||||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selection.empty) {
|
if (state.selection.empty) {
|
||||||
this.tooltip.style.display = 'none'
|
this.tooltip.style.display = 'none'
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltip.style.display = ''
|
this.tooltip.style.display = ''
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection
|
||||||
const start = view.coordsAtPos(from)
|
const start = view.coordsAtPos(from),
|
||||||
const end = view.coordsAtPos(to)
|
end = view.coordsAtPos(to)
|
||||||
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
const box = this.tooltip.offsetParent.getBoundingClientRect()
|
||||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||||
|
this.tooltip.style.left = left - box.left + 'px'
|
||||||
this.tooltip.style.left = `${left - box.left}px`
|
this.tooltip.style.bottom = box.bottom - (start.top + 15) + 'px'
|
||||||
this.tooltip.style.bottom = `${box.bottom - (start.top + 15)}px`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { inputRules } from 'prosemirror-inputrules'
|
import { inputRules } from 'prosemirror-inputrules'
|
||||||
import type { MarkType } from 'prosemirror-model'
|
import type { MarkSpec, MarkType } from 'prosemirror-model'
|
||||||
import { markInputRule } from './mark-input-rule'
|
import { markInputRule } from './mark-input-rule'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
import type OrderedMap from 'orderedmap'
|
||||||
|
|
||||||
const strikethroughRule = (nodeType: MarkType) => markInputRule(/~{2}(.+)~{2}$/, nodeType)
|
const strikethroughRule = (nodeType: MarkType) => markInputRule(/~{2}(.+)~{2}$/, nodeType)
|
||||||
|
|
||||||
|
@ -10,12 +11,12 @@ const strikethroughSchema = {
|
||||||
parseDOM: [{ tag: 'del' }],
|
parseDOM: [{ tag: 'del' }],
|
||||||
toDOM: () => ['del']
|
toDOM: () => ['del']
|
||||||
}
|
}
|
||||||
}
|
} as MarkSpec
|
||||||
|
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
marks: (prev.marks as any).append(strikethroughSchema)
|
marks: (prev.marks as OrderedMap<MarkSpec>).append(strikethroughSchema)
|
||||||
}),
|
}),
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
...prev,
|
...prev,
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { EditorState, Selection } from 'prosemirror-state'
|
import { EditorState, Selection } from 'prosemirror-state'
|
||||||
import type { Node, Schema, ResolvedPos } from 'prosemirror-model'
|
import type { Node, Schema, ResolvedPos, NodeSpec } from 'prosemirror-model'
|
||||||
import { InputRule, inputRules } from 'prosemirror-inputrules'
|
import { InputRule, inputRules } from 'prosemirror-inputrules'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { ProseMirrorExtension } from '../helpers'
|
||||||
|
import type OrderedMap from 'orderedmap'
|
||||||
|
|
||||||
export const tableInputRule = (schema: Schema) =>
|
export const tableInputRule = (schema: Schema) =>
|
||||||
new InputRule(
|
new InputRule(
|
||||||
new RegExp('^\\|{2,}\\s$'),
|
new RegExp('^\\|{2,}\\s$'),
|
||||||
(state: EditorState, match: string[], start: number, end: number) => {
|
(state: EditorState, match: string[], start: number, end: number) => {
|
||||||
const tr = state.tr
|
const tr = state.tr
|
||||||
const columns = [...Array.from({ length: match[0].trim().length - 1 })]
|
const columns = Array.from({ length: match[0].trim().length - 1 })
|
||||||
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
||||||
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
||||||
const table = schema.node(schema.nodes.table, {}, [
|
const table = schema.node(schema.nodes.table, {}, [
|
||||||
|
@ -95,7 +96,7 @@ const tableSchema = {
|
||||||
],
|
],
|
||||||
toDOM: (node: Node) => ['th', node.attrs, 0]
|
toDOM: (node: Node) => ['th', node.attrs, 0]
|
||||||
}
|
}
|
||||||
}
|
} as NodeSpec
|
||||||
|
|
||||||
const findParentPos = ($pos: ResolvedPos, fn: (n: Node) => boolean) => {
|
const findParentPos = ($pos: ResolvedPos, fn: (n: Node) => boolean) => {
|
||||||
for (let d = $pos.depth; d > 0; d--) {
|
for (let d = $pos.depth; d > 0; d--) {
|
||||||
|
@ -174,9 +175,8 @@ const getTextSize = (n: Node) => {
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
nodes: (prev.nodes as any).append(tableSchema)
|
nodes: (prev.nodes as OrderedMap<NodeSpec>).append(tableSchema)
|
||||||
}),
|
}),
|
||||||
// FIXME (extract functions)
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
keymap({
|
keymap({
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { DOMSerializer, Node as ProsemirrorNode, NodeType, Schema } from 'prosemirror-model'
|
import {
|
||||||
|
DOMOutputSpec,
|
||||||
|
DOMSerializer,
|
||||||
|
Node as ProsemirrorNode,
|
||||||
|
NodeSpec,
|
||||||
|
NodeType,
|
||||||
|
Schema
|
||||||
|
} from 'prosemirror-model'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
import { wrappingInputRule, inputRules } from 'prosemirror-inputrules'
|
import { wrappingInputRule, inputRules } from 'prosemirror-inputrules'
|
||||||
import { splitListItem } from 'prosemirror-schema-list'
|
import { splitListItem } from 'prosemirror-schema-list'
|
||||||
import { keymap } from 'prosemirror-keymap'
|
import { keymap } from 'prosemirror-keymap'
|
||||||
import type { ProseMirrorExtension } from '../state'
|
import type { NodeViewFn, ProseMirrorExtension } from '../helpers'
|
||||||
|
import type OrderedMap from 'orderedmap'
|
||||||
|
|
||||||
const todoListRule = (nodeType: NodeType) =>
|
const todoListRule = (nodeType: NodeType) =>
|
||||||
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
wrappingInputRule(new RegExp('^\\[( |x)]\\s$'), nodeType, (match) => ({
|
||||||
|
@ -44,22 +52,22 @@ const todoListSchema = {
|
||||||
['div', 0]
|
['div', 0]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
} as NodeSpec
|
||||||
|
|
||||||
class TodoItemView {
|
class TodoItemView {
|
||||||
contentDOM: HTMLElement
|
contentDOM: Node
|
||||||
dom: Node
|
dom: Node
|
||||||
view: EditorView
|
view: EditorView
|
||||||
getPos: () => number
|
getPos: () => number
|
||||||
|
|
||||||
constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {
|
constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number) {
|
||||||
const dom = node.type.spec.toDOM(node)
|
const dom: DOMOutputSpec = node.type.spec.toDOM(node)
|
||||||
const res = DOMSerializer.renderSpec(document, dom)
|
const res = DOMSerializer.renderSpec(document, dom)
|
||||||
this.dom = res.dom
|
this.dom = res.dom
|
||||||
this.contentDOM = res.contentDOM
|
this.contentDOM = res.contentDOM
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos
|
this.getPos = getPos
|
||||||
;(this.dom as Element).querySelector('input').addEventListener('click', this.handleClick.bind(this))
|
;(this.dom as HTMLElement).querySelector('input').addEventListener('click', this.handleClick.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(e: MouseEvent) {
|
handleClick(e: MouseEvent) {
|
||||||
|
@ -78,7 +86,7 @@ const todoListKeymap = (schema: Schema) => ({
|
||||||
export default (): ProseMirrorExtension => ({
|
export default (): ProseMirrorExtension => ({
|
||||||
schema: (prev) => ({
|
schema: (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
nodes: (prev.nodes as any).append(todoListSchema)
|
nodes: (prev.nodes as OrderedMap<NodeSpec>).append(todoListSchema)
|
||||||
}),
|
}),
|
||||||
plugins: (prev, schema) => [
|
plugins: (prev, schema) => [
|
||||||
keymap(todoListKeymap(schema)),
|
keymap(todoListKeymap(schema)),
|
||||||
|
@ -86,8 +94,8 @@ export default (): ProseMirrorExtension => ({
|
||||||
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
inputRules({ rules: [todoListRule(schema.nodes.todo_item)] })
|
||||||
],
|
],
|
||||||
nodeViews: {
|
nodeViews: {
|
||||||
todo_item: (node, view, getPos) => {
|
todo_item: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => {
|
||||||
return new TodoItemView(node, view, getPos)
|
return new TodoItemView(node, view, getPos)
|
||||||
}
|
}
|
||||||
}
|
} as unknown as { [key: string]: NodeViewFn }
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,13 +2,6 @@ import { Plugin, EditorState } from 'prosemirror-state'
|
||||||
import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
import type { Node, Schema, SchemaSpec } from 'prosemirror-model'
|
||||||
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
import type { Decoration, EditorView, NodeView } from 'prosemirror-view'
|
||||||
|
|
||||||
export type NodeViewFn = (
|
|
||||||
node: Node,
|
|
||||||
view: EditorView,
|
|
||||||
getPos: () => number,
|
|
||||||
decorations: Decoration[]
|
|
||||||
) => NodeView
|
|
||||||
|
|
||||||
export interface ProseMirrorExtension {
|
export interface ProseMirrorExtension {
|
||||||
schema?: (prev: SchemaSpec) => SchemaSpec
|
schema?: (prev: SchemaSpec) => SchemaSpec
|
||||||
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
|
plugins?: (prev: Plugin[], schema: Schema) => Plugin[]
|
||||||
|
@ -17,9 +10,16 @@ export interface ProseMirrorExtension {
|
||||||
|
|
||||||
export type ProseMirrorState = EditorState | unknown
|
export type ProseMirrorState = EditorState | unknown
|
||||||
|
|
||||||
export const isInitialized = (state: any) => state !== undefined && state instanceof EditorState
|
export type NodeViewFn = (
|
||||||
|
node: Node,
|
||||||
|
view: EditorView,
|
||||||
|
getPos: () => number,
|
||||||
|
decorations: Decoration[]
|
||||||
|
) => NodeView
|
||||||
|
|
||||||
export const isEmpty = (state: any) =>
|
export const isInitialized = (state) => state !== undefined && state instanceof EditorState
|
||||||
|
|
||||||
|
export const isEmpty = (state) =>
|
||||||
!isInitialized(state) ||
|
!isInitialized(state) ||
|
||||||
(state.doc.childCount === 1 &&
|
(state.doc.childCount === 1 &&
|
||||||
!state.doc.firstChild.type.spec.code &&
|
!state.doc.firstChild.type.spec.code &&
|
50
src/components/Editor/prosemirror/p2p.ts
Normal file
50
src/components/Editor/prosemirror/p2p.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { uniqueNamesGenerator, adjectives, animals } from 'unique-names-generator'
|
||||||
|
import { Awareness } from 'y-protocols/awareness'
|
||||||
|
import { WebrtcProvider } from 'y-webrtc'
|
||||||
|
import { Doc, XmlFragment } from 'yjs'
|
||||||
|
// import type { Reaction } from '../../../graphql/types.gen'
|
||||||
|
// import { setReactions } from '../../../stores/editor'
|
||||||
|
|
||||||
|
export const roomConnect = (
|
||||||
|
room: string,
|
||||||
|
username = '',
|
||||||
|
keyname = 'collab'
|
||||||
|
): [XmlFragment, WebrtcProvider] => {
|
||||||
|
const ydoc = new Doc()
|
||||||
|
// const yarr = ydoc.getArray(keyname + '-reactions')
|
||||||
|
// TODO: use reactions
|
||||||
|
// yarr.observeDeep(() => {
|
||||||
|
// console.debug('[p2p] yarray updated', yarr.toArray())
|
||||||
|
// setReactions(yarr.toArray() as Reaction[])
|
||||||
|
// })
|
||||||
|
const yXmlFragment = ydoc.getXmlFragment(keyname)
|
||||||
|
const webrtcOptions = {
|
||||||
|
awareness: new Awareness(ydoc),
|
||||||
|
filterBcConns: true,
|
||||||
|
maxConns: 33,
|
||||||
|
signaling: [
|
||||||
|
// 'wss://signaling.discours.io',
|
||||||
|
// 'wss://stun.l.google.com:19302',
|
||||||
|
'wss://y-webrtc-signaling-eu.herokuapp.com',
|
||||||
|
'wss://signaling.yjs.dev'
|
||||||
|
],
|
||||||
|
peerOpts: {},
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
// connect with provider
|
||||||
|
const provider = new WebrtcProvider(room, ydoc, webrtcOptions)
|
||||||
|
console.debug('[p2p] provider', provider)
|
||||||
|
// setting username
|
||||||
|
provider.awareness.setLocalStateField('user', {
|
||||||
|
name:
|
||||||
|
username ??
|
||||||
|
uniqueNamesGenerator({
|
||||||
|
dictionaries: [adjectives, animals],
|
||||||
|
style: 'capital',
|
||||||
|
separator: ' ',
|
||||||
|
length: 2
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return [yXmlFragment, provider]
|
||||||
|
}
|
|
@ -1,63 +1,39 @@
|
||||||
import { keymap } from 'prosemirror-keymap'
|
|
||||||
import type { ProseMirrorExtension } from './state'
|
|
||||||
import { Schema } from 'prosemirror-model'
|
|
||||||
import base from './extension/base'
|
|
||||||
import markdown from './extension/markdown'
|
|
||||||
import link from './extension/link'
|
|
||||||
// import scroll from './prosemirror/extension/scroll'
|
|
||||||
import todoList from './extension/todo-list'
|
|
||||||
import code from './extension/code'
|
|
||||||
import strikethrough from './extension/strikethrough'
|
|
||||||
import placeholder from './extension/placeholder'
|
|
||||||
// import menu from './extension/menu'
|
// import menu from './extension/menu'
|
||||||
// import image from './extension/image'
|
// import scroll from './prosemirror/extension/scroll'
|
||||||
|
import { keymap } from 'prosemirror-keymap'
|
||||||
|
import type { ProseMirrorExtension } from './helpers'
|
||||||
|
import { Schema } from 'prosemirror-model'
|
||||||
|
import { t } from '../../../utils/intl'
|
||||||
|
import base from './extension/base'
|
||||||
|
import code from './extension/code'
|
||||||
import dragHandle from './extension/drag-handle'
|
import dragHandle from './extension/drag-handle'
|
||||||
|
import image from './extension/image'
|
||||||
|
import link from './extension/link'
|
||||||
|
import markdown from './extension/markdown'
|
||||||
import pasteMarkdown from './extension/paste-markdown'
|
import pasteMarkdown from './extension/paste-markdown'
|
||||||
import table from './extension/table'
|
import table from './extension/table'
|
||||||
import collab from './extension/collab'
|
import collab from './extension/collab'
|
||||||
import type { Config, YOptions } from '../store'
|
import type { Collab, Config, ExtensionsProps, YOptions } from '../store/context'
|
||||||
import selectionMenu from './extension/selection'
|
import selectionMenu from './extension/selection'
|
||||||
|
import placeholder from './extension/placeholder'
|
||||||
|
import todoList from './extension/todo-list'
|
||||||
|
import strikethrough from './extension/strikethrough'
|
||||||
|
import scrollPlugin from './extension/scroll'
|
||||||
|
|
||||||
interface Opts {
|
const customKeymap = (props: ExtensionsProps): ProseMirrorExtension => ({
|
||||||
data?: unknown
|
plugins: (prev) => (props.keymap ? [...prev, keymap(props.keymap)] : prev)
|
||||||
keymap?: any
|
|
||||||
config: Config
|
|
||||||
markdown: boolean
|
|
||||||
path?: string
|
|
||||||
y?: YOptions
|
|
||||||
schema?: Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
const customKeymap = (opts: Opts): ProseMirrorExtension => ({
|
|
||||||
plugins: (prev) => (opts.keymap ? [...prev, keymap(opts.keymap)] : prev)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
export const createExtensions = (props: ExtensionsProps): ProseMirrorExtension[] => {
|
||||||
const codeMirrorKeymap = (props: Props) => {
|
const eee = [
|
||||||
const keys = []
|
placeholder(t('Just start typing...')),
|
||||||
for (const key in props.keymap) {
|
customKeymap(props),
|
||||||
keys.push({key: key, run: props.keymap[key]})
|
base(props.markdown),
|
||||||
}
|
|
||||||
|
|
||||||
return cmKeymap.of(keys)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const createExtensions = (opts: Opts): ProseMirrorExtension[] => {
|
|
||||||
return opts.markdown
|
|
||||||
? [
|
|
||||||
placeholder('Просто начните...'),
|
|
||||||
customKeymap(opts),
|
|
||||||
base(opts.markdown),
|
|
||||||
// scroll(props.config.typewriterMode),
|
|
||||||
collab(opts.y),
|
|
||||||
dragHandle()
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
selectionMenu(),
|
selectionMenu(),
|
||||||
customKeymap(opts),
|
scrollPlugin(props.config?.typewriterMode)
|
||||||
base(opts.markdown),
|
]
|
||||||
collab(opts.y),
|
if (props.markdown) {
|
||||||
|
eee.push(
|
||||||
markdown(),
|
markdown(),
|
||||||
todoList(),
|
todoList(),
|
||||||
dragHandle(),
|
dragHandle(),
|
||||||
|
@ -65,7 +41,7 @@ export const createExtensions = (opts: Opts): ProseMirrorExtension[] => {
|
||||||
strikethrough(),
|
strikethrough(),
|
||||||
link(),
|
link(),
|
||||||
table(),
|
table(),
|
||||||
// image(props.path), // TODO: image extension
|
image(props.path),
|
||||||
pasteMarkdown()
|
pasteMarkdown()
|
||||||
/*
|
/*
|
||||||
codeBlock({
|
codeBlock({
|
||||||
|
@ -76,7 +52,10 @@ export const createExtensions = (opts: Opts): ProseMirrorExtension[] => {
|
||||||
extensions: () => [codeMirrorKeymap(props)],
|
extensions: () => [codeMirrorKeymap(props)],
|
||||||
}),
|
}),
|
||||||
*/
|
*/
|
||||||
]
|
)
|
||||||
|
}
|
||||||
|
if (props.collab?.room) eee.push(collab(props.y))
|
||||||
|
return eee
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createEmptyText = () => ({
|
export const createEmptyText = () => ({
|
||||||
|
@ -91,11 +70,16 @@ export const createEmptyText = () => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createSchema = (opts: Opts) => {
|
export const createSchema = (props: ExtensionsProps) => {
|
||||||
const extensions = createExtensions(opts)
|
const extensions = createExtensions({
|
||||||
|
config: props.config,
|
||||||
|
markdown: props.markdown,
|
||||||
|
path: props.path,
|
||||||
|
keymap: props.keymap,
|
||||||
|
y: props.y
|
||||||
|
})
|
||||||
|
|
||||||
let schemaSpec = { nodes: {} }
|
let schemaSpec = { nodes: {} }
|
||||||
|
|
||||||
for (const extension of extensions) {
|
for (const extension of extensions) {
|
||||||
if (extension.schema) {
|
if (extension.schema) {
|
||||||
schemaSpec = extension.schema(schemaSpec)
|
schemaSpec = extension.schema(schemaSpec)
|
||||||
|
|
11
src/components/Editor/remote.ts
Normal file
11
src/components/Editor/remote.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import type { EditorState } from 'prosemirror-state'
|
||||||
|
import { serialize } from './markdown'
|
||||||
|
|
||||||
|
export const copy = async (text: string): Promise<void> => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const copyAllAsMarkdown = async (state: EditorState): Promise<void> => {
|
||||||
|
const text = serialize(state)
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
}
|
404
src/components/Editor/store/actions.ts
Normal file
404
src/components/Editor/store/actions.ts
Normal file
|
@ -0,0 +1,404 @@
|
||||||
|
import { Store, createStore, unwrap } from 'solid-js/store'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import type { EditorState } from 'prosemirror-state'
|
||||||
|
import { undo, redo } from 'prosemirror-history'
|
||||||
|
import { selectAll, deleteSelection } from 'prosemirror-commands'
|
||||||
|
import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
|
||||||
|
import debounce from 'lodash/debounce'
|
||||||
|
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
||||||
|
import { State, Draft, Config, ServiceError, newState, ExtensionsProps, EditorActions } from './context'
|
||||||
|
import { mod } from '../env'
|
||||||
|
import { serialize, createMarkdownParser } from '../markdown'
|
||||||
|
import db from '../db'
|
||||||
|
import { isEmpty, isInitialized } from '../prosemirror/helpers'
|
||||||
|
|
||||||
|
const isText = (x) => x && x.doc && x.selection
|
||||||
|
const isState = (x) => typeof x.lastModified !== 'string' && Array.isArray(x.drafts || [])
|
||||||
|
const isDraft = (x): boolean => x && (x.text || x.path)
|
||||||
|
|
||||||
|
export const createCtrl = (initial: State): [Store<State>, EditorActions] => {
|
||||||
|
const [store, setState] = createStore(initial)
|
||||||
|
|
||||||
|
const onUndo = () => {
|
||||||
|
if (!isInitialized(store.text)) return false
|
||||||
|
const text = store.text as EditorState
|
||||||
|
if (store.collab?.started) {
|
||||||
|
yUndo(text)
|
||||||
|
} else {
|
||||||
|
undo(text, store.editorView.dispatch)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRedo = () => {
|
||||||
|
if (!isInitialized(store.text)) return false
|
||||||
|
const text = store.text as EditorState
|
||||||
|
if (store.collab?.started) {
|
||||||
|
yRedo(text)
|
||||||
|
} else {
|
||||||
|
redo(text, store.editorView.dispatch)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const discard = () => {
|
||||||
|
if (store.path) {
|
||||||
|
discardText()
|
||||||
|
} else if (store.drafts.length > 0 && isEmpty(store.text)) {
|
||||||
|
discardText()
|
||||||
|
} else {
|
||||||
|
selectAll(store.editorView.state, store.editorView.dispatch)
|
||||||
|
deleteSelection(store.editorView.state, store.editorView.dispatch)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMarkdown = () => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
const editorState = store.text as EditorState
|
||||||
|
const markdown = !state.markdown
|
||||||
|
const selection = { type: 'text', anchor: 1, head: 1 }
|
||||||
|
let doc
|
||||||
|
|
||||||
|
if (markdown) {
|
||||||
|
const lines = serialize(editorState).split('\n')
|
||||||
|
const nodes = lines.map((text) =>
|
||||||
|
text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
|
||||||
|
)
|
||||||
|
|
||||||
|
doc = { type: 'doc', content: nodes }
|
||||||
|
} else {
|
||||||
|
const schema = createSchema({
|
||||||
|
config: state.config,
|
||||||
|
path: state.path,
|
||||||
|
y: state.collab?.y,
|
||||||
|
markdown,
|
||||||
|
keymap
|
||||||
|
})
|
||||||
|
|
||||||
|
const parser = createMarkdownParser(schema)
|
||||||
|
let textContent = ''
|
||||||
|
editorState.doc.forEach((node) => {
|
||||||
|
textContent += `${node.textContent}\n`
|
||||||
|
})
|
||||||
|
const text = parser.parse(textContent)
|
||||||
|
doc = text.toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: state.config,
|
||||||
|
markdown,
|
||||||
|
path: state.path,
|
||||||
|
keymap,
|
||||||
|
y: state.collab?.y
|
||||||
|
})
|
||||||
|
|
||||||
|
setState({
|
||||||
|
text: { selection, doc },
|
||||||
|
extensions,
|
||||||
|
markdown
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const keymap = {
|
||||||
|
[`${mod}-w`]: discard,
|
||||||
|
[`${mod}-z`]: onUndo,
|
||||||
|
[`Shift-${mod}-z`]: onRedo,
|
||||||
|
[`${mod}-y`]: onRedo,
|
||||||
|
[`${mod}-m`]: toggleMarkdown
|
||||||
|
} as ExtensionsProps['keymap']
|
||||||
|
|
||||||
|
const createTextFromDraft = async (draft: Draft) => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: state.config,
|
||||||
|
markdown: draft.markdown,
|
||||||
|
path: draft.path,
|
||||||
|
keymap
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: draft.text,
|
||||||
|
extensions,
|
||||||
|
lastModified: draft.lastModified ? new Date(draft.lastModified) : undefined,
|
||||||
|
path: draft.path,
|
||||||
|
markdown: draft.markdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToDrafts = (drafts: Draft[], prev: State) => {
|
||||||
|
const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
|
||||||
|
return [
|
||||||
|
...drafts,
|
||||||
|
{
|
||||||
|
text,
|
||||||
|
lastModified: prev.lastModified,
|
||||||
|
path: prev.path,
|
||||||
|
markdown: prev.markdown
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const discardText = async () => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
const index = state.drafts.length - 1
|
||||||
|
const draft = index !== -1 ? state.drafts[index] : undefined
|
||||||
|
|
||||||
|
let next: Partial<State>
|
||||||
|
if (draft) {
|
||||||
|
next = await createTextFromDraft(draft)
|
||||||
|
} else {
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: state.config ?? store.config,
|
||||||
|
markdown: state.markdown ?? store.markdown,
|
||||||
|
keymap
|
||||||
|
})
|
||||||
|
|
||||||
|
next = {
|
||||||
|
text: createEmptyText(),
|
||||||
|
extensions,
|
||||||
|
lastModified: undefined,
|
||||||
|
path: undefined,
|
||||||
|
markdown: state.markdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drafts = state.drafts.filter((f: Draft) => f !== draft)
|
||||||
|
|
||||||
|
setState({
|
||||||
|
drafts,
|
||||||
|
...next,
|
||||||
|
collab: draft ? undefined : state.collab,
|
||||||
|
error: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async (): Promise<State> => {
|
||||||
|
const state: State = unwrap(store)
|
||||||
|
const room = window.location.pathname?.slice(1).trim()
|
||||||
|
const args = { room: room ?? undefined }
|
||||||
|
const data = await db.get('state')
|
||||||
|
let parsed: State
|
||||||
|
if (data !== undefined) {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw new ServiceError('invalid_state', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
return { ...state, args }
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = state.text
|
||||||
|
if (parsed.text) {
|
||||||
|
if (!isText(parsed.text)) {
|
||||||
|
throw new ServiceError('invalid_state', parsed.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
text = parsed.text
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = createExtensions({
|
||||||
|
path: parsed.path,
|
||||||
|
markdown: parsed.markdown,
|
||||||
|
keymap,
|
||||||
|
config: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const newst = {
|
||||||
|
...parsed,
|
||||||
|
text,
|
||||||
|
extensions,
|
||||||
|
// config,
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newst.lastModified) {
|
||||||
|
newst.lastModified = new Date(newst.lastModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const draft of parsed.drafts || []) {
|
||||||
|
if (!isDraft(draft)) {
|
||||||
|
throw new ServiceError('invalid_draft', draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isState(newst)) {
|
||||||
|
throw new ServiceError('invalid_state', newst)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newst
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTheme = (state: State) => ({ theme: state.config.theme })
|
||||||
|
|
||||||
|
const clean = () => {
|
||||||
|
setState({
|
||||||
|
...newState(),
|
||||||
|
loading: 'initialized',
|
||||||
|
drafts: [],
|
||||||
|
fullscreen: store.fullscreen,
|
||||||
|
lastModified: new Date(),
|
||||||
|
error: undefined,
|
||||||
|
text: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
let data = await fetchData()
|
||||||
|
try {
|
||||||
|
if (data.args.room) {
|
||||||
|
data = await doStartCollab(data)
|
||||||
|
} else if (!data.text) {
|
||||||
|
const text = createEmptyText()
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: data.config ?? store.config,
|
||||||
|
markdown: data.markdown ?? store.markdown,
|
||||||
|
keymap
|
||||||
|
})
|
||||||
|
data = { ...data, text, extensions }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
data = { ...data, error: error.errorObject }
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({
|
||||||
|
...data,
|
||||||
|
config: { ...data.config, ...getTheme(data) },
|
||||||
|
loading: 'initialized'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveState = () =>
|
||||||
|
debounce(async (state: State) => {
|
||||||
|
const data: State = {
|
||||||
|
lastModified: state.lastModified,
|
||||||
|
drafts: state.drafts,
|
||||||
|
config: state.config,
|
||||||
|
path: state.path,
|
||||||
|
markdown: state.markdown,
|
||||||
|
collab: {
|
||||||
|
room: state.collab?.room
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitialized(state.text)) {
|
||||||
|
data.text = store.editorView.state.toJSON()
|
||||||
|
} else if (state.text) {
|
||||||
|
data.text = state.text
|
||||||
|
}
|
||||||
|
|
||||||
|
db.set('state', JSON.stringify(data))
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
const setFullscreen = (fullscreen: boolean) => {
|
||||||
|
setState({ fullscreen })
|
||||||
|
}
|
||||||
|
|
||||||
|
const startCollab = async () => {
|
||||||
|
const state: State = unwrap(store)
|
||||||
|
const update = await doStartCollab(state)
|
||||||
|
setState(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
const doStartCollab = async (state: State): Promise<State> => {
|
||||||
|
const backup = state.args?.room && state.collab?.room !== state.args.room
|
||||||
|
const room = state.args?.room ?? uuidv4()
|
||||||
|
window.history.replaceState(null, '', `/${room}`)
|
||||||
|
|
||||||
|
const { roomConnect } = await import('../prosemirror/p2p')
|
||||||
|
const [type, provider] = roomConnect(room)
|
||||||
|
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: state.config,
|
||||||
|
markdown: state.markdown,
|
||||||
|
path: state.path,
|
||||||
|
keymap,
|
||||||
|
y: { type, provider }
|
||||||
|
})
|
||||||
|
|
||||||
|
let newst = state
|
||||||
|
if ((backup && !isEmpty(state.text)) || state.path) {
|
||||||
|
let drafts = state.drafts
|
||||||
|
if (!state.error) {
|
||||||
|
drafts = addToDrafts(drafts, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
newst = {
|
||||||
|
...state,
|
||||||
|
drafts,
|
||||||
|
lastModified: undefined,
|
||||||
|
path: undefined,
|
||||||
|
error: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...newst,
|
||||||
|
extensions,
|
||||||
|
collab: { started: true, room, y: { type, provider } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopCollab = (state: State) => {
|
||||||
|
state.collab.y?.provider.destroy()
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: state.config,
|
||||||
|
markdown: state.markdown,
|
||||||
|
path: state.path,
|
||||||
|
keymap
|
||||||
|
})
|
||||||
|
|
||||||
|
setState({ collab: undefined, extensions })
|
||||||
|
window.history.replaceState(null, '', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateConfig = (config: Partial<Config>) => {
|
||||||
|
const state = unwrap(store)
|
||||||
|
const extensions = createExtensions({
|
||||||
|
config: { ...state.config, ...config },
|
||||||
|
markdown: state.markdown,
|
||||||
|
path: state.path,
|
||||||
|
keymap,
|
||||||
|
y: state.collab?.y
|
||||||
|
})
|
||||||
|
|
||||||
|
setState({
|
||||||
|
config: { ...state.config, ...config },
|
||||||
|
extensions,
|
||||||
|
lastModified: new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePath = (path: string) => {
|
||||||
|
setState({ path, lastModified: new Date() })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTheme = () => {
|
||||||
|
const { theme } = getTheme(unwrap(store))
|
||||||
|
setState('config', { theme })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctrl = {
|
||||||
|
clean,
|
||||||
|
discard,
|
||||||
|
getTheme,
|
||||||
|
init,
|
||||||
|
saveState,
|
||||||
|
setFullscreen,
|
||||||
|
setState,
|
||||||
|
startCollab,
|
||||||
|
stopCollab,
|
||||||
|
toggleMarkdown,
|
||||||
|
updateConfig,
|
||||||
|
updatePath,
|
||||||
|
updateTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return [store, ctrl]
|
||||||
|
}
|
|
@ -2,18 +2,25 @@ import { createContext, useContext } from 'solid-js'
|
||||||
import type { Store } from 'solid-js/store'
|
import type { Store } from 'solid-js/store'
|
||||||
import type { XmlFragment } from 'yjs'
|
import type { XmlFragment } from 'yjs'
|
||||||
import type { WebrtcProvider } from 'y-webrtc'
|
import type { WebrtcProvider } from 'y-webrtc'
|
||||||
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/state'
|
import type { ProseMirrorExtension, ProseMirrorState } from '../prosemirror/helpers'
|
||||||
import type { Reaction } from '../../../graphql/types.gen'
|
import type { Command, EditorState } from 'prosemirror-state'
|
||||||
import type { EditorView } from 'prosemirror-view'
|
import type { EditorView } from 'prosemirror-view'
|
||||||
|
import type { Schema } from 'prosemirror-model'
|
||||||
|
|
||||||
export const isMac = true
|
export interface ExtensionsProps {
|
||||||
|
data?: unknown
|
||||||
export const mod = isMac ? 'Cmd' : 'Ctrl'
|
keymap?: { [key: string]: Command }
|
||||||
export const alt = isMac ? 'Cmd' : 'Alt'
|
config: Config
|
||||||
|
markdown: boolean
|
||||||
|
path?: string
|
||||||
|
y?: YOptions
|
||||||
|
schema?: Schema
|
||||||
|
collab?: Collab
|
||||||
|
typewriterMode?: boolean
|
||||||
|
}
|
||||||
export interface Args {
|
export interface Args {
|
||||||
cwd?: string
|
cwd?: string
|
||||||
file?: string
|
draft?: string
|
||||||
room?: string
|
room?: string
|
||||||
text?: any
|
text?: any
|
||||||
}
|
}
|
||||||
|
@ -32,15 +39,13 @@ export interface Config {
|
||||||
font: string
|
font: string
|
||||||
fontSize: number
|
fontSize: number
|
||||||
contentWidth: number
|
contentWidth: number
|
||||||
alwaysOnTop: boolean
|
typewriterMode: boolean
|
||||||
// typewriterMode: boolean;
|
|
||||||
prettier: PrettierConfig
|
prettier: PrettierConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorObject {
|
export interface ErrorObject {
|
||||||
message: string
|
|
||||||
id: string
|
id: string
|
||||||
props: unknown
|
props?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YOptions {
|
export interface YOptions {
|
||||||
|
@ -57,45 +62,63 @@ export interface Collab {
|
||||||
|
|
||||||
export type LoadingType = 'loading' | 'initialized'
|
export type LoadingType = 'loading' | 'initialized'
|
||||||
|
|
||||||
export interface File {
|
|
||||||
text?: { [key: string]: any }
|
|
||||||
lastModified?: string
|
|
||||||
path?: string
|
|
||||||
markdown?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
isMac?: boolean
|
||||||
text?: ProseMirrorState
|
text?: ProseMirrorState
|
||||||
editorView?: EditorView
|
editorView?: EditorView
|
||||||
extensions?: ProseMirrorExtension[]
|
extensions?: ProseMirrorExtension[]
|
||||||
markdown?: boolean
|
markdown?: boolean
|
||||||
lastModified?: Date
|
lastModified?: Date
|
||||||
files: File[]
|
drafts: Draft[]
|
||||||
config: Config
|
config: Config
|
||||||
error?: ErrorObject
|
error?: ErrorObject
|
||||||
loading: LoadingType
|
loading?: LoadingType
|
||||||
fullscreen: boolean
|
fullscreen?: boolean
|
||||||
collab?: Collab
|
collab?: Collab
|
||||||
path?: string
|
path?: string
|
||||||
args?: Args
|
args?: Args
|
||||||
|
keymap?: { [key: string]: Command }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Draft {
|
||||||
|
body?: string
|
||||||
|
lastModified?: Date
|
||||||
|
text?: { doc: EditorState['doc']; selection: { type: string; anchor: number; head: number } }
|
||||||
|
path?: string
|
||||||
|
markdown?: boolean
|
||||||
|
extensions?: ProseMirrorExtension[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditorActions {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceError extends Error {
|
export class ServiceError extends Error {
|
||||||
public errorObject: ErrorObject
|
public errorObject: ErrorObject
|
||||||
constructor(id: string, props: unknown) {
|
constructor(id: string, props: unknown) {
|
||||||
super(id)
|
super(id)
|
||||||
this.errorObject = { id, props, message: '' }
|
this.errorObject = { id, props }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
export const StateContext = createContext<[Store<State>, EditorActions]>([undefined, undefined])
|
||||||
theme: '',
|
|
||||||
|
export const useState = () => useContext(StateContext)
|
||||||
|
|
||||||
|
export const newState = (props: Partial<State> = {}): State => ({
|
||||||
|
extensions: [],
|
||||||
|
drafts: [],
|
||||||
|
loading: 'loading',
|
||||||
|
fullscreen: false,
|
||||||
|
markdown: false,
|
||||||
|
config: {
|
||||||
|
theme: undefined,
|
||||||
// codeTheme: 'material-light',
|
// codeTheme: 'material-light',
|
||||||
font: 'muller',
|
font: 'muller',
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
contentWidth: 800,
|
contentWidth: 800,
|
||||||
alwaysOnTop: isMac,
|
typewriterMode: true,
|
||||||
// typewriterMode: true,
|
|
||||||
prettier: {
|
prettier: {
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
|
@ -103,18 +126,6 @@ const DEFAULT_CONFIG = {
|
||||||
semi: false,
|
semi: false,
|
||||||
singleQuote: true
|
singleQuote: true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
export const StateContext = createContext<[Store<State>, any]>([{} as Store<State>, undefined])
|
|
||||||
|
|
||||||
export const useState = () => useContext(StateContext)
|
|
||||||
|
|
||||||
export const newState = (props: Partial<State> = {}): State => ({
|
|
||||||
extensions: [],
|
|
||||||
files: [],
|
|
||||||
loading: 'loading',
|
|
||||||
fullscreen: false,
|
|
||||||
markdown: false,
|
|
||||||
config: DEFAULT_CONFIG,
|
|
||||||
...props
|
...props
|
||||||
})
|
})
|
|
@ -1,588 +0,0 @@
|
||||||
import { Store, createStore, unwrap } from 'solid-js/store'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import type { EditorState } from 'prosemirror-state'
|
|
||||||
import { undo, redo } from 'prosemirror-history'
|
|
||||||
import { selectAll, deleteSelection } from 'prosemirror-commands'
|
|
||||||
import { undo as yUndo, redo as yRedo } from 'y-prosemirror'
|
|
||||||
import { debounce } from 'ts-debounce'
|
|
||||||
// import * as remote from '../prosemirror/remote'
|
|
||||||
import { createSchema, createExtensions, createEmptyText } from '../prosemirror/setup'
|
|
||||||
import { State, File, Config, ServiceError, newState } from '.'
|
|
||||||
// import { isTauri, mod } from '../env'
|
|
||||||
import { serialize, createMarkdownParser } from '../prosemirror/markdown'
|
|
||||||
import { isEmpty, isInitialized } from '../prosemirror/state'
|
|
||||||
import { isServer } from 'solid-js/web'
|
|
||||||
import { roomConnect } from '../../../utils/p2p'
|
|
||||||
|
|
||||||
const mod = 'Ctrl'
|
|
||||||
const isTauri = false
|
|
||||||
const isText = (x: any) => x && x.doc && x.selection
|
|
||||||
const isState = (x: any) => typeof x.lastModified !== 'string' && Array.isArray(x.files)
|
|
||||||
const isFile = (x: any): boolean => x && (x.text || x.path)
|
|
||||||
|
|
||||||
export const createCtrl = (initial: State): [Store<State>, any] => {
|
|
||||||
const [store, setState] = createStore(initial)
|
|
||||||
|
|
||||||
const discardText = async () => {
|
|
||||||
const state = unwrap(store)
|
|
||||||
const index = state.files.length - 1
|
|
||||||
const file = index !== -1 ? state.files[index] : undefined
|
|
||||||
|
|
||||||
let next: Partial<State>
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
next = await createTextFromFile(file)
|
|
||||||
} else {
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config ?? store.config,
|
|
||||||
markdown: (state.markdown && store.markdown) as any,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
next = {
|
|
||||||
text: createEmptyText(),
|
|
||||||
extensions,
|
|
||||||
lastModified: undefined,
|
|
||||||
path: undefined,
|
|
||||||
markdown: state.markdown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = state.files.filter((f: File) => f !== file)
|
|
||||||
|
|
||||||
setState({
|
|
||||||
files,
|
|
||||||
...next,
|
|
||||||
collab: file ? undefined : state.collab,
|
|
||||||
error: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const addToFiles = (files: File[], prev: State) => {
|
|
||||||
const text = prev.path ? undefined : (prev.text as EditorState).toJSON()
|
|
||||||
|
|
||||||
return [
|
|
||||||
...files,
|
|
||||||
{
|
|
||||||
text,
|
|
||||||
lastModified: prev.lastModified?.toISOString(),
|
|
||||||
path: prev.path,
|
|
||||||
markdown: prev.markdown
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFile = () => {
|
|
||||||
if (isEmpty(store.text) && !store.path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const state: State = unwrap(store)
|
|
||||||
let files = state.files
|
|
||||||
|
|
||||||
if (!state.error) {
|
|
||||||
files = addToFiles(files, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensions: any[] = createExtensions({
|
|
||||||
config: state.config ?? store.config,
|
|
||||||
markdown: state.markdown,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
setState({
|
|
||||||
text: createEmptyText(),
|
|
||||||
extensions,
|
|
||||||
files,
|
|
||||||
lastModified: undefined,
|
|
||||||
path: undefined,
|
|
||||||
error: undefined,
|
|
||||||
collab: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const discard = async () => {
|
|
||||||
if (store.path) {
|
|
||||||
await discardText()
|
|
||||||
} else if (store.files?.length > 0 && isEmpty(store.text)) {
|
|
||||||
await discardText()
|
|
||||||
} else {
|
|
||||||
selectAll(store.editorView.state, store.editorView.dispatch)
|
|
||||||
deleteSelection(store.editorView.state, store.editorView.dispatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
||||||
const onQuit = () => {
|
|
||||||
if (!isTauri) {
|
|
||||||
console.debug('quit')
|
|
||||||
// return
|
|
||||||
}
|
|
||||||
// remote.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNew = () => {
|
|
||||||
newFile()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDiscard = () => {
|
|
||||||
discard()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onFullscreen = () => {
|
|
||||||
if (!isTauri) return
|
|
||||||
|
|
||||||
ctrl.setFullscreen(!store.fullscreen)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onToggleMarkdown = () => toggleMarkdown()
|
|
||||||
|
|
||||||
const onUndo = () => {
|
|
||||||
if (!isInitialized(store.text)) return
|
|
||||||
|
|
||||||
const text = store.text as EditorState
|
|
||||||
|
|
||||||
if (store.collab?.started) {
|
|
||||||
yUndo(text)
|
|
||||||
} else {
|
|
||||||
undo(text, store.editorView.dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRedo = () => {
|
|
||||||
if (!isInitialized(store.text)) return
|
|
||||||
|
|
||||||
const text = store.text as EditorState
|
|
||||||
|
|
||||||
if (store.collab?.started) {
|
|
||||||
yRedo(text)
|
|
||||||
} else {
|
|
||||||
redo(text, store.editorView.dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const keymap = {
|
|
||||||
[`${mod}-q`]: onQuit,
|
|
||||||
[`${mod}-n`]: onNew,
|
|
||||||
[`${mod}-w`]: onDiscard,
|
|
||||||
'Cmd-Enter': onFullscreen,
|
|
||||||
'Alt-Enter': onFullscreen,
|
|
||||||
[`${mod}-z`]: onUndo,
|
|
||||||
[`Shift-${mod}-z`]: onRedo,
|
|
||||||
[`${mod}-y`]: onRedo,
|
|
||||||
[`${mod}-m`]: onToggleMarkdown
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTextFromFile = async (file: File) => {
|
|
||||||
const state = unwrap(store)
|
|
||||||
|
|
||||||
// if (file.path) file = await loadFile(state.config, file.path)
|
|
||||||
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config,
|
|
||||||
markdown: file.markdown,
|
|
||||||
path: file.path,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: file.text,
|
|
||||||
extensions,
|
|
||||||
lastModified: file.lastModified ? new Date(file.lastModified) : undefined,
|
|
||||||
path: file.path,
|
|
||||||
markdown: file.markdown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const fetchData = async (): Promise<State> => {
|
|
||||||
let args = {} // await remote.getArgs().catch(() => undefined)
|
|
||||||
const state: State = unwrap(store)
|
|
||||||
|
|
||||||
if (!isTauri) {
|
|
||||||
const room = window.location.pathname?.slice(1).trim()
|
|
||||||
|
|
||||||
args = { room: room || undefined }
|
|
||||||
}
|
|
||||||
if (!isServer) {
|
|
||||||
const { default: db } = await import('../db')
|
|
||||||
const data: string = await db.get('state')
|
|
||||||
let parsed: any
|
|
||||||
|
|
||||||
if (data !== undefined) {
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(data)
|
|
||||||
} catch {
|
|
||||||
throw new ServiceError('invalid_state', data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsed) {
|
|
||||||
return { ...state, args }
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = state.text
|
|
||||||
|
|
||||||
if (parsed.text) {
|
|
||||||
if (!isText(parsed.text)) {
|
|
||||||
throw new ServiceError('invalid_state', parsed.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
text = parsed.text
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensions = createExtensions({
|
|
||||||
path: parsed.path,
|
|
||||||
markdown: parsed.markdown,
|
|
||||||
keymap,
|
|
||||||
config: {} as Config
|
|
||||||
})
|
|
||||||
|
|
||||||
const nState = {
|
|
||||||
...parsed,
|
|
||||||
text,
|
|
||||||
extensions,
|
|
||||||
// config,
|
|
||||||
args
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nState.lastModified) {
|
|
||||||
nState.lastModified = new Date(nState.lastModified)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of parsed.files) {
|
|
||||||
if (!isFile(file)) {
|
|
||||||
throw new ServiceError('invalid_file', file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isState(nState)) {
|
|
||||||
throw new ServiceError('invalid_state', nState)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nState
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTheme = (state: State) => ({ theme: state.config.theme })
|
|
||||||
|
|
||||||
const clean = () => {
|
|
||||||
setState({
|
|
||||||
...newState(),
|
|
||||||
loading: 'initialized',
|
|
||||||
files: [],
|
|
||||||
fullscreen: store.fullscreen,
|
|
||||||
lastModified: new Date(),
|
|
||||||
error: undefined,
|
|
||||||
text: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
let data = await fetchData()
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (data.args?.room) {
|
|
||||||
data = doStartCollab(data)
|
|
||||||
} else if (data.args?.text) {
|
|
||||||
data = await doOpenFile(data, { text: JSON.parse(data.args?.text) })
|
|
||||||
} /* else if (data.args?.file) {
|
|
||||||
const file = await loadFile(data.config, data.args?.file)
|
|
||||||
|
|
||||||
data = await doOpenFile(data, file)
|
|
||||||
} else if (data.path) {
|
|
||||||
const file = await loadFile(data.config, data.path)
|
|
||||||
|
|
||||||
data = await doOpenFile(data, file)
|
|
||||||
} */ else if (!data.text) {
|
|
||||||
const text = createEmptyText()
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: data.config,
|
|
||||||
markdown: data.markdown,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
data = { ...data, text, extensions }
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
data = { ...data, error: error.errorObject }
|
|
||||||
}
|
|
||||||
|
|
||||||
setState({
|
|
||||||
...data,
|
|
||||||
config: { ...data.config, ...getTheme(data) },
|
|
||||||
loading: 'initialized'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
const loadFile = async (config: Config, path: string): Promise<File> => {
|
|
||||||
try {
|
|
||||||
const fileContent = await remote.readFile(path)
|
|
||||||
const lastModified = await remote.getFileLastModified(path)
|
|
||||||
const schema = createSchema({
|
|
||||||
config,
|
|
||||||
markdown: false,
|
|
||||||
path,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
const parser = createMarkdownParser(schema)
|
|
||||||
const doc = parser.parse(fileContent).toJSON()
|
|
||||||
const text = {
|
|
||||||
doc,
|
|
||||||
selection: {
|
|
||||||
type: 'text',
|
|
||||||
anchor: 1,
|
|
||||||
head: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
lastModified: lastModified.toISOString(),
|
|
||||||
path
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
throw new ServiceError('file_permission_denied', { error: e })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const openFile = async (file: File) => {
|
|
||||||
const state: State = unwrap(store)
|
|
||||||
const update = await doOpenFile(state, file)
|
|
||||||
|
|
||||||
setState(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
const doOpenFile = async (state: State, file: File): Promise<State> => {
|
|
||||||
const findIndexOfFile = (f: File) => {
|
|
||||||
for (let i = 0; i < state.files.length; i++) {
|
|
||||||
if (state.files[i] === f) return i
|
|
||||||
|
|
||||||
if (f.path && state.files[i].path === f.path) return i
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = findIndexOfFile(file)
|
|
||||||
const item = index === -1 ? file : state.files[index]
|
|
||||||
let files = state.files.filter((f) => f !== item)
|
|
||||||
|
|
||||||
if (!isEmpty(state.text) && state.lastModified) {
|
|
||||||
files = addToFiles(files, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
file.lastModified = item.lastModified
|
|
||||||
const next = await createTextFromFile(file)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...next,
|
|
||||||
files,
|
|
||||||
collab: undefined,
|
|
||||||
error: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line solid/reactivity
|
|
||||||
const saveState = debounce(async (state: State) => {
|
|
||||||
const data: any = {
|
|
||||||
lastModified: state.lastModified,
|
|
||||||
files: state.files,
|
|
||||||
config: state.config,
|
|
||||||
path: state.path,
|
|
||||||
markdown: state.markdown,
|
|
||||||
collab: {
|
|
||||||
room: state.collab?.room
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInitialized(state.text)) {
|
|
||||||
//if (state.path) {
|
|
||||||
// const text = serialize(store.editorView.state)
|
|
||||||
// await remote.writeFile(state.path, text)
|
|
||||||
//}
|
|
||||||
data.text = store.editorView.state.toJSON()
|
|
||||||
} else if (state.text) {
|
|
||||||
data.text = state.text
|
|
||||||
}
|
|
||||||
if (!isServer) {
|
|
||||||
const { default: db } = await import('../db')
|
|
||||||
db.set('state', JSON.stringify(data))
|
|
||||||
}
|
|
||||||
}, 200)
|
|
||||||
|
|
||||||
const setFullscreen = (fullscreen: boolean) => {
|
|
||||||
// remote.setFullscreen(fullscreen)
|
|
||||||
setState({ fullscreen })
|
|
||||||
}
|
|
||||||
|
|
||||||
const startCollab = () => {
|
|
||||||
const state: State = unwrap(store)
|
|
||||||
const update = doStartCollab(state)
|
|
||||||
|
|
||||||
setState(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
const doStartCollab = (state: State): State => {
|
|
||||||
const backup = state.args?.room && state.collab?.room !== state.args.room
|
|
||||||
const room = state.args?.room ?? uuidv4()
|
|
||||||
const username = '' // FIXME: use authenticated user name
|
|
||||||
const [type, provider] = roomConnect(room, username)
|
|
||||||
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config,
|
|
||||||
markdown: state.markdown,
|
|
||||||
path: state.path,
|
|
||||||
keymap,
|
|
||||||
y: { type, provider }
|
|
||||||
})
|
|
||||||
|
|
||||||
let nState = state
|
|
||||||
|
|
||||||
if ((backup && !isEmpty(state.text)) || state.path) {
|
|
||||||
let files = state.files
|
|
||||||
|
|
||||||
if (!state.error) {
|
|
||||||
files = addToFiles(files, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
nState = {
|
|
||||||
...state,
|
|
||||||
files,
|
|
||||||
lastModified: undefined,
|
|
||||||
path: undefined,
|
|
||||||
error: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...nState,
|
|
||||||
extensions,
|
|
||||||
collab: { started: true, room, y: { type, provider } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopCollab = (state: State) => {
|
|
||||||
state.collab?.y?.provider.destroy()
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config,
|
|
||||||
markdown: state.markdown,
|
|
||||||
path: state.path,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
setState({ collab: undefined, extensions })
|
|
||||||
window.history.replaceState(null, '', '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleMarkdown = () => {
|
|
||||||
const state = unwrap(store)
|
|
||||||
const editorState = store.text as EditorState
|
|
||||||
const markdown = !state.markdown
|
|
||||||
const selection = { type: 'text', anchor: 1, head: 1 }
|
|
||||||
let doc: any
|
|
||||||
|
|
||||||
if (markdown) {
|
|
||||||
const lines = serialize(editorState).split('\n')
|
|
||||||
const nodes = lines.map((text) => {
|
|
||||||
return text ? { type: 'paragraph', content: [{ type: 'text', text }] } : { type: 'paragraph' }
|
|
||||||
})
|
|
||||||
|
|
||||||
doc = { type: 'doc', content: nodes }
|
|
||||||
} else {
|
|
||||||
const schema = createSchema({
|
|
||||||
config: state.config,
|
|
||||||
path: state.path,
|
|
||||||
y: state.collab?.y,
|
|
||||||
markdown,
|
|
||||||
keymap
|
|
||||||
})
|
|
||||||
|
|
||||||
const parser = createMarkdownParser(schema)
|
|
||||||
let textContent = ''
|
|
||||||
|
|
||||||
editorState.doc.forEach((node) => {
|
|
||||||
textContent += `${node.textContent}\n`
|
|
||||||
})
|
|
||||||
const text = parser.parse(textContent)
|
|
||||||
doc = text?.toJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: state.config,
|
|
||||||
markdown,
|
|
||||||
path: state.path,
|
|
||||||
keymap,
|
|
||||||
y: state.collab?.y
|
|
||||||
})
|
|
||||||
|
|
||||||
setState({
|
|
||||||
text: { selection, doc },
|
|
||||||
extensions,
|
|
||||||
markdown
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfig = (config: Partial<Config>) => {
|
|
||||||
const state = unwrap(store)
|
|
||||||
const extensions = createExtensions({
|
|
||||||
config: { ...state.config, ...config },
|
|
||||||
markdown: state.markdown,
|
|
||||||
path: state.path,
|
|
||||||
keymap,
|
|
||||||
y: state.collab?.y
|
|
||||||
})
|
|
||||||
|
|
||||||
setState({
|
|
||||||
config: { ...state.config, ...config },
|
|
||||||
extensions,
|
|
||||||
lastModified: new Date()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatePath = (path: string) => {
|
|
||||||
setState({ path, lastModified: new Date() })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateTheme = () => {
|
|
||||||
const { theme } = getTheme(unwrap(store))
|
|
||||||
|
|
||||||
setState('config', { theme })
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctrl = {
|
|
||||||
clean,
|
|
||||||
discard,
|
|
||||||
getTheme,
|
|
||||||
init,
|
|
||||||
// loadFile,
|
|
||||||
newFile,
|
|
||||||
openFile,
|
|
||||||
saveState,
|
|
||||||
setFullscreen,
|
|
||||||
setState,
|
|
||||||
startCollab,
|
|
||||||
stopCollab,
|
|
||||||
toggleMarkdown,
|
|
||||||
updateConfig,
|
|
||||||
updatePath,
|
|
||||||
updateTheme
|
|
||||||
}
|
|
||||||
|
|
||||||
return [store, ctrl]
|
|
||||||
}
|
|
|
@ -32,11 +32,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.article__controls {
|
.article__controls {
|
||||||
|
@include font-size(1.4rem);
|
||||||
|
|
||||||
align-content: baseline;
|
align-content: baseline;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@include font-size(1.4rem);
|
|
||||||
|
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
}
|
}
|
||||||
|
|
25
src/components/Editor/styles/Button.scss
Normal file
25
src/components/Editor/styles/Button.scss
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
button {
|
||||||
|
height: 50px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
background: none;
|
||||||
|
font-family: Muller, Arial, Helvetica, sans-serif;
|
||||||
|
color: var(--foreground);
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
border: 0;
|
||||||
|
background: var(--primary-background);
|
||||||
|
}
|
|
@ -1,10 +1,21 @@
|
||||||
@import './Button';
|
|
||||||
@import './Sidebar';
|
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(0 100 200);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: rgb(0 80 160);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +55,6 @@
|
||||||
button:focus {
|
button:focus {
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
@ -54,7 +64,7 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
font-variant-ligatures: none;
|
font-variant-ligatures: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
margin: 1em 1em 1em 2em;
|
margin: 1em 1em 1em 0;
|
||||||
|
|
||||||
.dark & {
|
.dark & {
|
||||||
color: var(--background);
|
color: var(--background);
|
||||||
|
@ -377,7 +387,7 @@ li.ProseMirror-selectednode::after {
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
background: var(--background);
|
background: #fff;
|
||||||
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
box-shadow: 0 4px 10px rgb(0 0 0 / 25%);
|
||||||
color: #000;
|
color: #000;
|
||||||
display: flex;
|
display: flex;
|
|
@ -3,7 +3,6 @@
|
||||||
overflow: y-auto;
|
overflow: y-auto;
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: 'JetBrains Mono';
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
3
src/components/Editor/styles/Index.scss
Normal file
3
src/components/Editor/styles/Index.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.index {
|
||||||
|
width: 350px;
|
||||||
|
}
|
|
@ -1,12 +1,10 @@
|
||||||
.layout--editor {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: Muller;
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
border-color: var(--background);
|
border-color: var(--background);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin-top: -2.2rem !important;
|
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
|
@ -1,3 +1,29 @@
|
||||||
|
.sidebar-container {
|
||||||
|
color: rgb(255 255 255 / 50%);
|
||||||
|
@include font-size(1.6rem);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
@include font-size(120%);
|
||||||
|
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: auto;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 0 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-off {
|
.sidebar-off {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -75,13 +101,12 @@
|
||||||
.sidebar-container button,
|
.sidebar-container button,
|
||||||
.sidebar-container a,
|
.sidebar-container a,
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
text-align: left;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-family: Muller;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container a,
|
.sidebar-container a,
|
||||||
|
@ -91,33 +116,6 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-container {
|
|
||||||
color: rgb(255 255 255 / 50%);
|
|
||||||
font-family: Muller;
|
|
||||||
@include font-size(1.6rem);
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
|
||||||
@include font-size(120%);
|
|
||||||
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
height: auto;
|
|
||||||
min-height: 50px;
|
|
||||||
padding: 0 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-link {
|
.sidebar-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -143,7 +141,7 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.file {
|
&.draft {
|
||||||
color: rgb(255 255 255 / 50%);
|
color: rgb(255 255 255 / 50%);
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0 0 1em 1.5em;
|
margin: 0 0 1em 1.5em;
|
|
@ -1,6 +1,6 @@
|
||||||
// TODO: additional entities list column + article
|
// TODO: additional entities list column + article
|
||||||
|
|
||||||
import { For, Show } from 'solid-js/web'
|
import { For, Show } from 'solid-js'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
|
@ -11,7 +11,7 @@ import { t } from '../../utils/intl'
|
||||||
|
|
||||||
interface BesideProps {
|
interface BesideProps {
|
||||||
title?: string
|
title?: string
|
||||||
values: any[]
|
values: (Shout | User | Topic | Author)[]
|
||||||
beside: Shout
|
beside: Shout
|
||||||
wrapper: 'topic' | 'author' | 'article' | 'top-article'
|
wrapper: 'topic' | 'author' | 'article' | 'top-article'
|
||||||
isTopicCompact?: boolean
|
isTopicCompact?: boolean
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
import { For, Show } from 'solid-js/web'
|
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { capitalize } from '../../utils'
|
import { capitalize } from '../../utils'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
|
@ -8,12 +7,9 @@ import { Icon } from '../Nav/Icon'
|
||||||
import styles from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { handleClientRouteLinkClick } from '../../stores/router'
|
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import CardTopic from './CardTopic'
|
import CardTopic from './CardTopic'
|
||||||
|
|
||||||
const log = getLogger('card component')
|
|
||||||
|
|
||||||
interface ArticleCardProps {
|
interface ArticleCardProps {
|
||||||
settings?: {
|
settings?: {
|
||||||
noicon?: boolean
|
noicon?: boolean
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { For, Show } from 'solid-js/web'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
import { For, Show } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import './Group.scss'
|
import './Group.scss'
|
||||||
|
|
||||||
interface GroupProps {
|
interface GroupProps {
|
||||||
articles: Shout[]
|
articles: Shout[]
|
||||||
header?: any
|
header?: JSX.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: GroupProps) => {
|
export default (props: GroupProps) => {
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { For, Suspense } from 'solid-js/web'
|
|
||||||
import { Row1 } from './Row1'
|
import { Row1 } from './Row1'
|
||||||
import { Row2 } from './Row2'
|
import { Row2 } from './Row2'
|
||||||
import { Row3 } from './Row3'
|
import { Row3 } from './Row3'
|
||||||
import { shuffle } from '../../utils'
|
import { shuffle } from '../../utils'
|
||||||
import { createMemo, createSignal } from 'solid-js'
|
import { createMemo, createSignal, For, Suspense } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import './List.scss'
|
import './List.scss'
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createComputed, createSignal, Show } from 'solid-js'
|
import { createComputed, createSignal, Show, For } from 'solid-js'
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { For } from 'solid-js/web'
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
import { For } from 'solid-js'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
|
|
||||||
export const Row3 = (props: { articles: Shout[]; header?: any }) => {
|
export const Row3 = (props: { articles: Shout[]; header?: JSX.Element }) => {
|
||||||
return (
|
return (
|
||||||
<div class="floor">
|
<div class="floor">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
|
|
||||||
const log = getLogger('Row5')
|
|
||||||
|
|
||||||
export const Row5 = (props: { articles: Shout[] }) => {
|
export const Row5 = (props: { articles: Shout[] }) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import { ArticleCard } from './Card'
|
import { ArticleCard } from './Card'
|
||||||
import { Swiper, Navigation, Pagination } from 'swiper'
|
import { Swiper, Navigation, Pagination } from 'swiper'
|
||||||
import type { SwiperOptions } from 'swiper'
|
import type { SwiperOptions } from 'swiper'
|
||||||
|
@ -7,7 +6,7 @@ import 'swiper/scss/navigation'
|
||||||
import 'swiper/scss/pagination'
|
import 'swiper/scss/pagination'
|
||||||
import './Slider.scss'
|
import './Slider.scss'
|
||||||
import type { Shout } from '../../graphql/types.gen'
|
import type { Shout } from '../../graphql/types.gen'
|
||||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, Show, For } from 'solid-js'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
|
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
|
|
13
src/components/Nav/AuthModal/EmailConfirm.module.scss
Normal file
13
src/components/Nav/AuthModal/EmailConfirm.module.scss
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
.title {
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #141414;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-bottom: 52px;
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { createSignal, JSX } from 'solid-js'
|
import { createSignal, JSX, Show } from 'solid-js'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { signIn, signSendLink } from '../../../stores/auth'
|
import { signIn, signSendLink } from '../../../stores/auth'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Show } from 'solid-js/web'
|
import { Show, createSignal } from 'solid-js'
|
||||||
import type { JSX } from 'solid-js'
|
import type { JSX } from 'solid-js'
|
||||||
import { t } from '../../../utils/intl'
|
import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
||||||
import { createSignal } from 'solid-js'
|
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { For, Portal, Show } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
import { useWarningsStore } from '../../stores/ui'
|
import { useWarningsStore } from '../../stores/ui'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { warnings } = useWarningsStore()
|
const { warnings } = useWarningsStore()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import type { JSX } from 'solid-js/jsx-runtime'
|
||||||
import type { ModalType } from '../../stores/ui'
|
import type { ModalType } from '../../stores/ui'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
|
|
||||||
export default (props: { name: ModalType; children: any }) => {
|
export default (props: { name: ModalType; children: JSX.Element }) => {
|
||||||
return (
|
return (
|
||||||
<a href="#" onClick={() => showModal(props.name)}>
|
<a href="#" onClick={() => showModal(props.name)}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { For } from 'solid-js/web'
|
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { hideModal } from '../../stores/ui'
|
import { hideModal } from '../../stores/ui'
|
||||||
import { useAuthStore, signOut } from '../../stores/auth'
|
import { useAuthStore, signOut } from '../../stores/auth'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, For } from 'solid-js'
|
||||||
|
|
||||||
const quit = () => {
|
const quit = () => {
|
||||||
signOut()
|
signOut()
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { newState } from '../Editor/store/context'
|
||||||
import { MainLayout } from '../Layouts/MainLayout'
|
import { MainLayout } from '../Layouts/MainLayout'
|
||||||
import { CreateView } from '../Views/Create'
|
import { CreateView } from '../Views/Create'
|
||||||
|
|
||||||
export const CreatePage = () => {
|
export const CreatePage = () => {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<CreateView />
|
<CreateView state={newState()} />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { capitalize, plural } from '../../utils'
|
import { capitalize, plural } from '../../utils'
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import style from './Card.module.scss'
|
import style from './Card.module.scss'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo, Show } from 'solid-js'
|
||||||
import { Show } from 'solid-js/web'
|
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
|
|
|
@ -6,11 +6,8 @@ import { t } from '../../utils/intl'
|
||||||
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import '../../styles/AllTopics.scss'
|
import '../../styles/AllTopics.scss'
|
||||||
|
|
||||||
const log = getLogger('AllAuthorsView')
|
|
||||||
|
|
||||||
type AllAuthorsPageSearchParams = {
|
type AllAuthorsPageSearchParams = {
|
||||||
by: '' | 'name' | 'shouts' | 'rating'
|
by: '' | 'name' | 'shouts' | 'rating'
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,6 @@ import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import '../../styles/AllTopics.scss'
|
import '../../styles/AllTopics.scss'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
|
|
||||||
const log = getLogger('AllTopicsView')
|
|
||||||
|
|
||||||
type AllTopicsPageSearchParams = {
|
type AllTopicsPageSearchParams = {
|
||||||
by: 'shouts' | 'authors' | 'title' | ''
|
by: 'shouts' | 'authors' | 'title' | ''
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
import { Show, onCleanup, createEffect, onError, onMount, untrack } from 'solid-js'
|
import { Show, onCleanup, createEffect, onError, onMount, untrack } from 'solid-js'
|
||||||
import { createMutable, unwrap } from 'solid-js/store'
|
import { createMutable, unwrap } from 'solid-js/store'
|
||||||
import { State, StateContext, newState } from '../Editor/store'
|
import { State, StateContext } from '../Editor/store/context'
|
||||||
import { createCtrl } from '../Editor/store/ctrl'
|
import { createCtrl } from '../Editor/store/actions'
|
||||||
import { Layout } from '../Editor/Layout'
|
import { Layout } from '../Editor/components/Layout'
|
||||||
import Editor from '../Editor'
|
import { Editor } from '../Editor/components/Editor'
|
||||||
import { Sidebar } from '../Editor/Sidebar'
|
import { Sidebar } from '../Editor/components/Sidebar'
|
||||||
import ErrorView from '../Editor/Error'
|
import ErrorView from '../Editor/components/Error'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
|
|
||||||
const log = getLogger('CreateView')
|
const matchDark = () => window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
export const CreateView = () => {
|
export const CreateView = (props: { state: State }) => {
|
||||||
const [store, ctrl] = createCtrl(newState())
|
let isMac = false
|
||||||
|
const onChangeTheme = () => ctrl.updateTheme()
|
||||||
|
onMount(() => {
|
||||||
|
isMac = window?.navigator.platform.includes('Mac')
|
||||||
|
matchDark().addEventListener('change', onChangeTheme)
|
||||||
|
onCleanup(() => matchDark().removeEventListener('change', onChangeTheme))
|
||||||
|
})
|
||||||
|
|
||||||
|
const [store, ctrl] = createCtrl({ ...props.state, isMac })
|
||||||
const mouseEnterCoords = createMutable({ x: 0, y: 0 })
|
const mouseEnterCoords = createMutable({ x: 0, y: 0 })
|
||||||
|
|
||||||
const onMouseEnter = (e: MouseEvent) => {
|
const onMouseEnter = (e: MouseEvent) => {
|
||||||
|
@ -20,19 +27,12 @@ export const CreateView = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (store.error) return
|
console.debug('[create] view mounted')
|
||||||
await ctrl.init()
|
if (store.error) {
|
||||||
})
|
console.error(store.error)
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await ctrl.init()
|
||||||
const mediaQuery = '(prefers-color-scheme: dark)'
|
|
||||||
|
|
||||||
window.matchMedia(mediaQuery).addEventListener('change', ctrl.updateTheme)
|
|
||||||
onCleanup(() => window.matchMedia(mediaQuery).removeEventListener('change', ctrl.updateTheme))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onError((error) => {
|
onError((error) => {
|
||||||
|
@ -47,6 +47,7 @@ export const CreateView = () => {
|
||||||
}
|
}
|
||||||
const state: State = untrack(() => unwrap(store))
|
const state: State = untrack(() => unwrap(store))
|
||||||
ctrl.saveState(state)
|
ctrl.saveState(state)
|
||||||
|
console.debug('[create] status update')
|
||||||
return store.loading
|
return store.loading
|
||||||
}, store.loading)
|
}, store.loading)
|
||||||
|
|
||||||
|
@ -57,13 +58,8 @@ export const CreateView = () => {
|
||||||
data-testid={store.error ? 'error' : store.loading}
|
data-testid={store.error ? 'error' : store.loading}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
>
|
>
|
||||||
<Show when={store.error}>
|
<Show when={!store.error} fallback={<ErrorView />}>
|
||||||
<ErrorView />
|
|
||||||
</Show>
|
|
||||||
<Show when={store.loading === 'initialized'}>
|
|
||||||
<Show when={!store.error}>
|
|
||||||
<Editor />
|
<Editor />
|
||||||
</Show>
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</Show>
|
</Show>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { Beside } from '../Feed/Beside'
|
||||||
import RowShort from '../Feed/RowShort'
|
import RowShort from '../Feed/RowShort'
|
||||||
import Slider from '../Feed/Slider'
|
import Slider from '../Feed/Slider'
|
||||||
import Group from '../Feed/Group'
|
import Group from '../Feed/Group'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
@ -26,8 +25,6 @@ import { locale } from '../../stores/ui'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
import { splitToPages } from '../../utils/splitToPages'
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
|
||||||
const log = getLogger('home view')
|
|
||||||
|
|
||||||
type HomeProps = {
|
type HomeProps = {
|
||||||
randomTopics: Topic[]
|
randomTopics: Topic[]
|
||||||
recentPublishedArticles: Shout[]
|
recentPublishedArticles: Shout[]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// in a separate file to avoid circular dependencies
|
// in a separate file to avoid circular dependencies
|
||||||
import type { Author, Shout, Topic } from '../graphql/types.gen'
|
import type { Author, Chat, Shout, Topic } from '../graphql/types.gen'
|
||||||
|
|
||||||
// all the things (she said) that could be passed from the server
|
// all the things (she said) that could be passed from the server
|
||||||
export type PageProps = {
|
export type PageProps = {
|
||||||
|
@ -15,4 +15,5 @@ export type PageProps = {
|
||||||
searchQuery?: string
|
searchQuery?: string
|
||||||
// other types?
|
// other types?
|
||||||
searchResults?: Shout[]
|
searchResults?: Shout[]
|
||||||
|
chats?: Chat[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// TODO: sync with backend
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ArticleMutation($article: Shout!) {
|
mutation CreateShoutMutation($shout: ShoutInput!) {
|
||||||
createArticle(article: $article) {
|
createShout(input: $shout) {
|
||||||
error
|
error
|
||||||
shout {
|
shout {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// TODO: sync with backend
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ArticleMutation($article_id: Int!) {
|
mutation DeleteShoutMutation($shout: String!) {
|
||||||
destroyArticle(article: $article_id) {
|
deleteShout(slug: $shout) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ArticleMutation($article: Shout!) {
|
mutation UpdateShoutMutation($shout: Shout!) {
|
||||||
updateArticle(article: $article) {
|
updateShout(input: $shout) {
|
||||||
error
|
error
|
||||||
shout {
|
shout {
|
||||||
_id: slug
|
_id: slug
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// TODO: sync with backend
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation CreateReactionMutation($reaction: ReactionInput!) {
|
mutation CreateReactionMutation($reaction: ReactionInput!) {
|
||||||
createReaction(reaction: $reaction) {
|
createReaction(reaction: $reaction) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// TODO: sync with backend
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation DeleteReactionMutation($id: Int!) {
|
mutation DeleteReactionMutation($id: Int!) {
|
||||||
deleteReaction(id: $id) {
|
deleteReaction(id: $id) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// TODO: sync with backend
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UpdateReactionMutation($reaction: ReactionInput!) {
|
mutation UpdateReactionMutation($reaction: ReactionInput!) {
|
||||||
updateReaction(reaction: $reaction) {
|
updateReaction(reaction: $reaction) {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { gql } from '@urql/core'
|
|
||||||
|
|
||||||
export default gql`
|
|
||||||
query ReactionsByShoutQuery($slug: String!, $limit: Int!, $offset: Int!) {
|
|
||||||
reactionsByShout(slug: $slug, limit: $limit, offset: $offset) {
|
|
||||||
id
|
|
||||||
body
|
|
||||||
createdAt
|
|
||||||
createdBy {
|
|
||||||
_id: slug
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
userpic
|
|
||||||
}
|
|
||||||
updatedAt
|
|
||||||
replyTo {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
kind
|
|
||||||
range
|
|
||||||
stat {
|
|
||||||
_id: viewed
|
|
||||||
viewed
|
|
||||||
reacted
|
|
||||||
rating
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -11,7 +11,7 @@ export default gql`
|
||||||
communities
|
communities
|
||||||
links
|
links
|
||||||
createdAt
|
createdAt
|
||||||
wasOnlineAt
|
lastSeen
|
||||||
ratings {
|
ratings {
|
||||||
_id: rater
|
_id: rater
|
||||||
rater
|
rater
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default gql`
|
||||||
communities
|
communities
|
||||||
links
|
links
|
||||||
createdAt
|
createdAt
|
||||||
wasOnlineAt
|
lastSeen
|
||||||
ratings {
|
ratings {
|
||||||
_id: rater
|
_id: rater
|
||||||
rater
|
rater
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default gql`
|
||||||
communities
|
communities
|
||||||
links
|
links
|
||||||
createdAt
|
createdAt
|
||||||
wasOnlineAt
|
lastSeen
|
||||||
ratings {
|
ratings {
|
||||||
_id: rater
|
_id: rater
|
||||||
rater
|
rater
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { initRouter } from '../stores/router'
|
||||||
|
|
||||||
const slug = Astro.params.slug?.toString()
|
const slug = Astro.params.slug?.toString()
|
||||||
if (slug.endsWith('.map')) {
|
if (slug.endsWith('.map')) {
|
||||||
return
|
return Astro.redirect('/404')
|
||||||
}
|
}
|
||||||
|
|
||||||
const article = await apiClient.getArticle({ slug })
|
const article = await apiClient.getArticle({ slug })
|
||||||
|
|
|
@ -11,5 +11,5 @@ initRouter(pathname, search)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Zine>
|
<Zine>
|
||||||
<Root client:load />
|
<Root chats={chatrooms} client:load />
|
||||||
</Zine>
|
</Zine>
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import type { AuthResult } from '../graphql/types.gen'
|
import type { AuthResult } from '../graphql/types.gen'
|
||||||
import { getLogger } from '../utils/logger'
|
|
||||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
const log = getLogger('auth-store')
|
|
||||||
|
|
||||||
const [session, setSession] = createSignal<AuthResult | null>(null)
|
const [session, setSession] = createSignal<AuthResult | null>(null)
|
||||||
|
|
||||||
export const signIn = async (params) => {
|
export const signIn = async (params) => {
|
||||||
const authResult = await apiClient.authLogin(params)
|
const authResult = await apiClient.authLogin(params)
|
||||||
setSession(authResult)
|
setSession(authResult)
|
||||||
setToken(authResult.token)
|
setToken(authResult.token)
|
||||||
log.debug('signed in')
|
console.debug('signed in')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signOut = () => {
|
export const signOut = () => {
|
||||||
// TODO: call backend to revoke token
|
// TODO: call backend to revoke token
|
||||||
setSession(null)
|
setSession(null)
|
||||||
resetToken()
|
resetToken()
|
||||||
log.debug('signed out')
|
console.debug('signed out')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({})
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { persistentAtom } from '@nanostores/persistent'
|
|
||||||
import { Reaction, ReactionKind } from '../graphql/types.gen'
|
|
||||||
import { atom, computed } from 'nanostores'
|
|
||||||
import { reactions } from './zine/reactions'
|
|
||||||
|
|
||||||
interface Draft {
|
|
||||||
createdAt: Date
|
|
||||||
body?: string
|
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Collab {
|
|
||||||
authors: string[] // slugs
|
|
||||||
invites?: string[]
|
|
||||||
createdAt: Date
|
|
||||||
body?: string
|
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const drafts = persistentAtom<Draft[]>('drafts', [], {
|
|
||||||
encode: JSON.stringify,
|
|
||||||
decode: JSON.parse
|
|
||||||
}) // save drafts on device
|
|
||||||
|
|
||||||
const collabs = atom<Collab[]>([]) // save collabs in backend or in p2p network
|
|
||||||
|
|
||||||
/*
|
|
||||||
const approvals = computed(
|
|
||||||
reactions,
|
|
||||||
(rdict) => Object.values(rdict)
|
|
||||||
.filter((r: Reaction) => r.kind === ReactionKind.Accept)
|
|
||||||
)
|
|
||||||
const proposals = computed<Reaction[], typeof reactions>(
|
|
||||||
reactions,
|
|
||||||
(rdict) => Object.values(rdict)
|
|
||||||
.filter((r: Reaction) => r.kind === ReactionKind.Propose)
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
export { drafts, collabs /* approvals, proposals */ }
|
|
37
src/stores/editor.ts
Normal file
37
src/stores/editor.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { persistentMap } from '@nanostores/persistent'
|
||||||
|
import type { Reaction } from '../graphql/types.gen'
|
||||||
|
import { atom } from 'nanostores'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import type { Draft } from '../components/Editor/store/context'
|
||||||
|
|
||||||
|
interface Collab {
|
||||||
|
authors: string[] // slugs
|
||||||
|
invites?: string[]
|
||||||
|
createdAt: Date
|
||||||
|
body?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const drafts = persistentMap<{ [key: string]: Draft }>(
|
||||||
|
'drafts',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
encode: JSON.stringify,
|
||||||
|
decode: JSON.parse
|
||||||
|
}
|
||||||
|
) // save drafts on device
|
||||||
|
|
||||||
|
export const collabs = atom<Collab[]>([]) // save collabs in backend or in p2p network
|
||||||
|
export const [editorReactions, setReactions] = createSignal<Reaction[]>([])
|
||||||
|
/*
|
||||||
|
const approvals = computed(
|
||||||
|
reactions,
|
||||||
|
(rdict) => Object.values(rdict)
|
||||||
|
.filter((r: Reaction) => r.kind === ReactionKind.Accept)
|
||||||
|
)
|
||||||
|
const proposals = computed<Reaction[], typeof reactions>(
|
||||||
|
reactions,
|
||||||
|
(rdict) => Object.values(rdict)
|
||||||
|
.filter((r: Reaction) => r.kind === ReactionKind.Propose)
|
||||||
|
)
|
||||||
|
*/
|
|
@ -3,13 +3,9 @@ import { apiClient } from '../../utils/apiClient'
|
||||||
import { addAuthorsByTopic } from './authors'
|
import { addAuthorsByTopic } from './authors'
|
||||||
import { addTopicsByAuthor } from './topics'
|
import { addTopicsByAuthor } from './topics'
|
||||||
import { byStat } from '../../utils/sortby'
|
import { byStat } from '../../utils/sortby'
|
||||||
|
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
|
||||||
const log = getLogger('articles store')
|
|
||||||
|
|
||||||
const [sortedArticles, setSortedArticles] = createSignal<Shout[]>([])
|
const [sortedArticles, setSortedArticles] = createSignal<Shout[]>([])
|
||||||
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
|
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
|
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
|
||||||
const log = getLogger('authors store')
|
|
||||||
|
|
||||||
export type AuthorsSortBy = 'shouts' | 'name' | 'rating'
|
export type AuthorsSortBy = 'shouts' | 'name' | 'rating'
|
||||||
|
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
|
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('shouts')
|
||||||
|
@ -24,17 +20,20 @@ const sortedAuthors = createLazyMemo(() => {
|
||||||
// authors.sort(byCreated)
|
// authors.sort(byCreated)
|
||||||
// break
|
// break
|
||||||
// }
|
// }
|
||||||
case 'rating':
|
case 'rating': {
|
||||||
// TODO:
|
// TODO:
|
||||||
break
|
break
|
||||||
case 'shouts':
|
}
|
||||||
|
case 'shouts': {
|
||||||
// TODO:
|
// TODO:
|
||||||
break
|
break
|
||||||
case 'name':
|
}
|
||||||
log.debug('sorted by name')
|
case 'name': {
|
||||||
|
console.debug('sorted by name')
|
||||||
authors.sort((a, b) => a.name.localeCompare(b.name))
|
authors.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return authors
|
return authors
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const loadArticleReactions = async ({
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const data = await apiClient.getArticleReactions({ articleSlug, limit, offset })
|
const data = await apiClient.getReactionsForShouts({ shoutSlugs: [articleSlug], limit, offset })
|
||||||
// TODO: const [data, provider] = roomConnect(articleSlug, username, "reactions")
|
// TODO: const [data, provider] = roomConnect(articleSlug, username, "reactions")
|
||||||
reactionsOrdered.set(data)
|
reactionsOrdered.set(data)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user