Compare commits
8 commits
Author | SHA1 | Date | |
---|---|---|---|
b12f | a9e2ef03fe | ||
Benjamin Bädorf | d8b729edc3 | ||
Benjamin Bädorf | 0fcb0d03a0 | ||
Benjamin Bädorf | e3ddcefb30 | ||
Benjamin Bädorf | 27b7e3afec | ||
Benjamin Bädorf | 520b3a6753 | ||
Benjamin Bädorf | b4eeb244bf | ||
Benjamin Bädorf | f1ff6f75ad |
|
@ -1,24 +1,7 @@
|
||||||
# Editor configuration, see http://editorconfig.org
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
# Ignore diffs/patches
|
trim_trailing_whitespace = true
|
||||||
[*.{diff,patch}]
|
insert_final_newline = true
|
||||||
end_of_line = unset
|
max_line_length = 100
|
||||||
insert_final_newline = unset
|
|
||||||
trim_trailing_whitespace = unset
|
|
||||||
indent_size = unset
|
|
||||||
charset = unset
|
|
||||||
indent_style = unset
|
|
||||||
indent_size = unset
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
max_line_length = off
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -7,7 +7,3 @@ dist-ssr
|
||||||
tags
|
tags
|
||||||
.temp
|
.temp
|
||||||
.cache
|
.cache
|
||||||
docs/.vitepress/cache
|
|
||||||
docs/.vitepress/dist
|
|
||||||
__screenshots__
|
|
||||||
__snapshots__
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { defineConfig } from 'vitepress';
|
import { defineConfig } from 'vitepress';
|
||||||
import { defineConfig as defineViteConfig } from 'vite';
|
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
lang: 'en-US',
|
lang: 'en-US',
|
||||||
|
@ -28,20 +25,5 @@ export default defineConfig({
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
|
|
||||||
vite: defineViteConfig({
|
|
||||||
plugins: [
|
|
||||||
vueJsx(),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@schlechtenburg/core': join(__dirname, '../../packages/core/lib/index.ts'),
|
|
||||||
'@schlechtenburg/paragraph': join(__dirname, '../../packages/paragraph/lib/index.ts'),
|
|
||||||
'@schlechtenburg/heading': join(__dirname, '../../packages/heading/lib/index.ts'),
|
|
||||||
'@schlechtenburg/image': join(__dirname, '../../packages/image/lib/index.ts'),
|
|
||||||
'@schlechtenburg/layout': join(__dirname, '../../packages/layout/lib/index.ts'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
});
|
|
@ -1,7 +1,6 @@
|
||||||
.example-editor {
|
.example-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 4rem;
|
|
||||||
|
|
||||||
&--mode {
|
&--mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import SbLayout from '@schlechtenburg/layout';
|
||||||
import SbHeading from '@schlechtenburg/heading';
|
import SbHeading from '@schlechtenburg/heading';
|
||||||
import SbParagraph from '@schlechtenburg/paragraph';
|
import SbParagraph from '@schlechtenburg/paragraph';
|
||||||
import SbImage from '@schlechtenburg/image';
|
import SbImage from '@schlechtenburg/image';
|
||||||
|
import SbItalicTool from '@schlechtenburg/italic';
|
||||||
|
|
||||||
import exampleData from './example-data';
|
import exampleData from './example-data';
|
||||||
|
|
||||||
|
@ -38,6 +39,9 @@ export default defineComponent({
|
||||||
SbImage,
|
SbImage,
|
||||||
SbParagraph,
|
SbParagraph,
|
||||||
]}
|
]}
|
||||||
|
availableInlineTools={[
|
||||||
|
SbItalicTool,
|
||||||
|
]}
|
||||||
mode={activeTab.value as SbMode}
|
mode={activeTab.value as SbMode}
|
||||||
eventUpdate={(data:IBlockData<any>) => {
|
eventUpdate={(data:IBlockData<any>) => {
|
||||||
block.id = data.id;
|
block.id = data.id;
|
||||||
|
|
13967
docs/api.json
13967
docs/api.json
File diff suppressed because it is too large
Load diff
|
@ -21,3 +21,7 @@ export default {
|
||||||
view: defineAsyncComponent(() => import('./view')),
|
view: defineAsyncComponent(() => import('./view')),
|
||||||
} as IBlockDefinition<any>;
|
} as IBlockDefinition<any>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Go by example
|
||||||
|
|
||||||
|
As Schlechtenburg is still in active development, it's good to check out the official blocks to see what they look like.
|
||||||
|
|
|
@ -23,6 +23,11 @@ html {
|
||||||
|
|
||||||
--interact: #3f9cff;
|
--interact: #3f9cff;
|
||||||
--interact-lite: #3f9cff;
|
--interact-lite: #3f9cff;
|
||||||
|
|
||||||
|
--info: var(--interact);
|
||||||
|
--success: green;
|
||||||
|
--warning: orange;
|
||||||
|
--error: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
---
|
---
|
||||||
layout: page
|
layout: page
|
||||||
---
|
---
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { withBase } from 'vitepress';
|
||||||
import ExampleEditor from './ExampleEditor';
|
import ExampleEditor from './ExampleEditor';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="sb-doc">
|
||||||
|
|
||||||
|
# Example Schlechtenburg Editor
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<ExampleEditor></ExampleEditor>
|
<ExampleEditor></ExampleEditor>
|
||||||
|
|
19
docs/vite.config.ts
Normal file
19
docs/vite.config.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vueJsx(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@schlechtenburg/core': join(__dirname, '../packages/core/lib/index.ts'),
|
||||||
|
'@schlechtenburg/paragraph': join(__dirname, '../packages/paragraph/lib/index.ts'),
|
||||||
|
'@schlechtenburg/heading': join(__dirname, '../packages/heading/lib/index.ts'),
|
||||||
|
'@schlechtenburg/image': join(__dirname, '../packages/image/lib/index.ts'),
|
||||||
|
'@schlechtenburg/layout': join(__dirname, '../packages/layout/lib/index.ts'),
|
||||||
|
'@schlechtenburg/italic': join(__dirname, '../packages/italic/lib/index.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
116
flake.lock
116
flake.lock
|
@ -1,116 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"devshell": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1713195852,
|
|
||||||
"narHash": "sha256-MEb4Hx/Aw7pcsmcHXBuldFsrVTfl9Q9dz1JSlxUanmE=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "devshell",
|
|
||||||
"rev": "2c8e04e5c29299bec53c2e5a73da0f9afa8dabb5",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "devshell",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1701680307,
|
|
||||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-utils_2": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1710146030,
|
|
||||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1728018373,
|
|
||||||
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"devshell": "devshell",
|
|
||||||
"flake-utils": "flake-utils_2",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
34
flake.nix
34
flake.nix
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
description = "Schlechtenburg";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
||||||
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
|
|
||||||
devshell.url = "github:numtide/devshell";
|
|
||||||
devshell.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, flake-utils, devshell, nixpkgs }:
|
|
||||||
flake-utils.lib.simpleFlake {
|
|
||||||
inherit self nixpkgs;
|
|
||||||
name = "schlechtenburg";
|
|
||||||
preOverlays = [ devshell.overlays.default ];
|
|
||||||
shell = { pkgs }: pkgs.devshell.mkShell {
|
|
||||||
devshell.packages = with pkgs; [
|
|
||||||
nodejs
|
|
||||||
nodePackages.typescript
|
|
||||||
nodePackages.typescript-language-server
|
|
||||||
nodePackages.vue-language-server
|
|
||||||
playwright
|
|
||||||
];
|
|
||||||
|
|
||||||
env = [
|
|
||||||
{ name = "PLAYWRIGHT_NODEJS_PATH"; value = "${pkgs.nodejs}/bin/node"; }
|
|
||||||
{ name = "PLAYWRIGHT_BROWSERS_PATH"; value = "${pkgs.playwright-driver.browsers}"; }
|
|
||||||
{ name = "PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS"; value = "true"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -2,6 +2,5 @@
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
"version": "0.0.0",
|
"version": "0.0.0"
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
|
||||||
}
|
}
|
||||||
|
|
26479
package-lock.json
generated
26479
package-lock.json
generated
File diff suppressed because it is too large
Load diff
36
package.json
36
package.json
|
@ -1,40 +1,24 @@
|
||||||
{
|
{
|
||||||
"name": "schlechtenburg",
|
"name": "schlechtenburg",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docs:type:build": "npx typedoc --json ./docs/api.json --entryPointStrategy packages --readme none packages/core packages/heading packages/standalone packages/paragraph packages/layout packages/image",
|
"docs:type:build": "npx typedoc --json ./docs/api.json --entryPointStrategy packages --readme none packages/core packages/heading packages/standalone packages/paragraph packages/layout packages/image",
|
||||||
"typecheck": "lerna run --stream typecheck",
|
"typecheck": "lerna run --stream typecheck",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:serve": "vitepress serve docs",
|
"docs:serve": "vitepress serve docs"
|
||||||
"test:browser": "vitest"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jsdom": "^21.1.7",
|
"@vitejs/plugin-vue-jsx": "^2.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"lerna": "^3.22.1",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
"sass": "^1.54.4",
|
||||||
"@vitest/browser": "^2.1.2",
|
"typedoc": "^0.22.13",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"typescript": "^4.6.2",
|
||||||
"deep-freeze": "^0.0.1",
|
"vitepress": "^1.0.0-alpha.13",
|
||||||
"lerna": "^8.1.8",
|
"vue": "^3.2.37"
|
||||||
"sass": "^1.75.0",
|
|
||||||
"typedoc": "^0.26.7",
|
|
||||||
"typescript": "^5.6.2",
|
|
||||||
"vitepress": "^1.3.4",
|
|
||||||
"vitest": "^2.1.2",
|
|
||||||
"vitest-browser-vue": "^0.0.1",
|
|
||||||
"vue": "^3.5.10"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21"
|
||||||
},
|
}
|
||||||
"workspaces": [
|
|
||||||
"packages/core",
|
|
||||||
"packages/heading",
|
|
||||||
"packages/image",
|
|
||||||
"packages/paragraph",
|
|
||||||
"packages/layout",
|
|
||||||
"packages/rich-text"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
16
packages/cms/.editorconfig
Normal file
16
packages/cms/.editorconfig
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[{package.json,*.yml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
6
packages/cms/.env.example
Normal file
6
packages/cms/.env.example
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=1337
|
||||||
|
APP_KEYS="toBeModified1,toBeModified2"
|
||||||
|
API_TOKEN_SALT=tobemodified
|
||||||
|
ADMIN_JWT_SECRET=tobemodified
|
||||||
|
JWT_SECRET=tobemodified
|
115
packages/cms/.gitignore
vendored
Normal file
115
packages/cms/.gitignore
vendored
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
############################
|
||||||
|
# OS X
|
||||||
|
############################
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
._*
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Linux
|
||||||
|
############################
|
||||||
|
|
||||||
|
*~
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Windows
|
||||||
|
############################
|
||||||
|
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Packages
|
||||||
|
############################
|
||||||
|
|
||||||
|
*.7z
|
||||||
|
*.csv
|
||||||
|
*.dat
|
||||||
|
*.dmg
|
||||||
|
*.gz
|
||||||
|
*.iso
|
||||||
|
*.jar
|
||||||
|
*.rar
|
||||||
|
*.tar
|
||||||
|
*.zip
|
||||||
|
*.com
|
||||||
|
*.class
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
*.seed
|
||||||
|
*.so
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
*.swn
|
||||||
|
*.swm
|
||||||
|
*.out
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Logs and databases
|
||||||
|
############################
|
||||||
|
|
||||||
|
.tmp
|
||||||
|
*.log
|
||||||
|
*.sql
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Misc.
|
||||||
|
############################
|
||||||
|
|
||||||
|
*#
|
||||||
|
ssl
|
||||||
|
.idea
|
||||||
|
nbproject
|
||||||
|
public/uploads/*
|
||||||
|
!public/uploads/.gitkeep
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Node.js
|
||||||
|
############################
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
lcov.info
|
||||||
|
pids
|
||||||
|
logs
|
||||||
|
results
|
||||||
|
node_modules
|
||||||
|
.node_history
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Tests
|
||||||
|
############################
|
||||||
|
|
||||||
|
testApp
|
||||||
|
coverage
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Strapi
|
||||||
|
############################
|
||||||
|
|
||||||
|
.env
|
||||||
|
license.txt
|
||||||
|
exports
|
||||||
|
*.cache
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.strapi-updater.json
|
57
packages/cms/README.md
Normal file
57
packages/cms/README.md
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# 🚀 Getting started with Strapi
|
||||||
|
|
||||||
|
Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html) (CLI) which lets you scaffold and manage your project in seconds.
|
||||||
|
|
||||||
|
### `dev`
|
||||||
|
|
||||||
|
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-develop)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### `start`
|
||||||
|
|
||||||
|
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-start)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
# or
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### `build`
|
||||||
|
|
||||||
|
Build your admin panel. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-build)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
# or
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Deployment
|
||||||
|
|
||||||
|
Strapi gives you many possible deployment options for your project. Find the one that suits you on the [deployment section of the documentation](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html).
|
||||||
|
|
||||||
|
## 📚 Learn more
|
||||||
|
|
||||||
|
- [Resource center](https://strapi.io/resource-center) - Strapi resource center.
|
||||||
|
- [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
|
||||||
|
- [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
|
||||||
|
- [Strapi blog](https://docs.strapi.io) - Official Strapi blog containing articles made by the Strapi team and the community.
|
||||||
|
- [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
|
||||||
|
|
||||||
|
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## ✨ Community
|
||||||
|
|
||||||
|
- [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
|
||||||
|
- [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
|
||||||
|
- [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>
|
8
packages/cms/config/admin.ts
Normal file
8
packages/cms/config/admin.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default ({ env }) => ({
|
||||||
|
auth: {
|
||||||
|
secret: env('ADMIN_JWT_SECRET'),
|
||||||
|
},
|
||||||
|
apiToken: {
|
||||||
|
salt: env('API_TOKEN_SALT'),
|
||||||
|
},
|
||||||
|
});
|
7
packages/cms/config/api.ts
Normal file
7
packages/cms/config/api.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
rest: {
|
||||||
|
defaultLimit: 25,
|
||||||
|
maxLimit: 100,
|
||||||
|
withCount: true,
|
||||||
|
},
|
||||||
|
};
|
11
packages/cms/config/database.ts
Normal file
11
packages/cms/config/database.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default ({ env }) => ({
|
||||||
|
connection: {
|
||||||
|
client: 'sqlite',
|
||||||
|
connection: {
|
||||||
|
filename: path.join(__dirname, '..', '..', env('DATABASE_FILENAME', '.tmp/data.db')),
|
||||||
|
},
|
||||||
|
useNullAsDefault: true,
|
||||||
|
},
|
||||||
|
});
|
12
packages/cms/config/middlewares.ts
Normal file
12
packages/cms/config/middlewares.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export default [
|
||||||
|
'strapi::errors',
|
||||||
|
'strapi::security',
|
||||||
|
'strapi::cors',
|
||||||
|
'strapi::poweredBy',
|
||||||
|
'strapi::logger',
|
||||||
|
'strapi::query',
|
||||||
|
'strapi::body',
|
||||||
|
'strapi::session',
|
||||||
|
'strapi::favicon',
|
||||||
|
'strapi::public',
|
||||||
|
];
|
7
packages/cms/config/server.ts
Normal file
7
packages/cms/config/server.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default ({ env }) => ({
|
||||||
|
host: env('HOST', '0.0.0.0'),
|
||||||
|
port: env.int('PORT', 1337),
|
||||||
|
app: {
|
||||||
|
keys: env.array('APP_KEYS'),
|
||||||
|
},
|
||||||
|
});
|
BIN
packages/cms/favicon.png
Normal file
BIN
packages/cms/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 497 B |
30093
packages/cms/package-lock.json
generated
Normal file
30093
packages/cms/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
packages/cms/package.json
Normal file
30
packages/cms/package.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "@schlechtenburg/cms",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "A Strapi application",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "strapi develop",
|
||||||
|
"start": "strapi start",
|
||||||
|
"build": "strapi build",
|
||||||
|
"strapi": "strapi"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strapi/plugin-graphql": "^4.5.3",
|
||||||
|
"@strapi/plugin-i18n": "4.5.3",
|
||||||
|
"@strapi/plugin-users-permissions": "4.5.3",
|
||||||
|
"@strapi/strapi": "4.5.3",
|
||||||
|
"better-sqlite3": "7.4.6"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "A Strapi developer"
|
||||||
|
},
|
||||||
|
"strapi": {
|
||||||
|
"uuid": "d70cca4c-887b-45a7-8b2d-afb95c49a0c8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.19.1 <=18.x.x",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
3
packages/cms/public/robots.txt
Normal file
3
packages/cms/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# To prevent search engines from seeing the site altogether, uncomment the next two lines:
|
||||||
|
# User-Agent: *
|
||||||
|
# Disallow: /
|
0
packages/cms/public/uploads/.gitkeep
Normal file
0
packages/cms/public/uploads/.gitkeep
Normal file
35
packages/cms/src/admin/app.example.tsx
Normal file
35
packages/cms/src/admin/app.example.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export default {
|
||||||
|
config: {
|
||||||
|
locales: [
|
||||||
|
// 'ar',
|
||||||
|
// 'fr',
|
||||||
|
// 'cs',
|
||||||
|
// 'de',
|
||||||
|
// 'dk',
|
||||||
|
// 'es',
|
||||||
|
// 'he',
|
||||||
|
// 'id',
|
||||||
|
// 'it',
|
||||||
|
// 'ja',
|
||||||
|
// 'ko',
|
||||||
|
// 'ms',
|
||||||
|
// 'nl',
|
||||||
|
// 'no',
|
||||||
|
// 'pl',
|
||||||
|
// 'pt-BR',
|
||||||
|
// 'pt',
|
||||||
|
// 'ru',
|
||||||
|
// 'sk',
|
||||||
|
// 'sv',
|
||||||
|
// 'th',
|
||||||
|
// 'tr',
|
||||||
|
// 'uk',
|
||||||
|
// 'vi',
|
||||||
|
// 'zh-Hans',
|
||||||
|
// 'zh',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
bootstrap(app) {
|
||||||
|
console.log(app);
|
||||||
|
},
|
||||||
|
};
|
13
packages/cms/src/admin/tsconfig.json
Normal file
13
packages/cms/src/admin/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "@strapi/typescript-utils/tsconfigs/admin",
|
||||||
|
"include": [
|
||||||
|
"../plugins/**/admin/src/**/*",
|
||||||
|
"./"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/",
|
||||||
|
"build/",
|
||||||
|
"dist/",
|
||||||
|
"**/*.test.ts"
|
||||||
|
]
|
||||||
|
}
|
9
packages/cms/src/admin/webpack.config.example.js
Normal file
9
packages/cms/src/admin/webpack.config.example.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
module.exports = (config, webpack) => {
|
||||||
|
// Note: we provide webpack above so you should not `require` it
|
||||||
|
// Perform customizations to webpack config
|
||||||
|
// Important: return the modified config
|
||||||
|
return config;
|
||||||
|
};
|
0
packages/cms/src/api/.gitkeep
Normal file
0
packages/cms/src/api/.gitkeep
Normal file
33
packages/cms/src/api/page/content-types/page/schema.json
Normal file
33
packages/cms/src/api/page/content-types/page/schema.json
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "pages",
|
||||||
|
"info": {
|
||||||
|
"singularName": "page",
|
||||||
|
"pluralName": "pages",
|
||||||
|
"displayName": "Page",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"block": {
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"regex": "[A-z0-9\\-]*",
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
"parent": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "oneToOne",
|
||||||
|
"target": "api::page.page"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
packages/cms/src/api/page/controllers/page.ts
Normal file
7
packages/cms/src/api/page/controllers/page.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* page controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi'
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::page.page');
|
7
packages/cms/src/api/page/routes/page.ts
Normal file
7
packages/cms/src/api/page/routes/page.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* page router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::page.page');
|
7
packages/cms/src/api/page/services/page.ts
Normal file
7
packages/cms/src/api/page/services/page.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* page service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::page.page');
|
0
packages/cms/src/extensions/.gitkeep
Normal file
0
packages/cms/src/extensions/.gitkeep
Normal file
18
packages/cms/src/index.ts
Normal file
18
packages/cms/src/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* An asynchronous register function that runs before
|
||||||
|
* your application is initialized.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to extend code.
|
||||||
|
*/
|
||||||
|
register(/*{ strapi }*/) {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An asynchronous bootstrap function that runs before
|
||||||
|
* your application gets started.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to set up your data model,
|
||||||
|
* run jobs, or perform some special logic.
|
||||||
|
*/
|
||||||
|
bootstrap(/*{ strapi }*/) {},
|
||||||
|
};
|
23
packages/cms/tsconfig.json
Normal file
23
packages/cms/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"extends": "@strapi/typescript-utils/tsconfigs/server",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./",
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.js",
|
||||||
|
"src/**/*.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/",
|
||||||
|
"build/",
|
||||||
|
"dist/",
|
||||||
|
".cache/",
|
||||||
|
".tmp/",
|
||||||
|
"src/admin/",
|
||||||
|
"**/*.test.*",
|
||||||
|
"src/plugins/**"
|
||||||
|
]
|
||||||
|
}
|
7
packages/core/__tests__/core.test.js
Normal file
7
packages/core/__tests__/core.test.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const core = require('..');
|
||||||
|
|
||||||
|
describe('@schlechtenburg/core', () => {
|
||||||
|
it('needs tests');
|
||||||
|
});
|
|
@ -1,33 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
import { withSetup } from '../../../test';
|
|
||||||
import { useActivation } from '..';
|
|
||||||
|
|
||||||
describe('@schlechtenburg/core', () => {
|
|
||||||
const a = 'a';
|
|
||||||
const b = 'b';
|
|
||||||
|
|
||||||
it('Should activate', async () => {
|
|
||||||
const {
|
|
||||||
activeBlockId,
|
|
||||||
isActive,
|
|
||||||
activate,
|
|
||||||
deactivate,
|
|
||||||
} = await withSetup(() => useActivation(a));
|
|
||||||
|
|
||||||
activate(a);
|
|
||||||
expect(activeBlockId.value).toBe(a);
|
|
||||||
expect(isActive.value).toBeTruthy();
|
|
||||||
|
|
||||||
activate(b);
|
|
||||||
expect(activeBlockId.value).toBe(b);
|
|
||||||
expect(isActive.value).toBeFalsy();
|
|
||||||
|
|
||||||
deactivate(activeBlockId.value);
|
|
||||||
expect(isActive.value).toBeFalsy();
|
|
||||||
expect(activeBlockId.value).toBe(null);
|
|
||||||
|
|
||||||
activate();
|
|
||||||
expect(activeBlockId.value).toBe(a);
|
|
||||||
expect(isActive.value).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,6 +2,7 @@
|
||||||
$block: &;
|
$block: &;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -164,6 +164,7 @@ export const SbBlock = defineComponent({
|
||||||
eventRemoveSelf={props.eventRemoveSelf}
|
eventRemoveSelf={props.eventRemoveSelf}
|
||||||
eventActivatePrevious={props.eventActivatePrevious}
|
eventActivatePrevious={props.eventActivatePrevious}
|
||||||
eventActivateNext={props.eventActivateNext}
|
eventActivateNext={props.eventActivateNext}
|
||||||
|
data-sb-block--content
|
||||||
|
|
||||||
{...{
|
{...{
|
||||||
onClick: ($event: MouseEvent) => {
|
onClick: ($event: MouseEvent) => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.sb-block-ordering {
|
.sb-block-ordering {
|
||||||
|
font-family: 'Montserrat';
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.sb-block-picker {
|
.sb-block-picker {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
|
||||||
&__add-button {
|
&__add-button {
|
||||||
padding: 24px 32px;
|
padding: 24px 32px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
.sb-block-placeholder {
|
.sb-block-placeholder {
|
||||||
flex-basis: 100%;
|
font-family: 'Montserrat';
|
||||||
flex-shrink: 2;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-basis: 1rem;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
|
||||||
&__add {
|
&__add {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.sb-block-toolbar {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
}
|
|
@ -1,9 +1,14 @@
|
||||||
.sb-button {
|
.sb-button {
|
||||||
|
font-family: 'Montserrat';
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
background-color: var(--grey-0);
|
background-color: var(--grey-0);
|
||||||
border: 1px solid var(--grey-2);
|
border: 1px solid var(--grey-2);
|
||||||
|
|
||||||
|
&_active {
|
||||||
|
box-shadow: inset 5px 5px 5px 5px black;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid var(--interact);
|
border: 1px solid var(--interact);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-context-menu {
|
.sb-context-menu {
|
||||||
|
font-family: 'Montserrat';
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--grey-0);
|
background: var(--grey-0);
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
|
$sb-style-root: '@schlechtenburg/style';
|
||||||
|
@import '@schlechtenburg/style/scss/montserrat.scss';
|
||||||
|
|
||||||
.sb-main {
|
.sb-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
|
padding: 0;
|
||||||
|
transition: padding 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
--grey-0: white;
|
--grey-0: white;
|
||||||
--grey-1-t: rgba(0, 0, 0, 0.05);
|
--grey-1-t: rgba(0, 0, 0, 0.05);
|
||||||
|
@ -25,6 +31,7 @@
|
||||||
--z-toolbar: 2000;
|
--z-toolbar: 2000;
|
||||||
--z-context-menu: 3000;
|
--z-context-menu: 3000;
|
||||||
--z-tree-block-select: 4000;
|
--z-tree-block-select: 4000;
|
||||||
|
--z-main-menu: 5000;
|
||||||
--z-modal: 10000;
|
--z-modal: 10000;
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
@ -32,4 +39,32 @@
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&_edit {
|
||||||
|
padding: 0rem 3rem 3rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--menu {
|
||||||
|
opacity: 1;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition-property: opacity, margin-bottom, transform;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
transition-timing-function: ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-active {
|
||||||
|
transition-delay: 0s 0s 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,11 @@ import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
provide,
|
provide,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
|
shallowRef,
|
||||||
ref,
|
ref,
|
||||||
watch,
|
watch,
|
||||||
|
computed,
|
||||||
|
Transition,
|
||||||
PropType,
|
PropType,
|
||||||
Ref,
|
Ref,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
@ -13,6 +16,8 @@ import {
|
||||||
IBlockLibrary,
|
IBlockLibrary,
|
||||||
ITreeNode,
|
ITreeNode,
|
||||||
OnUpdateBlockCb,
|
OnUpdateBlockCb,
|
||||||
|
IFormattingTool,
|
||||||
|
IFormattingToolLibrary,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { model } from '../block-helpers';
|
import { model } from '../block-helpers';
|
||||||
import { SymMode, SbMode } from '../mode';
|
import { SymMode, SbMode } from '../mode';
|
||||||
|
@ -24,13 +29,20 @@ import {
|
||||||
} from '../use-block-tree';
|
} from '../use-block-tree';
|
||||||
import { SymEditorDimensions, useResizeObserver } from '../use-resize-observer';
|
import { SymEditorDimensions, useResizeObserver } from '../use-resize-observer';
|
||||||
import { SymActiveBlock } from '../use-activation';
|
import { SymActiveBlock } from '../use-activation';
|
||||||
|
import { SymRootElement } from '../use-root-element';
|
||||||
|
import {
|
||||||
|
SbFormattingToolbar,
|
||||||
|
SymFormattingToolLibrary,
|
||||||
|
SymFormattingToolbarClients,
|
||||||
|
SymFormattingToolbarActiveClient,
|
||||||
|
} from '../rich-text';
|
||||||
|
|
||||||
import { SbMainMenu } from './MainMenu';
|
import { SbMainMenu } from './MainMenu';
|
||||||
import { SbBlockToolbar } from './BlockToolbar';
|
|
||||||
import { SbBlock } from './Block';
|
import { SbBlock } from './Block';
|
||||||
|
|
||||||
export interface ISbMainProps {
|
export interface ISbMainProps {
|
||||||
availableBlocks: IBlockDefinition<any>[];
|
availableBlocks: IBlockDefinition<any>[];
|
||||||
|
availableFormattingTools: IFormattingTool[];
|
||||||
block: IBlockData<any>;
|
block: IBlockData<any>;
|
||||||
eventUpdate: OnUpdateBlockCb;
|
eventUpdate: OnUpdateBlockCb;
|
||||||
mode: SbMode;
|
mode: SbMode;
|
||||||
|
@ -48,6 +60,10 @@ export const SbMain = defineComponent({
|
||||||
type: Array as PropType<IBlockDefinition<any>[]>,
|
type: Array as PropType<IBlockDefinition<any>[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
availableFormattingTools: {
|
||||||
|
type: Array as PropType<IFormattingTool[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
block: {
|
block: {
|
||||||
type: Object as PropType<IBlockData<any>>,
|
type: Object as PropType<IBlockData<any>>,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -71,6 +87,12 @@ export const SbMain = defineComponent({
|
||||||
setup(props: ISbMainProps) {
|
setup(props: ISbMainProps) {
|
||||||
const el: Ref<null|HTMLElement> = ref(null);
|
const el: Ref<null|HTMLElement> = ref(null);
|
||||||
useResizeObserver(el, SymEditorDimensions);
|
useResizeObserver(el, SymEditorDimensions);
|
||||||
|
provide(SymRootElement, el);
|
||||||
|
|
||||||
|
const inlineClients = shallowRef([]);
|
||||||
|
provide(SymFormattingToolbarClients, inlineClients);
|
||||||
|
|
||||||
|
provide(SymFormattingToolbarActiveClient, ref(null));
|
||||||
|
|
||||||
const mode = ref(props.mode);
|
const mode = ref(props.mode);
|
||||||
provide(SymMode, mode);
|
provide(SymMode, mode);
|
||||||
|
@ -79,6 +101,11 @@ export const SbMain = defineComponent({
|
||||||
mode.value = newMode;
|
mode.value = newMode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'sb-main': true,
|
||||||
|
[`sb-main_${mode.value}`]: true,
|
||||||
|
}));
|
||||||
|
|
||||||
const activeBlock = ref(null);
|
const activeBlock = ref(null);
|
||||||
provide(SymActiveBlock, activeBlock);
|
provide(SymActiveBlock, activeBlock);
|
||||||
|
|
||||||
|
@ -93,26 +120,38 @@ export const SbMain = defineComponent({
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
provide(SymBlockLibrary, blockLibrary);
|
provide(SymBlockLibrary, blockLibrary);
|
||||||
|
|
||||||
|
const inlineToolLibrary: IFormattingToolLibrary = shallowReactive({
|
||||||
|
...props.availableFormattingTools.reduce(
|
||||||
|
(tools: IFormattingToolLibrary, tool: IFormattingTool) => ({ ...tools, [tool.name]: tool }),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
provide(SymFormattingToolLibrary, inlineToolLibrary);
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<div
|
<div
|
||||||
class="sb-main"
|
class={classes.value}
|
||||||
ref={el}
|
ref={el}
|
||||||
>
|
>
|
||||||
{
|
<Transition
|
||||||
mode.value === SbMode.Edit
|
name="sb-main--menu"
|
||||||
? <>
|
mode="out-in"
|
||||||
<SbMainMenu block={props.block} />
|
>
|
||||||
<SbBlockToolbar />
|
{mode.value === SbMode.Edit
|
||||||
</>
|
? <SbMainMenu
|
||||||
: null
|
block={props.block}
|
||||||
}
|
class="sb-main--menu"
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
</Transition>
|
||||||
<SbBlock
|
<SbBlock
|
||||||
|
class="sb-main--block"
|
||||||
block={props.block}
|
block={props.block}
|
||||||
eventUpdate={props.eventUpdate}
|
eventUpdate={props.eventUpdate}
|
||||||
/>
|
/>
|
||||||
|
<SbFormattingToolbar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
.sb-main-menu {
|
.sb-main-menu {
|
||||||
|
font-family: 'Montserrat';
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: 4rem;
|
|
||||||
background-color: var(--grey-0);
|
background-color: var(--grey-0);
|
||||||
|
position: sticky;
|
||||||
|
z-index: var(--z-main-menu);
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
PropType,
|
PropType,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import { IBlockData } from '../types';
|
import { IBlockData } from '../types';
|
||||||
|
import { SbBlockToolbar } from './BlockToolbar';
|
||||||
import { SbTreeBlockSelect } from './TreeBlockSelect';
|
import { SbTreeBlockSelect } from './TreeBlockSelect';
|
||||||
|
|
||||||
import './MainMenu.scss';
|
import './MainMenu.scss';
|
||||||
|
@ -21,6 +22,7 @@ export const SbMainMenu = defineComponent({
|
||||||
return () => (
|
return () => (
|
||||||
<div class="sb-main-menu">
|
<div class="sb-main-menu">
|
||||||
<SbTreeBlockSelect />
|
<SbTreeBlockSelect />
|
||||||
|
<SbBlockToolbar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
.sb-missing-block {
|
.sb-missing-block {
|
||||||
|
font-family: 'Montserrat';
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.sb-modal {
|
.sb-modal {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
|
||||||
&__overlay {
|
&__overlay {
|
||||||
background-color: var(--grey-3-t);
|
background-color: var(--grey-3-t);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.sb-select {
|
.sb-select {
|
||||||
|
font-family: 'Montserrat';
|
||||||
background-color: var(--grey-0);
|
background-color: var(--grey-0);
|
||||||
border: 1px solid var(--grey-2);
|
border: 1px solid var(--grey-2);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.sb-toolbar {
|
.sb-toolbar {
|
||||||
|
font-family: 'Montserrat';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
.sb-tree-block-select {
|
.sb-tree-block-select {
|
||||||
|
font-family: 'Montserrat';
|
||||||
|
|
||||||
&__list {
|
&__list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export * from './mode';
|
export * from './mode';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
|
export * from './rich-text';
|
||||||
|
|
||||||
export * from './block-helpers';
|
export * from './block-helpers';
|
||||||
|
|
||||||
export * from './use-activation';
|
export * from './use-activation';
|
||||||
|
@ -8,10 +10,12 @@ export * from './use-dynamic-blocks';
|
||||||
export * from './use-resize-observer';
|
export * from './use-resize-observer';
|
||||||
|
|
||||||
export * from './components/Main';
|
export * from './components/Main';
|
||||||
|
|
||||||
export * from './components/Block';
|
export * from './components/Block';
|
||||||
export * from './components/BlockPicker';
|
export * from './components/BlockPicker';
|
||||||
export * from './components/BlockOrdering';
|
export * from './components/BlockOrdering';
|
||||||
export * from './components/BlockPlaceholder';
|
export * from './components/BlockPlaceholder';
|
||||||
|
|
||||||
export * from './components/Toolbar';
|
export * from './components/Toolbar';
|
||||||
export * from './components/Button';
|
export * from './components/Button';
|
||||||
export * from './components/Select';
|
export * from './components/Select';
|
||||||
|
|
12
packages/core/lib/rich-text/FormattingToolbar.scss
Normal file
12
packages/core/lib/rich-text/FormattingToolbar.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.sb-inline-toolbar {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--bg);
|
||||||
|
height: 2rem;
|
||||||
|
min-width: 2rem;
|
||||||
|
display: flex;
|
||||||
|
z-index: var(--z-toolbar);
|
||||||
|
|
||||||
|
&_hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
124
packages/core/lib/rich-text/FormattingToolbar.tsx
Normal file
124
packages/core/lib/rich-text/FormattingToolbar.tsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
defineComponent,
|
||||||
|
computed,
|
||||||
|
} from 'vue';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { useRootElement } from '../use-root-element';
|
||||||
|
import { useFormattingToolbar } from './use-formatting-toolbar';
|
||||||
|
import {
|
||||||
|
getSelection,
|
||||||
|
getAnchorElementForSelection,
|
||||||
|
getRangeFromSelection,
|
||||||
|
getRect,
|
||||||
|
} from './selection';
|
||||||
|
|
||||||
|
import './FormattingToolbar.scss';
|
||||||
|
|
||||||
|
export const SbFormattingToolbar = defineComponent({
|
||||||
|
name: 'sb-inlinetoolbar',
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const { rootElement } = useRootElement();
|
||||||
|
const {
|
||||||
|
activeClient,
|
||||||
|
setActiveClient,
|
||||||
|
clients,
|
||||||
|
getAllTools,
|
||||||
|
} = useFormattingToolbar();
|
||||||
|
|
||||||
|
const allTools = computed(() => getAllTools());
|
||||||
|
|
||||||
|
const selection: Ref<Selection|null>= ref(null);
|
||||||
|
const selectionRect: Ref<DOMRect|null>= ref(null);
|
||||||
|
const updateSelectionRect = () => {
|
||||||
|
selectionRect.value = getRect(selection.value!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showing: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
showing.value = true;
|
||||||
|
updateSelectionRect();
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
showing.value = false;
|
||||||
|
setActiveClient(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = computed(() => {
|
||||||
|
const rootRect = rootElement.value?.getBoundingClientRect();
|
||||||
|
const x = (selectionRect.value?.x || 0)
|
||||||
|
- (rootRect?.left || 0);
|
||||||
|
const y = (selectionRect.value?.y || 0)
|
||||||
|
+ (selectionRect.value?.height || 0)
|
||||||
|
- (rootRect?.top || 0);
|
||||||
|
return {
|
||||||
|
left: Math.floor(x) + 'px',
|
||||||
|
top: Math.floor(y) + 'px',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'sb-inline-toolbar': true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSelectionChanged = debounce(() => {
|
||||||
|
selection.value = getSelection();
|
||||||
|
if (!selection.value) {
|
||||||
|
console.warn('Could not get selection');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = getRangeFromSelection(selection.value);
|
||||||
|
// If we're not selecting anything, bail
|
||||||
|
if (!range || range.endOffset === range.startOffset) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusedElement = document.activeElement;
|
||||||
|
if (!focusedElement) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If new selection is not in registered clients, close it
|
||||||
|
if (!clients.value.find(client => client === focusedElement)) {
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveClient(document.activeElement as HTMLElement|null);
|
||||||
|
|
||||||
|
show();
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
rootElement.value?.addEventListener('click', close);
|
||||||
|
document.addEventListener('selectionchange', onSelectionChanged, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
rootElement.value?.removeEventListener('click', close);
|
||||||
|
document.removeEventListener('selectionchange', onSelectionChanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (allTools.value.length && showing.value)
|
||||||
|
? <div
|
||||||
|
style={style.value}
|
||||||
|
class={classes.value}
|
||||||
|
onClick={(event:MouseEvent) => { event.stopPropagation(); }}
|
||||||
|
>{allTools.value.map((tool) => {
|
||||||
|
const ToolUI = tool.ui as any;
|
||||||
|
return <ToolUI
|
||||||
|
activeClient={activeClient}
|
||||||
|
selection={selection.value}
|
||||||
|
></ToolUI>;
|
||||||
|
})}</div>
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
});
|
69
packages/core/lib/rich-text/RichText.tsx
Normal file
69
packages/core/lib/rich-text/RichText.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
h,
|
||||||
|
defineComponent,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
PropType,
|
||||||
|
computed,
|
||||||
|
watchEffect,
|
||||||
|
} from 'vue';
|
||||||
|
import { useFormattingToolbar } from './use-formatting-toolbar';
|
||||||
|
|
||||||
|
export const SbRichText = defineComponent({
|
||||||
|
name: 'sb-rich-text',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
inputRef: {
|
||||||
|
type: (null as unknown) as PropType<Ref<HTMLElement|null>>,
|
||||||
|
default: ref(null),
|
||||||
|
},
|
||||||
|
tag: { type: String, default: 'p' },
|
||||||
|
|
||||||
|
value: { type: String, default: '' },
|
||||||
|
|
||||||
|
onValueChange: {
|
||||||
|
type: (null as unknown) as PropType<(value: string) => void>,
|
||||||
|
default: (_:string) => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const {
|
||||||
|
registerClient,
|
||||||
|
unregisterClient,
|
||||||
|
} = useFormattingToolbar();
|
||||||
|
|
||||||
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (['Delete', 'Backspace'].indexOf(event.key) < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.inputRef.value?.textContent === '') {
|
||||||
|
props.inputRef.value.innerHTML = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
registerClient(props.inputRef.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unregisterClient(props.inputRef.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => h(
|
||||||
|
props.tag,
|
||||||
|
{
|
||||||
|
class: 'sb-contenteditable',
|
||||||
|
contenteditable: 'true',
|
||||||
|
ref: props.inputRef,
|
||||||
|
onKeydown,
|
||||||
|
onInput: () => {
|
||||||
|
props.onValueChange(props.inputRef.value?.innerHTML || '');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
37
packages/core/lib/rich-text/dom.ts
Normal file
37
packages/core/lib/rich-text/dom.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright notice:
|
||||||
|
*
|
||||||
|
* Large parts of this file are heavily inspired if not downright copied from editor.js,
|
||||||
|
* copyright MIT.
|
||||||
|
* https://editorjs.io/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if object is DOM node
|
||||||
|
*
|
||||||
|
* @param {*} node - object to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isElement = (node: any): node is Element => {
|
||||||
|
if (node instanceof Number) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if object is DocumentFragment node
|
||||||
|
*
|
||||||
|
* @param {object} node - object to check
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isFragment = (node: any): node is DocumentFragment => {
|
||||||
|
if (node instanceof Number) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return node && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||||
|
}
|
5
packages/core/lib/rich-text/index.ts
Normal file
5
packages/core/lib/rich-text/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './FormattingToolbar';
|
||||||
|
export * from './RichText';
|
||||||
|
export * from './use-formatting-toolbar';
|
||||||
|
export * from './selection';
|
||||||
|
export * from './dom';
|
93
packages/core/lib/rich-text/selection.ts
Normal file
93
packages/core/lib/rich-text/selection.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright notice:
|
||||||
|
*
|
||||||
|
* Large parts of this file are heavily inspired if not downright copied from editor.js,
|
||||||
|
* copyright MIT.
|
||||||
|
* https://editorjs.io/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isElement } from './dom';
|
||||||
|
|
||||||
|
export const getSelection = globalThis.getSelection ? globalThis.getSelection : () => null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns range from passed Selection object
|
||||||
|
*
|
||||||
|
* @param selection - Selection object to get Range from
|
||||||
|
*/
|
||||||
|
export const getRangeFromSelection = (selection: Selection): Range|null => {
|
||||||
|
return selection && selection.rangeCount ? selection.getRangeAt(0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns selected anchor element
|
||||||
|
*
|
||||||
|
* @returns {Element|null}
|
||||||
|
*/
|
||||||
|
export const getAnchorElementForSelection = (selection: Selection): Element | null => {
|
||||||
|
const anchorNode = selection.anchorNode;
|
||||||
|
|
||||||
|
if (!anchorNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isElement(anchorNode)) {
|
||||||
|
return anchorNode.parentElement;
|
||||||
|
} else {
|
||||||
|
return anchorNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates position and size of selected text
|
||||||
|
*
|
||||||
|
* @returns {DOMRect}
|
||||||
|
*/
|
||||||
|
export const getRect = (selection: Selection): DOMRect => {
|
||||||
|
const defaultRect = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
} as DOMRect;
|
||||||
|
|
||||||
|
if (selection.rangeCount === null || isNaN(selection.rangeCount)) {
|
||||||
|
console.warn('Method SelectionUtils.rangeCount is not supported');
|
||||||
|
return defaultRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.rangeCount === 0) {
|
||||||
|
return defaultRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0).cloneRange() as Range;
|
||||||
|
|
||||||
|
let rect = { ...defaultRect };
|
||||||
|
|
||||||
|
if (range.getBoundingClientRect) {
|
||||||
|
rect = range.getBoundingClientRect() as DOMRect;
|
||||||
|
}
|
||||||
|
// Fall back to inserting a temporary element
|
||||||
|
if (rect.x === 0 && rect.y === 0) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
|
||||||
|
if (span.getBoundingClientRect) {
|
||||||
|
// Ensure span has dimensions and position by
|
||||||
|
// adding a zero-width space character
|
||||||
|
span.appendChild(document.createTextNode('\u200b'));
|
||||||
|
range.insertNode(span);
|
||||||
|
rect = span.getBoundingClientRect() as DOMRect;
|
||||||
|
|
||||||
|
const spanParent = span.parentNode;
|
||||||
|
if (spanParent) {
|
||||||
|
spanParent.removeChild(span);
|
||||||
|
|
||||||
|
// Glue any broken text nodes back together
|
||||||
|
spanParent.normalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
};
|
58
packages/core/lib/rich-text/use-formatting-toolbar.ts
Normal file
58
packages/core/lib/rich-text/use-formatting-toolbar.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
Ref,
|
||||||
|
inject,
|
||||||
|
reactive,
|
||||||
|
} from 'vue';
|
||||||
|
import { IFormattingToolLibrary } from '../types';
|
||||||
|
|
||||||
|
export const SymFormattingToolbarActiveClient = Symbol('Schlechtenburg inline active client');
|
||||||
|
export const SymFormattingToolbarClients = Symbol('Schlechtenburg inline toolbar client elements');
|
||||||
|
export const SymFormattingToolLibrary = Symbol('Schlechtenburg inline tool library');
|
||||||
|
|
||||||
|
export const useFormattingToolbar = () => {
|
||||||
|
const clients: Ref<HTMLElement[]> = inject(SymFormattingToolbarClients, ref([]));
|
||||||
|
const activeClient: Ref<HTMLElement|null> = inject(SymFormattingToolbarActiveClient, ref(null));
|
||||||
|
|
||||||
|
const setActiveClient = (client: HTMLElement|null) => {
|
||||||
|
activeClient.value = client;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setClients = (newClients: HTMLElement[]) => {
|
||||||
|
clients.value = newClients;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientIsRegistered = (el: HTMLElement) => !!clients.value.find(cl => cl === el);
|
||||||
|
|
||||||
|
const registerClient = (el: HTMLElement) => {
|
||||||
|
if (clientIsRegistered(el)) {
|
||||||
|
console.warn('Not reregistering toolbar client that is already registered:', el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setClients([
|
||||||
|
...clients.value,
|
||||||
|
el,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unregisterClient = (el: HTMLElement) => {
|
||||||
|
setClients(clients.value.filter(cl => cl !== el));
|
||||||
|
};
|
||||||
|
|
||||||
|
const tools: IFormattingToolLibrary = inject(SymFormattingToolLibrary, reactive({}));
|
||||||
|
const getTool = (name: string) => tools[name];
|
||||||
|
const getAllTools = () => Object.keys(tools).map(name => tools[name]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
clients,
|
||||||
|
activeClient,
|
||||||
|
setActiveClient,
|
||||||
|
registerClient,
|
||||||
|
unregisterClient,
|
||||||
|
|
||||||
|
tools,
|
||||||
|
getTool,
|
||||||
|
getAllTools,
|
||||||
|
};
|
||||||
|
};
|
261
packages/core/lib/rich-text/values.tsx
Normal file
261
packages/core/lib/rich-text/values.tsx
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
import defaultTo from 'lodash/defaultTo';
|
||||||
|
import { IFormattingTool } from '../types';
|
||||||
|
|
||||||
|
export interface IRichTextFormat {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRichTextValue {
|
||||||
|
text: string;
|
||||||
|
formats: IRichTextFormat[][];
|
||||||
|
start: number|null;
|
||||||
|
end: number|null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findToolForElement = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
element: Element,
|
||||||
|
): IFormattingTool|null => tools.find(tool => {
|
||||||
|
if (tool.tagName && element.tagName !== tool.tagName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tool.className && !element.classList.contains(tool.className)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}) || null;
|
||||||
|
|
||||||
|
export const createFromString = (
|
||||||
|
value: string = '',
|
||||||
|
formats: IRichTextFormat[] = [],
|
||||||
|
): IRichTextValue => ({
|
||||||
|
text: value,
|
||||||
|
formats: (new Array(value.length)).fill([...formats]),
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createFromDOMNodeRecursively = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
node: ChildNode,
|
||||||
|
formats: IRichTextFormat[] = [],
|
||||||
|
): IRichTextValue => {
|
||||||
|
if (node.nodeType === node.TEXT_NODE) {
|
||||||
|
return createFromString(node.textContent || '', formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeType === node.ELEMENT_NODE) {
|
||||||
|
return createFromDOMElement(tools, node as Element, formats);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createFromString('', []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFromDOMElement = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
element: Element,
|
||||||
|
formats: IRichTextFormat[] = [],
|
||||||
|
): IRichTextValue => {
|
||||||
|
const tool = findToolForElement(tools, element);
|
||||||
|
const subFormats = tool
|
||||||
|
? [
|
||||||
|
...formats.filter(f => f.type !== tool.name),
|
||||||
|
{ type: tool.name },
|
||||||
|
]
|
||||||
|
: [ ...formats ];
|
||||||
|
|
||||||
|
const nodes = Array.from(element.childNodes);
|
||||||
|
return concat(...nodes.map(node => createFromDOMNodeRecursively(tools, node, subFormats)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFromDOMString = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
value: string,
|
||||||
|
formats: IRichTextFormat[] = [],
|
||||||
|
): IRichTextValue => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = value;
|
||||||
|
return createFromDOMElement(tools, div, formats);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const create = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
value: Element|string = '',
|
||||||
|
): IRichTextValue => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return createFromDOMString(tools, value);
|
||||||
|
}
|
||||||
|
return createFromDOMElement(tools, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toHTMLString = (
|
||||||
|
tools: IFormattingTool[],
|
||||||
|
value: IRichTextValue,
|
||||||
|
) => {
|
||||||
|
const tools = [...tools].sort((a, b) => {
|
||||||
|
const tensionA = defaultTo(a.surfaceTension, Infinity);
|
||||||
|
const tensionB = defaultTo(b.surfaceTension, Infinity);
|
||||||
|
return tensionA - tensionB;
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementTree = [];
|
||||||
|
const string = '';
|
||||||
|
for (let i = 0; i < value.text.length; i++) {
|
||||||
|
const c = value.text[i];
|
||||||
|
const formats = value.formats[i];
|
||||||
|
const activeFormats = value.formats[i - 1] || [];
|
||||||
|
|
||||||
|
const removedFormats = activeFormats
|
||||||
|
.filter(a => !formats
|
||||||
|
.find(f => f.type === a.type
|
||||||
|
&& JSON.stringify(f) === JSON.stringify(a)));
|
||||||
|
const addedFormats = formats
|
||||||
|
.filter(f => !activeFormats
|
||||||
|
.find(a => a.type === f.type
|
||||||
|
&& JSON.stringify(a) === JSON.stringify(f)));
|
||||||
|
|
||||||
|
console.log(c);
|
||||||
|
for (let removedFormat of removedFormats) {
|
||||||
|
const tool = tools.find(tool => tool.name === removedFormat.type);
|
||||||
|
if (!tool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tool.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getActiveFormats = (richTextValue: IRichTextValue): IRichTextFormat[] =>
|
||||||
|
richTextValue.start !== null
|
||||||
|
? richTextValue.formats[richTextValue.start]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
export const applyFormat = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
format: IRichTextFormat,
|
||||||
|
startIndex?: number,
|
||||||
|
endIndex?: number,
|
||||||
|
): IRichTextValue => {
|
||||||
|
const start = defaultTo(defaultTo(startIndex, value.start), 0);
|
||||||
|
const end = defaultTo(defaultTo(endIndex, value.end), value.text.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
formats: [
|
||||||
|
...value.formats.slice(0, start),
|
||||||
|
...value.formats.slice(start, end).map(letterFormatList => [
|
||||||
|
...letterFormatList.filter(letterFormat => letterFormat.type === format.type),
|
||||||
|
format,
|
||||||
|
]),
|
||||||
|
...value.formats.slice(end),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeFormat = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
format: IRichTextFormat,
|
||||||
|
startIndex?: number,
|
||||||
|
endIndex?: number,
|
||||||
|
): IRichTextValue => {
|
||||||
|
const start = defaultTo(defaultTo(startIndex, value.start), 0);
|
||||||
|
const end = defaultTo(defaultTo(endIndex, value.end), value.text.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
formats: [
|
||||||
|
...value.formats.slice(0, start),
|
||||||
|
...value.formats.slice(start, end)
|
||||||
|
.map(letterFormatList => letterFormatList.filter(letterFormat => letterFormat.type === format.type)),
|
||||||
|
...value.formats.slice(end),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleFormat = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
format: IRichTextFormat,
|
||||||
|
): IRichTextValue => {
|
||||||
|
const activeFormats = getActiveFormats(value);
|
||||||
|
if (activeFormats.find(f => f.type === format.type)) {
|
||||||
|
return removeFormat(value, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyFormat(value, format);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const concat = (...richTextValues:IRichTextValue[]): IRichTextValue => richTextValues
|
||||||
|
.reduce((newValue, value) => ({
|
||||||
|
text: newValue.text + value.text,
|
||||||
|
formats: [
|
||||||
|
...newValue.formats,
|
||||||
|
...value.formats,
|
||||||
|
],
|
||||||
|
start: newValue.start !== null ? newValue.start : value.start,
|
||||||
|
end: value.end !== null ? value.end : newValue.end,
|
||||||
|
}), { text: '', formats: [], start: null, end: null });
|
||||||
|
|
||||||
|
export const join = (
|
||||||
|
richTextValues:IRichTextValue[],
|
||||||
|
separator?: string|IRichTextValue,
|
||||||
|
): IRichTextValue => richTextValues
|
||||||
|
.reduce((total, value) => concat(
|
||||||
|
total,
|
||||||
|
...(separator
|
||||||
|
? [typeof separator === 'string' ? createFromString(separator) : separator]
|
||||||
|
: []),
|
||||||
|
value,
|
||||||
|
), createFromString());
|
||||||
|
|
||||||
|
export const slice = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
startIndex?: number,
|
||||||
|
endIndex?: number,
|
||||||
|
): IRichTextValue => {
|
||||||
|
const start = defaultTo(defaultTo(startIndex, value.start), 0);
|
||||||
|
const end = defaultTo(defaultTo(endIndex, value.end), value.text.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: value.text.slice(start, end),
|
||||||
|
formats: value.formats.slice(start, end),
|
||||||
|
start: value.start !== null ? (value.start >= start ? value.start - start : null) : null,
|
||||||
|
end: value.end !== null ? (value.end <= end ? end - value.end : null) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insert = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
valueToInsert: IRichTextValue|string,
|
||||||
|
startIndex?: number,
|
||||||
|
endIndex?: number,
|
||||||
|
) => {
|
||||||
|
const start = defaultTo(defaultTo(startIndex, value.start), value.text.length);
|
||||||
|
const end = defaultTo(defaultTo(endIndex, value.end), value.text.length);
|
||||||
|
|
||||||
|
return concat(
|
||||||
|
slice(value, 0, start),
|
||||||
|
typeof valueToInsert === 'string' ? createFromString(valueToInsert) : valueToInsert,
|
||||||
|
slice(value, end, value.text.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const split = (
|
||||||
|
value: IRichTextValue,
|
||||||
|
valueToInsert: IRichTextValue|string,
|
||||||
|
startIndex?: number,
|
||||||
|
endIndex?: number,
|
||||||
|
) => {
|
||||||
|
const start = defaultTo(defaultTo(startIndex, value.start), value.text.length);
|
||||||
|
const end = defaultTo(defaultTo(endIndex, value.end), value.text.length);
|
||||||
|
|
||||||
|
return concat(
|
||||||
|
slice(value, 0, start),
|
||||||
|
typeof valueToInsert === 'string' ? createFromString(valueToInsert) : valueToInsert,
|
||||||
|
slice(value, end, value.text.length),
|
||||||
|
);
|
||||||
|
}
|
|
@ -170,3 +170,21 @@ export interface IBlockDefinition<T> {
|
||||||
export interface IBlockLibrary {
|
export interface IBlockLibrary {
|
||||||
[name: string]: IBlockDefinition<any>;
|
[name: string]: IBlockDefinition<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFormattingTool {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
tagName?: string;
|
||||||
|
className?: string;
|
||||||
|
edit: Component;
|
||||||
|
surfaceTension?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schlechtenburg maintains a library of formatting tools that are available
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface IFormattingToolLibrary {
|
||||||
|
[name: string]: IFormattingTool;
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
|
|
||||||
export const SymActiveBlock = Symbol('Schlechtenburg active block');
|
export const SymActiveBlock = Symbol('Schlechtenburg active block');
|
||||||
export function useActivation(currentBlockId: string|null = null) {
|
export const useActivation = (currentBlockId: string|null = null) => {
|
||||||
const activeBlockId: Ref<string|null> = inject(SymActiveBlock, ref(null));
|
const activeBlockId: Ref<string|null> = inject(SymActiveBlock, ref(null));
|
||||||
|
|
||||||
const isActive = computed(() => activeBlockId.value === currentBlockId);
|
const isActive = computed(() => activeBlockId.value === currentBlockId);
|
||||||
|
@ -46,4 +46,4 @@ export function useActivation(currentBlockId: string|null = null) {
|
||||||
deactivate,
|
deactivate,
|
||||||
requestActivation,
|
requestActivation,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -10,14 +10,12 @@ import {
|
||||||
ITreeNode,
|
ITreeNode,
|
||||||
IBlockData,
|
IBlockData,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { useDynamicBlocks } from './use-dynamic-blocks';
|
|
||||||
import { SbMode } from './mode';
|
|
||||||
|
|
||||||
export const SymBlockTree= Symbol('Schlechtenburg block tree');
|
export const SymBlockTree= Symbol('Schlechtenburg block tree');
|
||||||
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
|
export const SymBlockTreeRegister = Symbol('Schlechtenburg block tree register');
|
||||||
export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregister');
|
export const SymBlockTreeUnregister = Symbol('Schlechtenburg block tree unregister');
|
||||||
|
|
||||||
export function useBlockTree() {
|
export const useBlockTree = () => {
|
||||||
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
|
const blockTree: Ref<ITreeNode|null> = inject(SymBlockTree, ref(null));
|
||||||
const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
|
const registerWithParent = inject(SymBlockTreeRegister, (_b: ITreeNode, _i: number) => {});
|
||||||
const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
|
const unregisterWithParent = inject(SymBlockTreeUnregister, (_b: ITreeNode) => {});
|
||||||
|
@ -52,18 +50,11 @@ export function useBlockTree() {
|
||||||
self.children = self.children.filter((child: ITreeNode) => child.id !== id);
|
self.children = self.children.filter((child: ITreeNode) => child.id !== id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mode } = useDynamicBlocks();
|
|
||||||
|
|
||||||
const register = (block: IBlockData<any>, index: number = 0) => {
|
const register = (block: IBlockData<any>, index: number = 0) => {
|
||||||
if (!block.id) {
|
if (!block.id) {
|
||||||
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
|
throw new Error(`Cannot register a block without an id: ${JSON.stringify(block)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode.value !== SbMode.Edit) {
|
|
||||||
console.warn('Ignoring block tree registration requests outside of edit mode.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.id = block.id;
|
self.id = block.id;
|
||||||
self.name = block.name;
|
self.name = block.name;
|
||||||
|
|
||||||
|
@ -82,4 +73,4 @@ export function useBlockTree() {
|
||||||
blockTree,
|
blockTree,
|
||||||
register,
|
register,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface BlockRect {
|
||||||
|
|
||||||
export const SymBlockDimensions = Symbol('Schlechtenburg block dimensions');
|
export const SymBlockDimensions = Symbol('Schlechtenburg block dimensions');
|
||||||
export const SymEditorDimensions = Symbol('Schlechtenburg editor dimensions');
|
export const SymEditorDimensions = Symbol('Schlechtenburg editor dimensions');
|
||||||
export function useResizeObserver(el: Ref<null|HTMLElement>, symbol: symbol) {
|
export const useResizeObserver = (el: Ref<null|HTMLElement>, symbol: symbol) => {
|
||||||
const dimensions: Ref<null|BlockRect> = ref(null);
|
const dimensions: Ref<null|BlockRect> = ref(null);
|
||||||
provide(symbol, dimensions);
|
provide(symbol, dimensions);
|
||||||
const triggerSizeCalculation = () => {
|
const triggerSizeCalculation = () => {
|
||||||
|
@ -47,7 +47,7 @@ export function useResizeObserver(el: Ref<null|HTMLElement>, symbol: symbol) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return { triggerSizeCalculation, dimensions };
|
return { triggerSizeCalculation, dimensions };
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useBlockSizing() {
|
export function useBlockSizing() {
|
||||||
const editorDimensions: Ref<BlockRect|null> = inject(SymEditorDimensions, ref(null));
|
const editorDimensions: Ref<BlockRect|null> = inject(SymEditorDimensions, ref(null));
|
||||||
|
|
11
packages/core/lib/use-root-element.ts
Normal file
11
packages/core/lib/use-root-element.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import {
|
||||||
|
Ref,
|
||||||
|
ref,
|
||||||
|
inject,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
export const SymRootElement = Symbol('Schlechtenburg root element');
|
||||||
|
export const useRootElement = () => {
|
||||||
|
const rootElement: Ref<HTMLElement|null> = inject(SymRootElement, ref(null));
|
||||||
|
return { rootElement };
|
||||||
|
};
|
6699
packages/core/package-lock.json
generated
6699
packages/core/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,13 +7,12 @@
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"main": "lib/index.ts",
|
"main": "lib/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run json-to-md:watch",
|
|
||||||
"typecheck": "vuedx-typecheck --no-pretty ./lib",
|
"typecheck": "vuedx-typecheck --no-pretty ./lib",
|
||||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||||
},
|
},
|
||||||
"directories": {
|
"directories": {
|
||||||
"doc": "docs",
|
"lib": "lib",
|
||||||
"lib": "lib"
|
"test": "__tests__"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib",
|
"lib",
|
||||||
|
@ -27,6 +26,8 @@
|
||||||
"url": "git@git.b12f.io:b12f/schlechtenburg.git"
|
"url": "git@git.b12f.io:b12f/schlechtenburg.git"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@schlechtenburg/style": "^0.0.0",
|
||||||
|
"@wordpress/rich-text": "^6.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
|
@ -36,8 +37,9 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash-es": "^4.17.4",
|
"@types/lodash-es": "^4.17.4",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
|
"@types/wordpress__rich-text": "^3.4.6",
|
||||||
"@vuedx/typecheck": "^0.6.3",
|
"@vuedx/typecheck": "^0.6.3",
|
||||||
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
"@vuedx/typescript-plugin-vue": "^0.6.3",
|
||||||
"vue": "^3.4.23"
|
"vue": "^3.2.31"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
8
packages/example-site/.gitignore
vendored
Normal file
8
packages/example-site/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
42
packages/example-site/README.md
Normal file
42
packages/example-site/README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Nuxt 3 Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install --shamefully-hoist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on http://localhost:3000
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
79
packages/example-site/app.scss
Normal file
79
packages/example-site/app.scss
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-app {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
--ex-nav-mobile-width: 60px;
|
||||||
|
--ex-nav-desktop-width: 320px;
|
||||||
|
|
||||||
|
--ex-nav-width: var(--ex-nav-mobile-width);
|
||||||
|
|
||||||
|
--interact: #3f9cff;
|
||||||
|
--interact-lite: #3f9cff;
|
||||||
|
|
||||||
|
--grey-0: white;
|
||||||
|
--grey-1-t: rgba(0, 0, 0, 0.05);
|
||||||
|
--grey-1: rgb(242, 242, 242);
|
||||||
|
--grey-2-t: rgba(0, 0, 0, 0.1);
|
||||||
|
--grey-2: rgb(230, 230, 230);
|
||||||
|
--grey-3-t: rgba(0, 0, 0, 0.2);
|
||||||
|
--grey-3: rgb(205, 205, 205);
|
||||||
|
--grey-4-t: rgba(0, 0, 0, 0.4);
|
||||||
|
--grey-4: rgb(155, 155, 155);
|
||||||
|
--grey-5-t: rgba(0, 0, 0, 0.7);
|
||||||
|
--grey-5: rgb(75, 75, 75);
|
||||||
|
--black: rgba(0, 0, 0, 0.9);
|
||||||
|
|
||||||
|
--bg: var(--grey-1);
|
||||||
|
--fg: var(--black);
|
||||||
|
|
||||||
|
--interact: #3f9cff;
|
||||||
|
--interact-lite: #3f9cff;
|
||||||
|
|
||||||
|
--z-toolbar: 2000;
|
||||||
|
--z-context-menu: 3000;
|
||||||
|
--z-tree-block-select: 4000;
|
||||||
|
--z-modal: 10000;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
--ex-nav-width: var(--ex-nav-desktop-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--edit-nav {
|
||||||
|
z-index: 100;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
position: unset;
|
||||||
|
width: unset;
|
||||||
|
flex-basis: var(--ex-nav-width);
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--page {
|
||||||
|
flex-basis: 100%;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: var(--ex-nav-width);
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
packages/example-site/app.tsx
Normal file
20
packages/example-site/app.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { NuxtPage } from '#components';
|
||||||
|
|
||||||
|
import './app.scss';
|
||||||
|
|
||||||
|
const AdminNav = defineAsyncComponent(() => import('~~/components/_/Nav'));
|
||||||
|
const Toaster = defineAsyncComponent(() => import('~~/components/_/Toaster'));
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { me } = useMe();
|
||||||
|
return () => (
|
||||||
|
<div class="ex-app">
|
||||||
|
{me.value ? <AdminNav class="ex-app--edit-nav" /> : null}
|
||||||
|
<NuxtPage class="ex-app--page" />
|
||||||
|
{me.value ? <Toaster /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
12
packages/example-site/components/Page.scss
Normal file
12
packages/example-site/components/Page.scss
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.ex-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&--editor {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_missing {
|
||||||
|
margin: 4rem;
|
||||||
|
}
|
||||||
|
}
|
88
packages/example-site/components/Page.tsx
Normal file
88
packages/example-site/components/Page.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import {
|
||||||
|
SbMain,
|
||||||
|
SbButton,
|
||||||
|
} from '@schlechtenburg/core';
|
||||||
|
import PageToolbar from '~~/components/PageToolbar';
|
||||||
|
import SbLayout from '@schlechtenburg/layout';
|
||||||
|
import SbHeading from '@schlechtenburg/heading';
|
||||||
|
import SbParagraph from '@schlechtenburg/paragraph';
|
||||||
|
import SbImage from '@schlechtenburg/image';
|
||||||
|
|
||||||
|
import './Page.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
async setup() {
|
||||||
|
const { me } = useMe();
|
||||||
|
|
||||||
|
const loggedIn = computed(() => !!me.value?.id);
|
||||||
|
|
||||||
|
const { setCurrentPageId, currentPage } = useCurrentPage();
|
||||||
|
const block = computed(() => currentPage.value?.attributes?.block);
|
||||||
|
|
||||||
|
if (!block) {
|
||||||
|
console.error('No block!');
|
||||||
|
console.error('page', currentPage.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
draft,
|
||||||
|
updateDraft,
|
||||||
|
} = useEditor();
|
||||||
|
|
||||||
|
const { edit } = useEditor();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
updateDraft(block.value!);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { insertPage } = usePages();
|
||||||
|
|
||||||
|
const createPageHere = () => {
|
||||||
|
insertPage({
|
||||||
|
id: 'draft',
|
||||||
|
attributes: {
|
||||||
|
title: 'New page',
|
||||||
|
block: getNewPageBlock(),
|
||||||
|
slug: '',
|
||||||
|
parent: {
|
||||||
|
data: {
|
||||||
|
id: currentPage.value?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentPageId('draft');
|
||||||
|
edit(currentPage.value?.attributes?.block!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="ex-page">
|
||||||
|
{loggedIn.value ? <PageToolbar></PageToolbar> : null}
|
||||||
|
{draft.value
|
||||||
|
? <SbMain
|
||||||
|
class="ex-page--editor"
|
||||||
|
mode={mode.value}
|
||||||
|
eventUpdate={(updatedBlock) => updateDraft(updatedBlock)}
|
||||||
|
block={draft.value}
|
||||||
|
availableBlocks={[
|
||||||
|
SbLayout,
|
||||||
|
SbHeading,
|
||||||
|
SbParagraph,
|
||||||
|
SbImage,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
: <div class="ex-page ex-page_missing">
|
||||||
|
<h1>Ooops!</h1>
|
||||||
|
<p>This page does not exist yet. However, you can create it right now!</p>
|
||||||
|
<SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => createPageHere()}
|
||||||
|
>Create a page here</SbButton>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
3
packages/example-site/components/PageBreadcrumb.scss
Normal file
3
packages/example-site/components/PageBreadcrumb.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.ex-page-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
}
|
27
packages/example-site/components/PageBreadcrumb.tsx
Normal file
27
packages/example-site/components/PageBreadcrumb.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { ComputedRef, defineComponent } from 'vue';
|
||||||
|
import { NuxtLink } from '#components';
|
||||||
|
import { IPage } from '~~/composables/pages';
|
||||||
|
|
||||||
|
import './PageBreadcrumb.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
async setup() {
|
||||||
|
const { currentPage } = useCurrentPage();
|
||||||
|
const { pages } = usePages();
|
||||||
|
|
||||||
|
const parents:ComputedRef<IPage[]> = computed(() => currentPage.value ? getPageParents(currentPage.value, pages.value, [currentPage.value]) : []);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return (<div class="ex-page-breadcrumb">
|
||||||
|
{...parents.value.map((parent) => (
|
||||||
|
<div class="ex-page-breadcrumb--crumb">
|
||||||
|
/
|
||||||
|
<NuxtLink to={getPagePath(parent, pages.value)}>
|
||||||
|
{parent?.attributes?.slug}
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
7
packages/example-site/components/PageToolbar.scss
Normal file
7
packages/example-site/components/PageToolbar.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.ex-page-toolbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: 1px solid var(--grey-2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
72
packages/example-site/components/PageToolbar.tsx
Normal file
72
packages/example-site/components/PageToolbar.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { SbButton, SbMode } from '@schlechtenburg/core';
|
||||||
|
import PageBreadcrumb from '~~/components/PageBreadcrumb';
|
||||||
|
|
||||||
|
import './PageToolbar.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
async setup() {
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
currentPageId,
|
||||||
|
setCurrentPageId,
|
||||||
|
} = useCurrentPage();
|
||||||
|
|
||||||
|
const { pages, insertPage } = usePages();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
edit,
|
||||||
|
cancel,
|
||||||
|
save,
|
||||||
|
} = useEditor();
|
||||||
|
|
||||||
|
const addChildPage = () => {
|
||||||
|
insertPage({
|
||||||
|
id: 'draft',
|
||||||
|
attributes: {
|
||||||
|
title: 'New page',
|
||||||
|
block: getNewPageBlock(),
|
||||||
|
slug: 'new-page',
|
||||||
|
parent: {
|
||||||
|
data: {
|
||||||
|
id: currentPage.value?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentPageId('draft');
|
||||||
|
edit(currentPage.value?.attributes?.block!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div class="ex-page-toolbar">
|
||||||
|
<PageBreadcrumb />
|
||||||
|
{currentPageId.value !== 'draft' && !!currentPageId.value
|
||||||
|
? <SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => addChildPage()}
|
||||||
|
>Add child page</SbButton>
|
||||||
|
: null}
|
||||||
|
{!!currentPageId.value
|
||||||
|
? (mode.value === SbMode.View
|
||||||
|
? <SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => edit(currentPage.value?.attributes?.block!)}
|
||||||
|
>Edit</SbButton>
|
||||||
|
: <>
|
||||||
|
<SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => cancel()}
|
||||||
|
>Cancel</SbButton>
|
||||||
|
<SbButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => save()}
|
||||||
|
>Save</SbButton>
|
||||||
|
</>)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
84
packages/example-site/components/_/Nav.scss
Normal file
84
packages/example-site/components/_/Nav.scss
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
.ex-edit-nav {
|
||||||
|
background-color: white;
|
||||||
|
width: var(--ex-nav-width);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_expanded {
|
||||||
|
width: 300px;
|
||||||
|
max-width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--toggle {
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-right: 1px solid var(--grey-2);
|
||||||
|
|
||||||
|
&-spacer {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--menu-item {
|
||||||
|
&-action {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--ex-nav-mobile-width);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--bg);
|
||||||
|
font-weight: bold;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--bg);
|
||||||
|
background-color: var(--interact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
opacity: 0.001;
|
||||||
|
height: auto;
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0px;
|
||||||
|
|
||||||
|
@media screen and (min-width: 1000px) {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_expanded &--menu-item {
|
||||||
|
&-action {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
opacity: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
packages/example-site/components/_/Nav.tsx
Normal file
67
packages/example-site/components/_/Nav.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { NuxtLink } from '#components';
|
||||||
|
|
||||||
|
import './Nav.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { setMe } = useMe();
|
||||||
|
|
||||||
|
const expanded = useState(() => false);
|
||||||
|
const toggle = () => {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
'ex-edit-nav': true,
|
||||||
|
'ex-edit-nav_expanded': expanded.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
setMe(null);
|
||||||
|
useGqlToken({
|
||||||
|
token: null,
|
||||||
|
config: { type: 'Bearer' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<nav class={classes.value}>
|
||||||
|
<button
|
||||||
|
class="ex-edit-nav--toggle"
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggle()}
|
||||||
|
aria-label="Toggle"
|
||||||
|
>
|
||||||
|
{expanded.value
|
||||||
|
? <Icon name="icon-park-solid:expand-right" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
: <Icon name="icon-park-solid:expand-left" class="ex-edit-nav--menu-item-icon" />}
|
||||||
|
</button>
|
||||||
|
<ul class="ex-edit-nav--menu">
|
||||||
|
<li class="ex-edit-nav--menu-item">
|
||||||
|
<NuxtLink
|
||||||
|
class="ex-edit-nav--menu-item-action"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:web" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
<span class="ex-edit-nav--menu-item-title">Website</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="ex-edit-nav--menu-spacer"></li>
|
||||||
|
|
||||||
|
<li class="ex-edit-nav--menu-item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ex-edit-nav--menu-item-action"
|
||||||
|
onClick={() => logout()}
|
||||||
|
>
|
||||||
|
<Icon name="mdi:logout" class="ex-edit-nav--menu-item-icon" />
|
||||||
|
<span class="ex-edit-nav--menu-item-title">Logout</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
28
packages/example-site/components/_/Toaster.scss
Normal file
28
packages/example-site/components/_/Toaster.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.ex-toaster {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&--toast {
|
||||||
|
margin: 1rem 2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&_info {
|
||||||
|
background-color: var(--info);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_success {
|
||||||
|
background-color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_warning {
|
||||||
|
background-color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_error {
|
||||||
|
background-color: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
packages/example-site/components/_/Toaster.tsx
Normal file
15
packages/example-site/components/_/Toaster.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import './Toaster.scss';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const { toasts } = useToaster();
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<nav class="ex-toaster">
|
||||||
|
{toasts.value.map(toast => <div class={`ex-toaster--toast ex-toaster--toast_${toast.type}`}>{toast.content}</div>)}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
107
packages/example-site/composables/editor.ts
Normal file
107
packages/example-site/composables/editor.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { IBlockData, SbMode } from "@schlechtenburg/core";
|
||||||
|
import { IPage } from "./pages";
|
||||||
|
|
||||||
|
export const useEditor = () => {
|
||||||
|
const {
|
||||||
|
currentPage,
|
||||||
|
currentPageId,
|
||||||
|
setCurrentPageId,
|
||||||
|
} = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
removePage,
|
||||||
|
updatePage,
|
||||||
|
insertPage,
|
||||||
|
pages,
|
||||||
|
} = usePages();
|
||||||
|
|
||||||
|
const mode = useState<SbMode>('mode', () => SbMode.View);
|
||||||
|
const draft = useState<IBlockData<any>|null>('draft', () => null);
|
||||||
|
|
||||||
|
const setMode = (newMode: SbMode) => {
|
||||||
|
mode.value = newMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDraft = (newDraft: IBlockData<any>) => {
|
||||||
|
draft.value = newDraft;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeDraft = () => {
|
||||||
|
draft.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = (block: IBlockData<any>) => {
|
||||||
|
draft.value = block;
|
||||||
|
mode.value = SbMode.Edit;
|
||||||
|
updateDraft(currentPage.value?.attributes?.block!);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
if (currentPageId.value === 'draft') {
|
||||||
|
const { data, error } = await useAsyncGql(
|
||||||
|
'createPage',
|
||||||
|
{ data: {
|
||||||
|
title: currentPage.value?.attributes?.title,
|
||||||
|
slug: currentPage.value?.attributes?.slug,
|
||||||
|
block: currentPage.value?.attributes?.block,
|
||||||
|
parent: currentPage.value?.attributes?.parent?.data?.id,
|
||||||
|
publishedAt: (new Date()).toISOString(),
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error creating page!');
|
||||||
|
console.error('error:', error.value);
|
||||||
|
console.error('data:', data.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(SbMode.View);
|
||||||
|
insertPage(data.value?.createPage?.data! as IPage);
|
||||||
|
removeDraft();
|
||||||
|
navigateTo(getPagePath(data.value?.createPage?.data! as IPage, pages.value));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const { data, error } = await useAsyncGql(
|
||||||
|
'updatePage',
|
||||||
|
{
|
||||||
|
id: currentPage.value?.id || '',
|
||||||
|
data: {
|
||||||
|
block: draft.value!
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error updating page!');
|
||||||
|
console.error('error:', error.value);
|
||||||
|
console.error('data:', data.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(SbMode.View);
|
||||||
|
updatePage(data.value?.updatePage?.data?.attributes?.block);
|
||||||
|
updateDraft(data.value?.updatePage?.data?.attributes?.block);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
setMode(SbMode.View);
|
||||||
|
if (currentPageId.value === 'draft') {
|
||||||
|
setCurrentPageId(currentPage.value?.attributes?.parent?.data?.id || null);
|
||||||
|
removePage('draft');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
setMode,
|
||||||
|
|
||||||
|
edit,
|
||||||
|
cancel,
|
||||||
|
save,
|
||||||
|
|
||||||
|
draft,
|
||||||
|
updateDraft,
|
||||||
|
};
|
||||||
|
};
|
31
packages/example-site/composables/me.ts
Normal file
31
packages/example-site/composables/me.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export interface IRole {
|
||||||
|
id?: string|null;
|
||||||
|
attributes?: {
|
||||||
|
description: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUser {
|
||||||
|
id?: string|null;
|
||||||
|
attributes?: {
|
||||||
|
blocked?: boolean;
|
||||||
|
confirmed?: boolean;
|
||||||
|
email?: string;
|
||||||
|
role?: IRole;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMe = () => {
|
||||||
|
const me = useState<IUser|null>('me', () => null);
|
||||||
|
|
||||||
|
const setMe = (user: IUser|null) => {
|
||||||
|
me.value = user;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
me,
|
||||||
|
setMe,
|
||||||
|
};
|
||||||
|
};
|
104
packages/example-site/composables/pages.ts
Normal file
104
packages/example-site/composables/pages.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { IBlockData } from "@schlechtenburg/core";
|
||||||
|
|
||||||
|
export interface IPage {
|
||||||
|
id?: string|null;
|
||||||
|
attributes?: {
|
||||||
|
title?: string;
|
||||||
|
block?: IBlockData<any>|null;
|
||||||
|
slug?: string;
|
||||||
|
parent?: {
|
||||||
|
data?: {
|
||||||
|
id?: string|null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePages = () => {
|
||||||
|
const pages = useState<IPage[]|[]>('pages', () => []);
|
||||||
|
|
||||||
|
const getPage = (id:string) => pages.value.find(p => p.id === id);
|
||||||
|
|
||||||
|
const setPages = (newPages: IPage[] = []) => {
|
||||||
|
pages.value = newPages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePage = (page: Partial<IPage>) => {
|
||||||
|
const existing = pages.value.find(p => p.id === page.id);
|
||||||
|
if (!existing) {
|
||||||
|
console.warn('Could not update page because it was not found in the store', page);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPages([
|
||||||
|
...pages.value.filter(p => p.id !== page.id),
|
||||||
|
{
|
||||||
|
id: existing.id,
|
||||||
|
attributes: {
|
||||||
|
...existing.attributes,
|
||||||
|
...page.attributes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePage = (id: string) => {
|
||||||
|
setPages(pages.value.filter(p => p.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertPage = (page: IPage) => {
|
||||||
|
setPages([
|
||||||
|
...pages.value,
|
||||||
|
page,
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPages = async () => {
|
||||||
|
const { data, error } = await useAsyncGql('pages');
|
||||||
|
setPages(data.value?.pages?.data as IPage[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pages,
|
||||||
|
setPages,
|
||||||
|
getPage,
|
||||||
|
|
||||||
|
fetchPages,
|
||||||
|
|
||||||
|
insertPage,
|
||||||
|
updatePage,
|
||||||
|
removePage,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCurrentPage = () => {
|
||||||
|
const { pages, insertPage } = usePages();
|
||||||
|
|
||||||
|
const currentPageId = useState<string|null>('currentPageId', () => null);
|
||||||
|
|
||||||
|
const setCurrentPage = (newPage: IPage|null) => {
|
||||||
|
if (!newPage || !newPage.id) {
|
||||||
|
currentPageId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pages.value.find(p => p.id === newPage.id)) {
|
||||||
|
insertPage(newPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPageId.value = newPage.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentPageId = (newPageId: string|null) => {
|
||||||
|
currentPageId.value = newPageId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentPage = computed(() => pages.value.find(p => p.id === currentPageId.value));
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage,
|
||||||
|
setCurrentPage,
|
||||||
|
currentPageId,
|
||||||
|
setCurrentPageId,
|
||||||
|
};
|
||||||
|
};
|
48
packages/example-site/composables/toaster.ts
Normal file
48
packages/example-site/composables/toaster.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
export enum ToastType {
|
||||||
|
SUCCESS = 'success',
|
||||||
|
INFO = 'info',
|
||||||
|
WARNING = 'warning',
|
||||||
|
ERROR = 'error',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IToaster {
|
||||||
|
type: ToastType,
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IToastedToaster extends IToaster {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_TOAST_TIME = 5000;
|
||||||
|
|
||||||
|
export const useToaster = () => {
|
||||||
|
const toasts = useState<IToastedToaster[]>('toasts', () => []);
|
||||||
|
|
||||||
|
const removeToast = (id: number) => {
|
||||||
|
toasts.value = toasts.value.filter(toast => toast.id !== id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showToast = (toast: IToaster, time = DEFAULT_TOAST_TIME) => {
|
||||||
|
const id = +(new Date());
|
||||||
|
toasts.value = [
|
||||||
|
...toasts.value,
|
||||||
|
{
|
||||||
|
...toast,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
removeToast(id);
|
||||||
|
}, time);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
toasts,
|
||||||
|
showToast,
|
||||||
|
removeToast,
|
||||||
|
};
|
||||||
|
};
|
9
packages/example-site/middleware/authenticate.ts
Normal file
9
packages/example-site/middleware/authenticate.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { IUser } from "~~/composables/states";
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
|
const { me, setMe } = useMe();
|
||||||
|
|
||||||
|
const { data } = await useAsyncGql('me');
|
||||||
|
|
||||||
|
setMe((data.value?.me as IUser) || null);
|
||||||
|
});
|
39
packages/example-site/middleware/page.ts
Normal file
39
packages/example-site/middleware/page.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { IPage } from "~~/composables/pages";
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware(async (to) => {
|
||||||
|
const { setCurrentPage } = useCurrentPage();
|
||||||
|
const { fetchPages } = usePages();
|
||||||
|
|
||||||
|
|
||||||
|
const pathParts = to.path.split('/').filter(p => p !== '');
|
||||||
|
pathParts.unshift('');
|
||||||
|
|
||||||
|
const filters = pathParts.reduce((total, part) => {
|
||||||
|
return {
|
||||||
|
id: { ne: null },
|
||||||
|
slug: { eq: part },
|
||||||
|
parent: total,
|
||||||
|
};
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const [{ data, error }] = await Promise.all([
|
||||||
|
useAsyncGql({
|
||||||
|
operation: 'pages',
|
||||||
|
variables: { filters },
|
||||||
|
}),
|
||||||
|
fetchPages(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
console.error('Error getting pages!');
|
||||||
|
console.error(error.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPage = (data.value?.pages?.data[0] as IPage) || null;
|
||||||
|
if (!newPage) {
|
||||||
|
setResponseStatus(404)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCurrentPage(newPage);
|
||||||
|
});
|
14
packages/example-site/nuxt.config.ts
Normal file
14
packages/example-site/nuxt.config.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineNuxtConfig } from 'nuxt/config';
|
||||||
|
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: [
|
||||||
|
'nuxt-graphql-client',
|
||||||
|
'nuxt-icon',
|
||||||
|
],
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
GQL_HOST: 'http://localhost:1337/graphql', // overwritten by process.env.GQL_HOST
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
21042
packages/example-site/package-lock.json
generated
Normal file
21042
packages/example-site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
28
packages/example-site/package.json
Normal file
28
packages/example-site/package.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "@schlechtenburg/example-site",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nuxt": "3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@graphql-codegen/cli": "^2.16.1",
|
||||||
|
"@schlechtenburg/core": "^0.0.0",
|
||||||
|
"@schlechtenburg/heading": "^0.0.0",
|
||||||
|
"@schlechtenburg/image": "^0.0.0",
|
||||||
|
"@schlechtenburg/layout": "^0.0.0",
|
||||||
|
"@schlechtenburg/paragraph": "^0.0.0",
|
||||||
|
"@schlechtenburg/style": "^0.0.0",
|
||||||
|
"event-target-polyfill": "^0.0.3",
|
||||||
|
"nuxt-graphql-client": "^0.2.23",
|
||||||
|
"nuxt-icon": "^0.1.8",
|
||||||
|
"sass": "^1.57.1"
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue