mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-08 19:06:06 +03:00
Compare commits
271 Commits
prosemirro
...
v21.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09436836a5 | ||
|
|
bb455d7788 | ||
|
|
009212ab80 | ||
|
|
ba9cb591c8 | ||
|
|
d00ac2f34e | ||
|
|
bd4dc6d463 | ||
|
|
d91180a909 | ||
|
|
bc2913a5cb | ||
|
|
4802394562 | ||
|
|
1755556468 | ||
|
|
01cdbdb7ae | ||
|
|
fc8bbf3eab | ||
|
|
3cdab19319 | ||
|
|
5661d20e87 | ||
|
|
91f80123e8 | ||
|
|
7a0636d0f8 | ||
|
|
0fe5bdfbac | ||
|
|
f88687e977 | ||
|
|
68d437d05b | ||
|
|
1e56aaea04 | ||
|
|
dab170a6fe | ||
|
|
a8de717d9b | ||
|
|
78fe95b6fc | ||
|
|
e0c24e41aa | ||
|
|
fa8553839b | ||
|
|
b8fcefc794 | ||
|
|
88bcb68fcb | ||
|
|
7c000553ae | ||
|
|
391fa35c80 | ||
|
|
c6773a8c9f | ||
|
|
9b226e7d39 | ||
|
|
9865446267 | ||
|
|
926abbe776 | ||
|
|
4fabef3a57 | ||
|
|
5ef4cd80c3 | ||
|
|
e01f23583f | ||
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
6f4a6ab8ea | ||
|
|
9c4b6f36f1 | ||
|
|
78886b1e67 | ||
|
|
d9debaf032 | ||
|
|
d4360d6347 | ||
|
|
175b1785c0 | ||
|
|
c8740c0171 | ||
|
|
91ee895a74 | ||
|
|
a045e46571 | ||
|
|
44eaa65c3b | ||
|
|
0a22af7b14 | ||
|
|
b54702ab08 | ||
|
|
c4fdcfc5d1 | ||
|
|
cb8117e8df | ||
|
|
5a218d5056 | ||
|
|
8dbc5cf9c6 | ||
|
|
71e81615a3 | ||
|
|
611d37da04 | ||
|
|
0e799a3857 | ||
|
|
b91d6e2bfa | ||
|
|
ea16ad7e94 | ||
|
|
ba6eb54552 | ||
|
|
f705e7683b | ||
|
|
dc996adb20 | ||
|
|
a64c638ccc | ||
|
|
359c067279 | ||
|
|
66a746e297 | ||
|
|
a4d43ee24b | ||
|
|
f7793a70a9 | ||
|
|
ceba3d31fb | ||
|
|
eecc08edde | ||
|
|
eb19aadc75 | ||
|
|
06c81e69b9 | ||
|
|
3dc3d4a639 | ||
|
|
94c59c1e3d | ||
|
|
4d2205853a | ||
|
|
751772b87a | ||
|
|
76e30869e1 | ||
|
|
3edc9fe9eb | ||
|
|
616c62703e | ||
|
|
ecd56917e7 | ||
|
|
e22c9cae91 | ||
|
|
29ddb6e1b9 | ||
|
|
2ff90e2ff0 | ||
|
|
04ecc128a2 | ||
|
|
87d1d3423b | ||
|
|
4818192a2a | ||
|
|
965dd97f54 | ||
|
|
195b74926c | ||
|
|
2120db12b2 | ||
|
|
ed563fef28 | ||
|
|
0d31a8e3f1 | ||
|
|
b8354b974b | ||
|
|
034c1e289d | ||
|
|
f31605a3de | ||
|
|
e7cc75c74d | ||
|
|
4b79d5e4e8 | ||
|
|
34854915b3 | ||
|
|
af6f34b529 | ||
|
|
fb82a2b896 | ||
|
|
5b464938b6 | ||
|
|
81f954890d | ||
|
|
0e2bbcec62 | ||
|
|
fdd339f525 | ||
|
|
8cf7d6a83d | ||
|
|
58a5008718 | ||
|
|
c44a8df55d | ||
|
|
ff1494c519 | ||
|
|
b8ce8fd852 | ||
|
|
75e7454a5f | ||
|
|
2558ea8931 | ||
|
|
ac0f47a4b2 | ||
|
|
4f16129869 | ||
|
|
64a8037fdd | ||
|
|
7502ba1bc8 | ||
|
|
33a04697ef | ||
|
|
b70a5c0cdb | ||
|
|
9443ae9f40 | ||
|
|
220c2a4102 | ||
|
|
e9914eb301 | ||
|
|
934512d09c | ||
|
|
9102c90986 | ||
|
|
c3e74219c4 | ||
|
|
13c9d7bc2d | ||
|
|
119b539586 | ||
|
|
29a5c180f0 | ||
|
|
7906602291 | ||
|
|
6dafe773ff | ||
|
|
25bc28a1be | ||
|
|
4c561c7fa0 | ||
|
|
95b3e78573 | ||
|
|
63a345bc93 | ||
|
|
e093a172cb | ||
|
|
4b01f8934b | ||
|
|
bc116b45b5 | ||
|
|
a059960b9e | ||
|
|
7770966fed | ||
|
|
d7adcf6c69 | ||
|
|
04a364dcc3 | ||
|
|
db83ac7eaa | ||
|
|
3ca9dddf61 | ||
|
|
bf74f53ca7 | ||
|
|
9d67efb4a4 | ||
|
|
3a39b9f440 | ||
|
|
27f7aab375 | ||
|
|
337da0c467 | ||
|
|
f56b3560c4 | ||
|
|
02dfe11ce6 | ||
|
|
83d06beb70 | ||
|
|
a8cfc059c8 | ||
|
|
1614b2bab0 | ||
|
|
4bdec0d214 | ||
|
|
6a7d7e7c2b | ||
|
|
30d4674657 | ||
|
|
9f961f95f8 | ||
|
|
bab99a26ec | ||
|
|
9a7fecd269 | ||
|
|
a8dc0d449b | ||
|
|
a0381f76bf | ||
|
|
6102f66daa | ||
|
|
c6134d162d | ||
|
|
2046f9b9de | ||
|
|
ac3ba594a4 | ||
|
|
22df25a480 | ||
|
|
8b30c7f02e | ||
|
|
757cdddc7c | ||
|
|
df95e99680 | ||
|
|
5a6d544db7 | ||
|
|
16117d329c | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
67d9e24d8f | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
753f6394f7 | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
1fe933e4ea | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
8dec674cc3 | ||
|
|
f784c03746 | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
f184d763ad | ||
|
|
a91d42634d | ||
|
|
f517ef3616 | ||
|
|
e99507ddcf | ||
|
|
d2cacf1945 | ||
|
|
448ac1405b | ||
|
|
6ad21ce885 |
4
.github/translators.txt
vendored
4
.github/translators.txt
vendored
@@ -206,7 +206,3 @@ Thiago Rafael Pereira de Carvalho (thiago.rafael) :: Portuguese, Brazilian
|
||||
Ken Roger Bolgnes (kenbo124) :: Norwegian Bokmal
|
||||
Nguyen Hung Phuong (hnwolf) :: Vietnamese
|
||||
Umut ERGENE (umutergene67) :: Turkish
|
||||
Tomáš Batelka (Vofy) :: Czech
|
||||
Mundo Racional (ismael.mesquita) :: Portuguese, Brazilian
|
||||
Zarik (3apuk) :: Russian
|
||||
Ali Shaatani (a.shaatani) :: Arabic
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,10 +5,10 @@ Homestead.yaml
|
||||
.idea
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/public/dist
|
||||
/public/dist/*.map
|
||||
/public/plugins
|
||||
/public/css
|
||||
/public/js
|
||||
/public/css/*.map
|
||||
/public/js/*.map
|
||||
/public/bower
|
||||
/public/build/
|
||||
/storage/images
|
||||
|
||||
49
TODO
49
TODO
@@ -1,49 +0,0 @@
|
||||
### Next
|
||||
|
||||
- Table cell height resize & cell width resize via width style
|
||||
- Column resize source: https://github.com/ProseMirror/prosemirror-tables/blob/master/src/columnresizing.js
|
||||
- Have updated column resizing to set cell widths
|
||||
- Now need to handle table overall size on change, then heights.
|
||||
|
||||
- Details/Summary
|
||||
- Need view to control summary editability, make readonly but editable via popover.
|
||||
- Need some default styles to visualise details boundary.
|
||||
- Markdown parser needs to be updated to handle separate open/close tags for blocks.
|
||||
|
||||
### In-Progress
|
||||
|
||||
- Tables
|
||||
- Details/Summary
|
||||
|
||||
### Features
|
||||
|
||||
- Images
|
||||
- Drawings
|
||||
- LTR/RTL control
|
||||
- Fullscreen
|
||||
- Paste Image Uploading
|
||||
- Drag + Drop Image Uploading
|
||||
- Checkbox/TODO list items
|
||||
- Code blocks
|
||||
- Indents
|
||||
- Attachment integration (Drag & drop)
|
||||
- Template system integration.
|
||||
|
||||
### Improvements
|
||||
|
||||
- List type changing.
|
||||
- Color picker options should have "clear" option.
|
||||
- Color picker buttons should be split, with button to re-apply last selected color.
|
||||
- Color picker options should change color if different instead of remove.
|
||||
- Clear formatting, If no selection range, clear the formatting of parent block.
|
||||
- If no marks, clear the block type if text type?
|
||||
- Remove links button? (Action already in place if link href is empty).
|
||||
- Links - Validate URL.
|
||||
- Links - Integrate entity picker.
|
||||
- iFrame - Parse iframe HTML & auto-convert youtube/vimeo urls to embeds.
|
||||
|
||||
### Notes
|
||||
|
||||
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
|
||||
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
|
||||
but its tricky since editing the markdown content would change the block definition/type while editing.
|
||||
@@ -4,10 +4,8 @@ namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -70,32 +68,14 @@ class DispatchWebhookJob implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail);
|
||||
$webhookData = $themeResponse ?? $this->buildWebhookData();
|
||||
$lastError = null;
|
||||
$response = Http::asJson()
|
||||
->withOptions(['allow_redirects' => ['strict' => true]])
|
||||
->timeout(3)
|
||||
->post($this->webhook->endpoint, $this->buildWebhookData());
|
||||
|
||||
try {
|
||||
$response = Http::asJson()
|
||||
->withOptions(['allow_redirects' => ['strict' => true]])
|
||||
->timeout($this->webhook->timeout)
|
||||
->post($this->webhook->endpoint, $webhookData);
|
||||
} catch (\Exception $exception) {
|
||||
$lastError = $exception->getMessage();
|
||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
|
||||
}
|
||||
|
||||
if (isset($response) && $response->failed()) {
|
||||
$lastError = "Response status from endpoint was {$response->status()}";
|
||||
if ($response->failed()) {
|
||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}");
|
||||
}
|
||||
|
||||
$this->webhook->last_called_at = now();
|
||||
if ($lastError) {
|
||||
$this->webhook->last_errored_at = now();
|
||||
$this->webhook->last_error = $lastError;
|
||||
}
|
||||
|
||||
$this->webhook->save();
|
||||
}
|
||||
|
||||
protected function buildWebhookData(): array
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -15,22 +14,13 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property string $endpoint
|
||||
* @property Collection $trackedEvents
|
||||
* @property bool $active
|
||||
* @property int $timeout
|
||||
* @property string $last_error
|
||||
* @property Carbon $last_called_at
|
||||
* @property Carbon $last_errored_at
|
||||
*/
|
||||
class Webhook extends Model implements Loggable
|
||||
{
|
||||
protected $fillable = ['name', 'endpoint', 'timeout'];
|
||||
protected $fillable = ['name', 'endpoint'];
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'last_called_at' => 'datetime',
|
||||
'last_errored_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Define the tracked event relation a webhook.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,6 @@ use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\TrashCan;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Exception;
|
||||
|
||||
@@ -86,19 +85,15 @@ class ChapterRepo
|
||||
* 'book:<id>' (book:5).
|
||||
*
|
||||
* @throws MoveOperationException
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function move(Chapter $chapter, string $parentIdentifier): Book
|
||||
{
|
||||
/** @var Book $parent */
|
||||
$parent = $this->findParentByIdentifier($parentIdentifier);
|
||||
if (is_null($parent)) {
|
||||
throw new MoveOperationException('Book to move chapter into not found');
|
||||
}
|
||||
|
||||
if (!userCan('chapter-create', $parent)) {
|
||||
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
|
||||
}
|
||||
|
||||
$chapter->changeBook($parent->id);
|
||||
$chapter->rebuildPermissions();
|
||||
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
|
||||
|
||||
@@ -328,7 +328,7 @@ class PageRepo
|
||||
public function move(Page $page, string $parentIdentifier): Entity
|
||||
{
|
||||
$parent = $this->findParentByIdentifier($parentIdentifier);
|
||||
if (is_null($parent)) {
|
||||
if ($parent === null) {
|
||||
throw new MoveOperationException('Book or chapter to move page into not found');
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\SortOperationException;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BookContents
|
||||
@@ -106,209 +107,111 @@ class BookContents
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the books content using the given sort map.
|
||||
* Sort the books content using the given map.
|
||||
* The map is a single-dimension collection of objects in the following format:
|
||||
* {
|
||||
* +"id": "294" (ID of item)
|
||||
* +"sort": 1 (Sort order index)
|
||||
* +"parentChapter": false (ID of parent chapter, as string, or false)
|
||||
* +"type": "page" (Entity type of item)
|
||||
* +"book": "1" (Id of book to place item in)
|
||||
* }.
|
||||
*
|
||||
* Returns a list of books that were involved in the operation.
|
||||
*
|
||||
* @returns Book[]
|
||||
* @throws SortOperationException
|
||||
*/
|
||||
public function sortUsingMap(BookSortMap $sortMap): array
|
||||
public function sortUsingMap(Collection $sortMap): Collection
|
||||
{
|
||||
// Load models into map
|
||||
$modelMap = $this->loadModelsFromSortMap($sortMap);
|
||||
|
||||
// Sort our changes from our map to be chapters first
|
||||
// Since they need to be process to ensure book alignment for child page changes.
|
||||
$sortMapItems = $sortMap->all();
|
||||
usort($sortMapItems, function (BookSortMapItem $itemA, BookSortMapItem $itemB) {
|
||||
$aScore = $itemA->type === 'page' ? 2 : 1;
|
||||
$bScore = $itemB->type === 'page' ? 2 : 1;
|
||||
|
||||
return $aScore - $bScore;
|
||||
});
|
||||
$this->loadModelsIntoSortMap($sortMap);
|
||||
$booksInvolved = $this->getBooksInvolvedInSort($sortMap);
|
||||
|
||||
// Perform the sort
|
||||
foreach ($sortMapItems as $item) {
|
||||
$this->applySortUpdates($item, $modelMap);
|
||||
}
|
||||
$sortMap->each(function ($mapItem) {
|
||||
$this->applySortUpdates($mapItem);
|
||||
});
|
||||
|
||||
/** @var Book[] $booksInvolved */
|
||||
$booksInvolved = array_values(array_filter($modelMap, function (string $key) {
|
||||
return strpos($key, 'book:') === 0;
|
||||
}, ARRAY_FILTER_USE_KEY));
|
||||
|
||||
// Update permissions of books involved
|
||||
foreach ($booksInvolved as $book) {
|
||||
// Update permissions and activity.
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
$book->rebuildPermissions();
|
||||
}
|
||||
});
|
||||
|
||||
return $booksInvolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the given sort map item, detect changes for the related model
|
||||
* and update it if required. Changes where permissions are lacking will
|
||||
* be skipped and not throw an error.
|
||||
*
|
||||
* @param array<string, Entity> $modelMap
|
||||
* and update it if required.
|
||||
*/
|
||||
protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void
|
||||
protected function applySortUpdates(\stdClass $sortMapItem)
|
||||
{
|
||||
/** @var BookChild $model */
|
||||
$model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null;
|
||||
if (!$model) {
|
||||
return;
|
||||
}
|
||||
$model = $sortMapItem->model;
|
||||
|
||||
$priorityChanged = $model->priority !== $sortMapItem->sort;
|
||||
$bookChanged = $model->book_id !== $sortMapItem->parentBookId;
|
||||
$chapterChanged = ($model instanceof Page) && $model->chapter_id !== $sortMapItem->parentChapterId;
|
||||
$priorityChanged = intval($model->priority) !== intval($sortMapItem->sort);
|
||||
$bookChanged = intval($model->book_id) !== intval($sortMapItem->book);
|
||||
$chapterChanged = ($model instanceof Page) && intval($model->chapter_id) !== $sortMapItem->parentChapter;
|
||||
|
||||
// Stop if there's no change
|
||||
if (!$priorityChanged && !$bookChanged && !$chapterChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentParentKey = 'book:' . $model->book_id;
|
||||
if ($model instanceof Page && $model->chapter_id) {
|
||||
$currentParentKey = 'chapter:' . $model->chapter_id;
|
||||
}
|
||||
|
||||
$currentParent = $modelMap[$currentParentKey] ?? null;
|
||||
/** @var Book $newBook */
|
||||
$newBook = $modelMap['book:' . $sortMapItem->parentBookId] ?? null;
|
||||
/** @var ?Chapter $newChapter */
|
||||
$newChapter = $sortMapItem->parentChapterId ? ($modelMap['chapter:' . $sortMapItem->parentChapterId] ?? null) : null;
|
||||
|
||||
if (!$this->isSortChangePermissible($sortMapItem, $model, $currentParent, $newBook, $newChapter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Action the required changes
|
||||
if ($bookChanged) {
|
||||
$model->changeBook($newBook->id);
|
||||
$model->changeBook($sortMapItem->book);
|
||||
}
|
||||
|
||||
if ($chapterChanged) {
|
||||
$model->chapter_id = $newChapter->id ?? 0;
|
||||
$model->chapter_id = intval($sortMapItem->parentChapter);
|
||||
$model->save();
|
||||
}
|
||||
|
||||
if ($priorityChanged) {
|
||||
$model->priority = $sortMapItem->sort;
|
||||
}
|
||||
|
||||
if ($chapterChanged || $priorityChanged) {
|
||||
$model->priority = intval($sortMapItem->sort);
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has permissions to apply the given sorting change.
|
||||
* Is quite complex since items can gain a different parent change. Acts as a:
|
||||
* - Update of old parent element (Change of content/order).
|
||||
* - Update of sorted/moved element.
|
||||
* - Deletion of element (Relative to parent upon move).
|
||||
* - Creation of element within parent (Upon move to new parent).
|
||||
* Load models from the database into the given sort map.
|
||||
*/
|
||||
protected function isSortChangePermissible(BookSortMapItem $sortMapItem, BookChild $model, ?Entity $currentParent, ?Entity $newBook, ?Entity $newChapter): bool
|
||||
protected function loadModelsIntoSortMap(Collection $sortMap): void
|
||||
{
|
||||
// Stop if we can't see the current parent or new book.
|
||||
if (!$currentParent || !$newBook) {
|
||||
return false;
|
||||
$keyMap = $sortMap->keyBy(function (\stdClass $sortMapItem) {
|
||||
return $sortMapItem->type . ':' . $sortMapItem->id;
|
||||
});
|
||||
$pageIds = $sortMap->where('type', '=', 'page')->pluck('id');
|
||||
$chapterIds = $sortMap->where('type', '=', 'chapter')->pluck('id');
|
||||
|
||||
$pages = Page::visible()->whereIn('id', $pageIds)->get();
|
||||
$chapters = Chapter::visible()->whereIn('id', $chapterIds)->get();
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$sortItem = $keyMap->get('page:' . $page->id);
|
||||
$sortItem->model = $page;
|
||||
}
|
||||
|
||||
$hasNewParent = $newBook->id !== $model->book_id || ($model instanceof Page && $model->chapter_id !== ($sortMapItem->parentChapterId ?? 0));
|
||||
if ($model instanceof Chapter) {
|
||||
$hasPermission = userCan('book-update', $currentParent)
|
||||
&& userCan('book-update', $newBook)
|
||||
&& userCan('chapter-update', $model)
|
||||
&& (!$hasNewParent || userCan('chapter-create', $newBook))
|
||||
&& (!$hasNewParent || userCan('chapter-delete', $model));
|
||||
|
||||
if (!$hasPermission) {
|
||||
return false;
|
||||
}
|
||||
foreach ($chapters as $chapter) {
|
||||
$sortItem = $keyMap->get('chapter:' . $chapter->id);
|
||||
$sortItem->model = $chapter;
|
||||
}
|
||||
|
||||
if ($model instanceof Page) {
|
||||
$parentPermission = ($currentParent instanceof Chapter) ? 'chapter-update' : 'book-update';
|
||||
$hasCurrentParentPermission = userCan($parentPermission, $currentParent);
|
||||
|
||||
// This needs to check if there was an intended chapter location in the original sort map
|
||||
// rather than inferring from the $newChapter since that variable may be null
|
||||
// due to other reasons (Visibility).
|
||||
$newParent = $sortMapItem->parentChapterId ? $newChapter : $newBook;
|
||||
if (!$newParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hasPageEditPermission = userCan('page-update', $model);
|
||||
$newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id);
|
||||
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
|
||||
$hasNewParentPermission = userCan($newParentPermission, $newParent);
|
||||
|
||||
$hasDeletePermissionIfMoving = (!$hasNewParent || userCan('page-delete', $model));
|
||||
$hasCreatePermissionIfMoving = (!$hasNewParent || userCan('page-create', $newParent));
|
||||
|
||||
$hasPermission = $hasCurrentParentPermission
|
||||
&& $newParentInRightLocation
|
||||
&& $hasNewParentPermission
|
||||
&& $hasPageEditPermission
|
||||
&& $hasDeletePermissionIfMoving
|
||||
&& $hasCreatePermissionIfMoving;
|
||||
|
||||
if (!$hasPermission) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load models from the database into the given sort map.
|
||||
* Get the books involved in a sort.
|
||||
* The given sort map should have its models loaded first.
|
||||
*
|
||||
* @return array<string, Entity>
|
||||
* @throws SortOperationException
|
||||
*/
|
||||
protected function loadModelsFromSortMap(BookSortMap $sortMap): array
|
||||
protected function getBooksInvolvedInSort(Collection $sortMap): Collection
|
||||
{
|
||||
$modelMap = [];
|
||||
$ids = [
|
||||
'chapter' => [],
|
||||
'page' => [],
|
||||
'book' => [],
|
||||
];
|
||||
$bookIdsInvolved = collect([$this->book->id]);
|
||||
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('book'));
|
||||
$bookIdsInvolved = $bookIdsInvolved->concat($sortMap->pluck('model.book_id'));
|
||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
||||
|
||||
foreach ($sortMap->all() as $sortMapItem) {
|
||||
$ids[$sortMapItem->type][] = $sortMapItem->id;
|
||||
$ids['book'][] = $sortMapItem->parentBookId;
|
||||
if ($sortMapItem->parentChapterId) {
|
||||
$ids['chapter'][] = $sortMapItem->parentChapterId;
|
||||
}
|
||||
$books = Book::hasPermission('update')->whereIn('id', $bookIdsInvolved)->get();
|
||||
|
||||
if (count($books) !== count($bookIdsInvolved)) {
|
||||
throw new SortOperationException('Could not find all books requested in sort operation');
|
||||
}
|
||||
|
||||
$pages = Page::visible()->whereIn('id', array_unique($ids['page']))->get(Page::$listAttributes);
|
||||
/** @var Page $page */
|
||||
foreach ($pages as $page) {
|
||||
$modelMap['page:' . $page->id] = $page;
|
||||
$ids['book'][] = $page->book_id;
|
||||
if ($page->chapter_id) {
|
||||
$ids['chapter'][] = $page->chapter_id;
|
||||
}
|
||||
}
|
||||
|
||||
$chapters = Chapter::visible()->whereIn('id', array_unique($ids['chapter']))->get();
|
||||
/** @var Chapter $chapter */
|
||||
foreach ($chapters as $chapter) {
|
||||
$modelMap['chapter:' . $chapter->id] = $chapter;
|
||||
$ids['book'][] = $chapter->book_id;
|
||||
}
|
||||
|
||||
$books = Book::visible()->whereIn('id', array_unique($ids['book']))->get();
|
||||
/** @var Book $book */
|
||||
foreach ($books as $book) {
|
||||
$modelMap['book:' . $book->id] = $book;
|
||||
}
|
||||
|
||||
return $modelMap;
|
||||
return $books;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
class BookSortMap
|
||||
{
|
||||
/**
|
||||
* @var BookSortMapItem[]
|
||||
*/
|
||||
protected $mapData = [];
|
||||
|
||||
public function addItem(BookSortMapItem $mapItem): void
|
||||
{
|
||||
$this->mapData[] = $mapItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BookSortMapItem[]
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->mapData;
|
||||
}
|
||||
|
||||
public static function fromJson(string $json): self
|
||||
{
|
||||
$map = new BookSortMap();
|
||||
$mapData = json_decode($json);
|
||||
|
||||
foreach ($mapData as $mapDataItem) {
|
||||
$item = new BookSortMapItem(
|
||||
intval($mapDataItem->id),
|
||||
intval($mapDataItem->sort),
|
||||
$mapDataItem->parentChapter ? intval($mapDataItem->parentChapter) : null,
|
||||
$mapDataItem->type,
|
||||
intval($mapDataItem->book)
|
||||
);
|
||||
|
||||
$map->addItem($item);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Entities\Tools;
|
||||
|
||||
class BookSortMapItem
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $sort;
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
public $parentChapterId;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
public $parentBookId;
|
||||
|
||||
public function __construct(int $id, int $sort, ?int $parentChapterId, string $type, int $parentBookId)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->sort = $sort;
|
||||
$this->parentChapterId = $parentChapterId;
|
||||
$this->type = $type;
|
||||
$this->parentBookId = $parentBookId;
|
||||
}
|
||||
}
|
||||
9
app/Exceptions/SortOperationException.php
Normal file
9
app/Exceptions/SortOperationException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class SortOperationException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -29,8 +29,6 @@ class MfaBackupCodesController extends Controller
|
||||
|
||||
$downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode("\n\n", $codes));
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_gen_backup_codes_title'));
|
||||
|
||||
return view('mfa.backup-codes-generate', [
|
||||
'codes' => $codes,
|
||||
'downloadUrl' => $downloadUrl,
|
||||
|
||||
@@ -21,8 +21,6 @@ class MfaController extends Controller
|
||||
->get(['id', 'method'])
|
||||
->groupBy('method');
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_setup'));
|
||||
|
||||
return view('mfa.setup', [
|
||||
'userMethods' => $userMethods,
|
||||
]);
|
||||
|
||||
@@ -34,8 +34,6 @@ class MfaTotpController extends Controller
|
||||
$qrCodeUrl = $totp->generateUrl($totpSecret, $this->currentOrLastAttemptedUser());
|
||||
$svg = $totp->generateQrCodeSvg($qrCodeUrl);
|
||||
|
||||
$this->setPageTitle(trans('auth.mfa_gen_totp_title'));
|
||||
|
||||
return view('mfa.totp-generate', [
|
||||
'url' => $qrCodeUrl,
|
||||
'svg' => $svg,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\BookSortMap;
|
||||
use BookStack\Exceptions\SortOperationException;
|
||||
use BookStack\Facades\Activity;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -58,14 +59,20 @@ class BookSortController extends Controller
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
|
||||
$sortMap = collect(json_decode($request->get('sort-tree')));
|
||||
$bookContents = new BookContents($book);
|
||||
$booksInvolved = $bookContents->sortUsingMap($sortMap);
|
||||
$booksInvolved = collect();
|
||||
|
||||
try {
|
||||
$booksInvolved = $bookContents->sortUsingMap($sortMap);
|
||||
} catch (SortOperationException $exception) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
// Rebuild permissions and add activity for involved books.
|
||||
foreach ($booksInvolved as $bookInvolved) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
|
||||
}
|
||||
$booksInvolved->each(function (Book $book) {
|
||||
Activity::add(ActivityType::BOOK_SORT, $book);
|
||||
});
|
||||
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
@@ -181,8 +180,6 @@ class ChapterController extends Controller
|
||||
|
||||
try {
|
||||
$newBook = $this->chapterRepo->move($chapter, $entitySelection);
|
||||
} catch (PermissionsException $exception) {
|
||||
$this->showPermissionError();
|
||||
} catch (MoveOperationException $exception) {
|
||||
$this->showErrorNotification(trans('errors.selected_book_not_found'));
|
||||
|
||||
|
||||
@@ -48,8 +48,6 @@ abstract class Controller extends BaseController
|
||||
/**
|
||||
* On a permission error redirect to home and display.
|
||||
* the error as a notification.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
protected function showPermissionError()
|
||||
{
|
||||
|
||||
@@ -21,8 +21,6 @@ class FavouriteController extends Controller
|
||||
|
||||
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
|
||||
|
||||
$this->setPageTitle(trans('entities.my_favourites'));
|
||||
|
||||
return view('common.detailed-listing-with-more', [
|
||||
'title' => trans('entities.my_favourites'),
|
||||
'entities' => $favourites->slice(0, $viewCount),
|
||||
|
||||
@@ -368,8 +368,6 @@ class PageController extends Controller
|
||||
->paginate(20)
|
||||
->setPath(url('/pages/recently-updated'));
|
||||
|
||||
$this->setPageTitle(trans('entities.recently_updated_pages'));
|
||||
|
||||
return view('common.detailed-listing-paginated', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'entities' => $pages,
|
||||
@@ -412,9 +410,11 @@ class PageController extends Controller
|
||||
|
||||
try {
|
||||
$parent = $this->pageRepo->move($page, $entitySelection);
|
||||
} catch (PermissionsException $exception) {
|
||||
$this->showPermissionError();
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
$this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
|
||||
|
||||
return redirect()->back();
|
||||
|
||||
@@ -29,8 +29,6 @@ class RoleController extends Controller
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$roles = $this->permissionsRepo->getAllRoles();
|
||||
|
||||
$this->setPageTitle(trans('settings.roles'));
|
||||
|
||||
return view('settings.roles.index', ['roles' => $roles]);
|
||||
}
|
||||
|
||||
@@ -51,8 +49,6 @@ class RoleController extends Controller
|
||||
$role->display_name .= ' (' . trans('common.copy') . ')';
|
||||
}
|
||||
|
||||
$this->setPageTitle(trans('settings.role_create'));
|
||||
|
||||
return view('settings.roles.create', ['role' => $role]);
|
||||
}
|
||||
|
||||
@@ -86,8 +82,6 @@ class RoleController extends Controller
|
||||
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||
}
|
||||
|
||||
$this->setPageTitle(trans('settings.role_edit'));
|
||||
|
||||
return view('settings.roles.edit', ['role' => $role]);
|
||||
}
|
||||
|
||||
@@ -122,8 +116,6 @@ class RoleController extends Controller
|
||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||
$roles->prepend($blankRole);
|
||||
|
||||
$this->setPageTitle(trans('settings.role_delete'));
|
||||
|
||||
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ class TagController extends Controller
|
||||
'name' => $nameFilter,
|
||||
]));
|
||||
|
||||
$this->setPageTitle(trans('entities.tags'));
|
||||
|
||||
return view('tags.index', [
|
||||
'tags' => $tags,
|
||||
'search' => $search,
|
||||
|
||||
@@ -18,8 +18,6 @@ class UserProfileController extends Controller
|
||||
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
|
||||
$assetCounts = $repo->getAssetCounts($user);
|
||||
|
||||
$this->setPageTitle($user->name);
|
||||
|
||||
return view('users.profile', [
|
||||
'user' => $user,
|
||||
'activity' => $userActivity,
|
||||
|
||||
@@ -25,8 +25,6 @@ class WebhookController extends Controller
|
||||
->with('trackedEvents')
|
||||
->get();
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks'));
|
||||
|
||||
return view('settings.webhooks.index', ['webhooks' => $webhooks]);
|
||||
}
|
||||
|
||||
@@ -35,8 +33,6 @@ class WebhookController extends Controller
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->setPageTitle(trans('settings.webhooks_create'));
|
||||
|
||||
return view('settings.webhooks.create');
|
||||
}
|
||||
|
||||
@@ -50,7 +46,6 @@ class WebhookController extends Controller
|
||||
'endpoint' => ['required', 'url', 'max:500'],
|
||||
'events' => ['required', 'array'],
|
||||
'active' => ['required'],
|
||||
'timeout' => ['required', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
$webhook = new Webhook($validated);
|
||||
@@ -73,8 +68,6 @@ class WebhookController extends Controller
|
||||
->with('trackedEvents')
|
||||
->findOrFail($id);
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks_edit'));
|
||||
|
||||
return view('settings.webhooks.edit', ['webhook' => $webhook]);
|
||||
}
|
||||
|
||||
@@ -88,7 +81,6 @@ class WebhookController extends Controller
|
||||
'endpoint' => ['required', 'url', 'max:500'],
|
||||
'events' => ['required', 'array'],
|
||||
'active' => ['required'],
|
||||
'timeout' => ['required', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
/** @var Webhook $webhook */
|
||||
@@ -111,8 +103,6 @@ class WebhookController extends Controller
|
||||
/** @var Webhook $webhook */
|
||||
$webhook = Webhook::query()->findOrFail($id);
|
||||
|
||||
$this->setPageTitle(trans('settings.webhooks_delete'));
|
||||
|
||||
return view('settings.webhooks.delete', ['webhook' => $webhook]);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,20 +79,4 @@ class ThemeEvents
|
||||
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
|
||||
*/
|
||||
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
|
||||
|
||||
/**
|
||||
* Webhook call before event.
|
||||
* Runs before a webhook endpoint is called. Allows for customization
|
||||
* of the data format & content within the webhook POST request.
|
||||
* Provides the original event name as a string (see \BookStack\Actions\ActivityType)
|
||||
* along with the webhook instance along with the event detail which may be a
|
||||
* "Loggable" model type or a string.
|
||||
* If the listener returns a non-null value, that will be used as the POST data instead
|
||||
* of the system default.
|
||||
*
|
||||
* @param string $event
|
||||
* @param \BookStack\Actions\Webhook $webhook
|
||||
* @param string|\BookStack\Interfaces\Loggable $detail
|
||||
*/
|
||||
const WEBHOOK_CALL_BEFORE = 'webhook_call_before';
|
||||
}
|
||||
|
||||
@@ -228,21 +228,6 @@ class ImageService
|
||||
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given image and image data is apng.
|
||||
*/
|
||||
protected function isApngData(Image $image, string &$imageData): bool
|
||||
{
|
||||
$isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
|
||||
if (!$isPng) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
|
||||
|
||||
return strpos($initialHeader, 'acTL') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thumbnail for an image.
|
||||
* If $keepRatio is true only the width will be used.
|
||||
@@ -253,7 +238,6 @@ class ImageService
|
||||
*/
|
||||
public function getThumbnail(Image $image, ?int $width, ?int $height, bool $keepRatio = false): string
|
||||
{
|
||||
// Do not resize GIF images where we're not cropping
|
||||
if ($keepRatio && $this->isGif($image)) {
|
||||
return $this->getPublicUrl($image->path);
|
||||
}
|
||||
@@ -262,35 +246,19 @@ class ImageService
|
||||
$imagePath = $image->path;
|
||||
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
|
||||
|
||||
$thumbCacheKey = 'images::' . $image->id . '::' . $thumbFilePath;
|
||||
|
||||
// Return path if in cache
|
||||
$cachedThumbPath = $this->cache->get($thumbCacheKey);
|
||||
if ($cachedThumbPath) {
|
||||
return $this->getPublicUrl($cachedThumbPath);
|
||||
}
|
||||
|
||||
// If thumbnail has already been generated, serve that and cache path
|
||||
$storage = $this->getStorageDisk($image->type);
|
||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
$this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
$imageData = $storage->get($this->adjustPathForStorageDisk($imagePath, $image->type));
|
||||
|
||||
// Do not resize apng images where we're not cropping
|
||||
if ($keepRatio && $this->isApngData($image, $imageData)) {
|
||||
$this->cache->put($thumbCacheKey, $image->path, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($image->path);
|
||||
$storage = $this->getStorageDisk($image->type);
|
||||
if ($storage->exists($this->adjustPathForStorageDisk($thumbFilePath, $image->type))) {
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
// If not in cache and thumbnail does not exist, generate thumb and cache path
|
||||
$thumbData = $this->resizeImage($imageData, $width, $height, $keepRatio);
|
||||
$thumbData = $this->resizeImage($storage->get($this->adjustPathForStorageDisk($imagePath, $image->type)), $width, $height, $keepRatio);
|
||||
|
||||
$this->saveImageDataInPublicSpace($storage, $this->adjustPathForStorageDisk($thumbFilePath, $image->type), $thumbData);
|
||||
$this->cache->put($thumbCacheKey, $thumbFilePath, 60 * 60 * 72);
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
return $this->getPublicUrl($thumbFilePath);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use Exception;
|
||||
@@ -17,7 +16,6 @@ class UserAvatars
|
||||
{
|
||||
$this->imageService = $imageService;
|
||||
$this->http = $http;
|
||||
$ldapService = app()->make(LdapService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,6 @@ class WebSafeMimeSniffer
|
||||
'application/json',
|
||||
'application/octet-stream',
|
||||
'application/pdf',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
|
||||
@@ -20,7 +20,6 @@ class WebhookFactory extends Factory
|
||||
'name' => 'My webhook for ' . $this->faker->country(),
|
||||
'endpoint' => $this->faker->url,
|
||||
'active' => true,
|
||||
'timeout' => 3,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebhooksTimeoutErrorColumns extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('webhooks', function (Blueprint $table) {
|
||||
$table->unsignedInteger('timeout')->default(3);
|
||||
$table->text('last_error')->default('');
|
||||
$table->timestamp('last_called_at')->nullable();
|
||||
$table->timestamp('last_errored_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('webhooks', function (Blueprint $table) {
|
||||
$table->dropColumn('timeout');
|
||||
$table->dropColumn('last_error');
|
||||
$table->dropColumn('last_called_at');
|
||||
$table->dropColumn('last_errored_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
432
package-lock.json
generated
432
package-lock.json
generated
@@ -7,18 +7,9 @@
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"crelt": "^1.0.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.6.0",
|
||||
"prosemirror-model": "^1.15.0",
|
||||
"prosemirror-schema-list": "^1.1.6",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-view": "^1.23.2",
|
||||
"sortablejs": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -228,11 +219,6 @@
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
|
||||
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
@@ -1241,11 +1227,6 @@
|
||||
"integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz",
|
||||
"integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ=="
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -1364,193 +1345,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz",
|
||||
"integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-6+YwTjmqDwlA/Dm+5wK67ezgqgjA/MhSDgaNxKUzH97SmeuWFXyLeDRxxOPZeSo7yTxcDGUCWTEjmQZsVBuMrQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-example-setup": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.1.2.tgz",
|
||||
"integrity": "sha512-MTpIMyqk08jFnzxeRMCinCEMtVSTUtxKgQBGxfCbVe9C6zIOqp9qZZJz5Ojaad1GETySyuj8+OIHHvQsIaaaGQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-dropcursor": "^1.0.0",
|
||||
"prosemirror-gapcursor": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-inputrules": "^1.0.0",
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-menu": "^1.0.0",
|
||||
"prosemirror-schema-list": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.2.0.tgz",
|
||||
"integrity": "sha512-yCLy5+0rVqLir/KcHFathQj4Rf8aRHi80FmEfKtM0JmyzvwdomslLzDZ/pX4oFhFKDgjl/WBBBFNqDyNifWg7g==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz",
|
||||
"integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz",
|
||||
"integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz",
|
||||
"integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.6.0.tgz",
|
||||
"integrity": "sha512-y/gRpJIIrNArtkyMax7ypYafb+ZMjddbVHI+AwlcUfCLCCXK57cOmfBMKYVq9kdEKJYVdYHdoyWsVNn1nWLHUg==",
|
||||
"dependencies": {
|
||||
"markdown-it": "^10.0.0",
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/markdown-it": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.4.tgz",
|
||||
"integrity": "sha512-2ROsji/X9ciDnVSRvSTqFygI34GEdHfQSsK4zBKjPxSEroeiHHcdRMS1ofNIf2zM0Vpp5/YqfpxynElymQkqzg==",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.15.0.tgz",
|
||||
"integrity": "sha512-hQJv7SnIhlAy9ga3lhPPgaufhvCbQB9tHwscJ9E1H1pPHmN8w5V/lURueoYv9Kc3/bpNWoyHa8r3g//m7N0ChQ==",
|
||||
"dependencies": {
|
||||
"orderedmap": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.6.tgz",
|
||||
"integrity": "sha512-aFGEdaCWmJzouZ8DwedmvSsL50JpRkqhQ6tcpThwJONVVmCgI36LJHtoQ4VGZbusMavaBhXXr33zyD2IVsTlkw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz",
|
||||
"integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
|
||||
"integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.1.2",
|
||||
"prosemirror-model": "^1.8.1",
|
||||
"prosemirror-state": "^1.3.1",
|
||||
"prosemirror-transform": "^1.2.1",
|
||||
"prosemirror-view": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
|
||||
"integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.23.2.tgz",
|
||||
"integrity": "sha512-iPgRw6tpcN+KH1yKmSnRmDKsJBVkWLFP6laHcz9rh/n0Ndz7YKKCDldtw6FhHBYoWmZeubbhV/rrQW0VCDG9iw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -1614,11 +1408,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz",
|
||||
"integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
@@ -1732,11 +1521,6 @@
|
||||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
@@ -1874,11 +1658,6 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
@@ -2147,11 +1926,6 @@
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"crelt": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
|
||||
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
@@ -2856,11 +2630,6 @@
|
||||
"integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
|
||||
"dev": true
|
||||
},
|
||||
"orderedmap": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz",
|
||||
"integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ=="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -2940,192 +2709,6 @@
|
||||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
||||
"dev": true
|
||||
},
|
||||
"prosemirror-commands": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz",
|
||||
"integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-dropcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-6+YwTjmqDwlA/Dm+5wK67ezgqgjA/MhSDgaNxKUzH97SmeuWFXyLeDRxxOPZeSo7yTxcDGUCWTEjmQZsVBuMrQ==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-example-setup": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.1.2.tgz",
|
||||
"integrity": "sha512-MTpIMyqk08jFnzxeRMCinCEMtVSTUtxKgQBGxfCbVe9C6zIOqp9qZZJz5Ojaad1GETySyuj8+OIHHvQsIaaaGQ==",
|
||||
"requires": {
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-dropcursor": "^1.0.0",
|
||||
"prosemirror-gapcursor": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-inputrules": "^1.0.0",
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-menu": "^1.0.0",
|
||||
"prosemirror-schema-list": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-gapcursor": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.2.0.tgz",
|
||||
"integrity": "sha512-yCLy5+0rVqLir/KcHFathQj4Rf8aRHi80FmEfKtM0JmyzvwdomslLzDZ/pX4oFhFKDgjl/WBBBFNqDyNifWg7g==",
|
||||
"requires": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-history": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz",
|
||||
"integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-inputrules": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz",
|
||||
"integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-keymap": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz",
|
||||
"integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-markdown": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.6.0.tgz",
|
||||
"integrity": "sha512-y/gRpJIIrNArtkyMax7ypYafb+ZMjddbVHI+AwlcUfCLCCXK57cOmfBMKYVq9kdEKJYVdYHdoyWsVNn1nWLHUg==",
|
||||
"requires": {
|
||||
"markdown-it": "^10.0.0",
|
||||
"prosemirror-model": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prosemirror-menu": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.4.tgz",
|
||||
"integrity": "sha512-2ROsji/X9ciDnVSRvSTqFygI34GEdHfQSsK4zBKjPxSEroeiHHcdRMS1ofNIf2zM0Vpp5/YqfpxynElymQkqzg==",
|
||||
"requires": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-model": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.15.0.tgz",
|
||||
"integrity": "sha512-hQJv7SnIhlAy9ga3lhPPgaufhvCbQB9tHwscJ9E1H1pPHmN8w5V/lURueoYv9Kc3/bpNWoyHa8r3g//m7N0ChQ==",
|
||||
"requires": {
|
||||
"orderedmap": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-schema-list": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.6.tgz",
|
||||
"integrity": "sha512-aFGEdaCWmJzouZ8DwedmvSsL50JpRkqhQ6tcpThwJONVVmCgI36LJHtoQ4VGZbusMavaBhXXr33zyD2IVsTlkw==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-state": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz",
|
||||
"integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-tables": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
|
||||
"integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
|
||||
"requires": {
|
||||
"prosemirror-keymap": "^1.1.2",
|
||||
"prosemirror-model": "^1.8.1",
|
||||
"prosemirror-state": "^1.3.1",
|
||||
"prosemirror-transform": "^1.2.1",
|
||||
"prosemirror-view": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"prosemirror-transform": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
|
||||
"integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-view": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.23.2.tgz",
|
||||
"integrity": "sha512-iPgRw6tpcN+KH1yKmSnRmDKsJBVkWLFP6laHcz9rh/n0Ndz7YKKCDldtw6FhHBYoWmZeubbhV/rrQW0VCDG9iw==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
@@ -3174,11 +2757,6 @@
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"rope-sequence": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz",
|
||||
"integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
@@ -3274,11 +2852,6 @@
|
||||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||
"dev": true
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
@@ -3386,11 +2959,6 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
|
||||
11
package.json
11
package.json
@@ -7,8 +7,6 @@
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
|
||||
"build:js_editor:dev": "esbuild --bundle ./resources/js/editor.js --outfile=public/dist/editor.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js_editor:watch": "chokidar --initial \"./resources/js/editor.js\" \"./resources/js/editor/**/*.js\" -c \"npm run build:js_editor:dev\"",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
@@ -27,18 +25,9 @@
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"crelt": "^1.0.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.6.0",
|
||||
"prosemirror-model": "^1.15.0",
|
||||
"prosemirror-schema-list": "^1.1.6",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-view": "^1.23.2",
|
||||
"sortablejs": "^1.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
72
public/dist/app.js
vendored
Normal file
72
public/dist/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/dist/export-styles.css
vendored
Normal file
1
public/dist/export-styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/dist/print-styles.css
vendored
Normal file
1
public/dist/print-styles.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
:root{--color-primary: #206ea7;--color-primary-light: rgba(32,110,167,0.15);--color-page: #206ea7;--color-page-draft: #7e50b1;--color-chapter: #af4d0d;--color-book: #077b70;--color-bookshelf: #a94747}header{display:none}html,body{font-size:12px;background-color:#fff}.page-content{margin:0 auto}.print-hidden{display:none !important}.tri-layout-container{grid-template-columns:1fr;grid-template-areas:"b";margin-inline-start:0;margin-inline-end:0;display:block}.card{box-shadow:none}.content-wrap.card{padding-inline-start:0;padding-inline-end:0}/*# sourceMappingURL=print-styles.css.map */
|
||||
1
public/dist/styles.css
vendored
Normal file
1
public/dist/styles.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -34,17 +34,13 @@ Big thanks to these companies for supporting the project.
|
||||
Note: Listed services are not tested, vetted nor supported by the official BookStack project in any manner.
|
||||
[View all sponsors](https://github.com/sponsors/ssddanbrown).
|
||||
|
||||
#### Silver Sponsor
|
||||
#### Bronze Sponsors
|
||||
|
||||
<table><tbody><tr>
|
||||
<td><a href="https://www.diagrams.net/" target="_blank">
|
||||
<img width="420" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/diagramsnet.png" alt="Diagrams.net">
|
||||
<img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/diagramsnet.png" alt="Diagrams.net">
|
||||
</a></td>
|
||||
</tr></tbody></table>
|
||||
|
||||
#### Bronze Sponsor
|
||||
|
||||
<table><tbody><tr>
|
||||
<td><a href="https://www.stellarhosted.com/bookstack/" target="_blank">
|
||||
<img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/stellarhosted.png" alt="Stellar Hosted">
|
||||
</a></td>
|
||||
|
||||
@@ -7,8 +7,6 @@ class EntitySelectorPopup {
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.selectButton = this.$refs.select;
|
||||
this.searchInput = this.$refs.searchInput;
|
||||
|
||||
window.EntitySelectorPopup = this;
|
||||
|
||||
this.callback = null;
|
||||
@@ -22,7 +20,6 @@ class EntitySelectorPopup {
|
||||
show(callback) {
|
||||
this.callback = callback;
|
||||
this.elem.components.popup.show();
|
||||
this.searchInput.focus();
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import MarkdownView from "./editor/MarkdownView";
|
||||
import ProseMirrorView from "./editor/ProseMirrorView";
|
||||
|
||||
// Next step: https://prosemirror.net/examples/menu/
|
||||
|
||||
const place = document.querySelector("#editor");
|
||||
let view = new ProseMirrorView(place, document.getElementById('content').innerHTML);
|
||||
|
||||
const markdownToggle = document.getElementById('markdown-toggle');
|
||||
markdownToggle.addEventListener('change', event => {
|
||||
const View = markdownToggle.checked ? MarkdownView : ProseMirrorView;
|
||||
if (view instanceof View) return
|
||||
const content = view.content
|
||||
console.log(content);
|
||||
view.destroy()
|
||||
view = new View(place, content)
|
||||
view.focus()
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import {htmlToDoc, docToHtml} from "./util";
|
||||
|
||||
import parser from "./markdown-parser";
|
||||
import serializer from "./markdown-serializer";
|
||||
|
||||
class MarkdownView {
|
||||
constructor(target, content) {
|
||||
// Build DOM from content
|
||||
const htmlDoc = htmlToDoc(content);
|
||||
const markdown = serializer.serialize(htmlDoc);
|
||||
|
||||
this.textarea = target.appendChild(document.createElement("textarea"))
|
||||
this.textarea.value = markdown;
|
||||
this.textarea.style.width = '1000px';
|
||||
this.textarea.style.height = '1000px';
|
||||
}
|
||||
|
||||
get content() {
|
||||
const markdown = this.textarea.value;
|
||||
const doc = parser.parse(markdown);
|
||||
return docToHtml(doc);
|
||||
}
|
||||
|
||||
focus() { this.textarea.focus() }
|
||||
destroy() { this.textarea.remove() }
|
||||
}
|
||||
|
||||
export default MarkdownView;
|
||||
@@ -1,52 +0,0 @@
|
||||
import {EditorState} from "prosemirror-state";
|
||||
import {EditorView} from "prosemirror-view";
|
||||
import {exampleSetup} from "prosemirror-example-setup";
|
||||
import {tableEditing} from "prosemirror-tables";
|
||||
|
||||
import {DOMParser} from "prosemirror-model";
|
||||
|
||||
import schema from "./schema";
|
||||
import menu from "./menu";
|
||||
import nodeViews from "./node-views";
|
||||
import {stateToHtml} from "./util";
|
||||
import {columnResizing} from "./plugins/table-resizing";
|
||||
|
||||
class ProseMirrorView {
|
||||
constructor(target, content) {
|
||||
|
||||
// Build DOM from content
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.innerHTML = content;
|
||||
|
||||
this.view = new EditorView(target, {
|
||||
state: EditorState.create({
|
||||
doc: DOMParser.fromSchema(schema).parse(renderDoc.body),
|
||||
plugins: [
|
||||
...exampleSetup({schema, menuBar: false}),
|
||||
menu,
|
||||
columnResizing(),
|
||||
tableEditing(),
|
||||
]
|
||||
}),
|
||||
nodeViews,
|
||||
});
|
||||
|
||||
// Fix for native handles (Such as table size handling) in some browsers
|
||||
document.execCommand("enableObjectResizing", false, "false")
|
||||
document.execCommand("enableInlineTableEditing", false, "false")
|
||||
}
|
||||
|
||||
get content() {
|
||||
return stateToHtml(this.view.state);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.view.focus()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.view.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export default ProseMirrorView;
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* @param {String} attrName
|
||||
* @param {String} attrValue
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function setBlockAttr(attrName, attrValue) {
|
||||
return function (state, dispatch) {
|
||||
const ref = state.selection;
|
||||
const from = ref.from;
|
||||
const to = ref.to;
|
||||
let applicable = false;
|
||||
|
||||
state.doc.nodesBetween(from, to, function (node, pos) {
|
||||
if (applicable) {
|
||||
return false
|
||||
}
|
||||
if (!node.isTextblock || node.attrs[attrName] === attrValue) {
|
||||
return
|
||||
}
|
||||
|
||||
applicable = node.attrs[attrName] !== undefined;
|
||||
});
|
||||
|
||||
if (!applicable) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const tr = state.tr;
|
||||
tr.doc.nodesBetween(from, to, function (node, pos) {
|
||||
const nodeAttrs = Object.assign({}, node.attrs);
|
||||
if (node.attrs[attrName] !== undefined) {
|
||||
nodeAttrs[attrName] = attrValue;
|
||||
tr.setBlockType(pos, pos + 1, node.type, nodeAttrs)
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNodeType} blockType
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function insertBlockBefore(blockType) {
|
||||
return function (state, dispatch) {
|
||||
const startPosition = state.selection.$from.before(1);
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.insert(startPosition, blockType.create()));
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} rows
|
||||
* @param {Number} columns
|
||||
* @param {Object} tableAttrs
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function insertTable(rows, columns, tableAttrs) {
|
||||
return function (state, dispatch) {
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const nodes = state.schema.nodes;
|
||||
|
||||
const rowNodes = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
const rowCells = [];
|
||||
for (let x = 0; x < columns; x++) {
|
||||
const cellText = nodes.paragraph.create(null);
|
||||
rowCells.push(nodes.table_cell.create(null, cellText));
|
||||
}
|
||||
rowNodes.push(nodes.table_row.create(null, rowCells));
|
||||
}
|
||||
|
||||
const table = nodes.table.create(tableAttrs, rowNodes);
|
||||
tr.replaceSelectionWith(table);
|
||||
dispatch(tr);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function removeMarks() {
|
||||
return function (state, dispatch) {
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.removeMark(state.selection.from, state.selection.to, null));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import schema from "./schema";
|
||||
import markdownit from "markdown-it";
|
||||
import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown";
|
||||
import {htmlToDoc, KeyedMultiStack} from "./util";
|
||||
|
||||
const tokens = defaultMarkdownParser.tokens;
|
||||
|
||||
// These are really a placeholder on the object to allow the below
|
||||
// parser.tokenHandlers.html_[block/inline] hacks to work as desired.
|
||||
tokens.html_block = {block: "callout", noCloseToken: true};
|
||||
tokens.html_inline = {mark: "underline"};
|
||||
|
||||
const tokenizer = markdownit("commonmark", {html: true});
|
||||
const parser = new MarkdownParser(schema, tokenizer, tokens);
|
||||
|
||||
// When we come across HTML blocks we use the document schema to parse them
|
||||
// into nodes then re-add those back into the parser state.
|
||||
parser.tokenHandlers.html_block = function(state, tok, tokens, i) {
|
||||
const contentDoc = htmlToDoc(tok.content || '');
|
||||
for (const node of contentDoc.content.content) {
|
||||
state.addNode(node.type, node.attrs, node.content);
|
||||
}
|
||||
};
|
||||
|
||||
// When we come across inline HTML we parse out the tag and keep track of
|
||||
// that in a stack, along with the marks they parse out to.
|
||||
// We open/close the marks within the state depending on the tag open/close type.
|
||||
const tagStack = new KeyedMultiStack();
|
||||
parser.tokenHandlers.html_inline = function(state, tok, tokens, i) {
|
||||
const isClosing = tok.content.startsWith('</');
|
||||
const isSelfClosing = tok.content.endsWith('/>');
|
||||
const tagName = parseTagNameFromHtmlTokenContent(tok.content);
|
||||
|
||||
if (!isClosing) {
|
||||
const completeTag = isSelfClosing ? tok.content : `${tok.content}a</${tagName}>`;
|
||||
const marks = extractMarksFromHtml(completeTag);
|
||||
tagStack.push(tagName, marks);
|
||||
for (const mark of marks) {
|
||||
state.openMark(mark);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelfClosing || isClosing) {
|
||||
const marks = (tagStack.pop(tagName) || []).reverse();
|
||||
for (const mark of marks) {
|
||||
state.closeMark(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} html
|
||||
* @return {PmMark[]}
|
||||
*/
|
||||
function extractMarksFromHtml(html) {
|
||||
const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
|
||||
const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;
|
||||
return marks || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tokenContent
|
||||
* @return {string}
|
||||
*/
|
||||
function parseTagNameFromHtmlTokenContent(tokenContent) {
|
||||
return tokenContent.split(' ')[0].replace(/[<>\/]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
export default parser;
|
||||
@@ -1,138 +0,0 @@
|
||||
import {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "prosemirror-markdown";
|
||||
import {docToHtml} from "./util";
|
||||
|
||||
const nodes = defaultMarkdownSerializer.nodes;
|
||||
const marks = defaultMarkdownSerializer.marks;
|
||||
|
||||
|
||||
nodes.callout = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.table = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.iframe = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.details = function (state, node) {
|
||||
wrapNodeWithHtml(state, node, '<details>', '</details>');
|
||||
};
|
||||
|
||||
nodes.details_summary = function(state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
|
||||
return false
|
||||
}
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) {
|
||||
return false
|
||||
}
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) {
|
||||
return true
|
||||
}
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks)
|
||||
}
|
||||
|
||||
marks.link = {
|
||||
open(state, mark, parent, index) {
|
||||
const attrs = mark.attrs;
|
||||
if (attrs.target) {
|
||||
return `<a href="${attrs.target}" ${attrs.title ? `title="${attrs.title}"` : ''} target="${attrs.target}">`
|
||||
}
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "["
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
if (mark.attrs.target) {
|
||||
return `</a>`;
|
||||
}
|
||||
return isPlainURL(mark, parent, index, -1) ? ">"
|
||||
: "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")"
|
||||
}
|
||||
};
|
||||
|
||||
marks.underline = {
|
||||
open: '<span style="text-decoration: underline;">',
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.strike = {
|
||||
open: '<span style="text-decoration: line-through;">',
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.superscript = {
|
||||
open: '<sup>',
|
||||
close: '</sup>',
|
||||
};
|
||||
|
||||
marks.subscript = {
|
||||
open: '<sub>',
|
||||
close: '</sub>',
|
||||
};
|
||||
|
||||
marks.text_color = {
|
||||
open(state, mark, parent, index) {
|
||||
return `<span style="color: ${mark.attrs.color};">`
|
||||
},
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.background_color = {
|
||||
open(state, mark, parent, index) {
|
||||
return `<span style="background-color: ${mark.attrs.color};">`
|
||||
},
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {MarkdownSerializerState} state
|
||||
* @param {PmNode} node
|
||||
*/
|
||||
function writeNodeAsHtml(state, node) {
|
||||
const html = docToHtml({content: [node]});
|
||||
state.write(html);
|
||||
state.ensureNewLine();
|
||||
state.write('\n');
|
||||
state.closeBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownSerializerState} state
|
||||
* @param {PmNode} node
|
||||
* @param {String} openTag
|
||||
* @param {String} closeTag
|
||||
*/
|
||||
function wrapNodeWithHtml(state, node, openTag, closeTag) {
|
||||
state.write(openTag);
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.write(closeTag);
|
||||
state.closeBlock();
|
||||
state.ensureNewLine();
|
||||
state.write('\n');
|
||||
}
|
||||
|
||||
// Update serializers to just write out as HTML if we have an attribute
|
||||
// or element that cannot be represented in commonmark without losing
|
||||
// formatting or content.
|
||||
for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
|
||||
nodes[nodeType] = function (state, node, parent, index) {
|
||||
if (node.attrs.align || node.attrs.height || node.attrs.width) {
|
||||
writeNodeAsHtml(state, node);
|
||||
} else {
|
||||
serializerFunction(state, node, parent, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const serializer = new MarkdownSerializer(nodes, marks);
|
||||
|
||||
export default serializer;
|
||||
@@ -1,62 +0,0 @@
|
||||
import crel from "crelt"
|
||||
import {prefix} from "./menu-utils";
|
||||
import {TextSelection} from "prosemirror-state"
|
||||
import {expandSelectionToMark} from "../util";
|
||||
|
||||
|
||||
class ColorPickerGrid {
|
||||
|
||||
constructor(markType, attrName, colors) {
|
||||
this.markType = markType;
|
||||
this.colors = colors
|
||||
this.attrName = attrName;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const colorElems = [];
|
||||
for (const color of this.colors) {
|
||||
const elem = crel("div", {class: prefix + "-color-grid-item", style: `background-color: ${color};`});
|
||||
colorElems.push(elem);
|
||||
}
|
||||
|
||||
const wrap = crel("div", {class: prefix + "-color-grid-container"}, colorElems);
|
||||
wrap.addEventListener('click', event => {
|
||||
if (event.target.classList.contains(prefix + "-color-grid-item")) {
|
||||
const color = event.target.style.backgroundColor;
|
||||
this.onColorSelect(view, color);
|
||||
}
|
||||
});
|
||||
|
||||
function update(state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
onColorSelect(view, color) {
|
||||
const attrs = {[this.attrName]: color};
|
||||
const selection = view.state.selection;
|
||||
const {from, to} = expandSelectionToMark(view.state, selection, this.markType);
|
||||
const tr = view.state.tr;
|
||||
|
||||
const currentColorMarks = selection.$from.marksAcross(selection.$to) || [];
|
||||
const activeRelevantMark = currentColorMarks.filter(mark => {
|
||||
return mark.type === this.markType;
|
||||
})[0];
|
||||
const colorIsActive = activeRelevantMark && activeRelevantMark.attrs[this.attrName] === color;
|
||||
|
||||
tr.removeMark(from, to, this.markType);
|
||||
if (!colorIsActive) {
|
||||
tr.addMark(from, to, this.markType.create(attrs));
|
||||
}
|
||||
|
||||
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
||||
view.dispatch(tr);
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorPickerGrid;
|
||||
@@ -1,59 +0,0 @@
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, renderItems} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
import {getIcon, icons} from "./icons";
|
||||
|
||||
class DialogBox {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the dialog.
|
||||
// **`closer`**`: function`
|
||||
// : The function to run when the dialog should close.
|
||||
constructor(content, options) {
|
||||
this.options = options || {};
|
||||
this.content = Array.isArray(content) ? content : [content];
|
||||
|
||||
this.closeMouseDownListener = null;
|
||||
this.wrap = null;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderItems(this.content, view)
|
||||
|
||||
const titleText = crel("div", {class: prefix + "-dialog-title-text"}, this.options.label);
|
||||
const titleClose = crel("button", {class: prefix + "-dialog-title-close primary-background", type: "button"}, getIcon(icons.close));
|
||||
const titleContent = crel("div", {class: prefix + "-dialog-title"}, titleText, titleClose);
|
||||
const dialog = crel("div", {class: prefix + "-dialog"}, titleContent,
|
||||
crel("div", {class: prefix + "-dialog-content"}, items.dom));
|
||||
const wrap = crel("div", {class: prefix + "-dialog-wrap"}, dialog);
|
||||
this.wrap = wrap;
|
||||
|
||||
this.closeMouseDownListener = (event) => {
|
||||
if (!dialog.contains(event.target) || titleClose.contains(event.target)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
wrap.addEventListener("click", this.closeMouseDownListener);
|
||||
|
||||
function update(state) {
|
||||
let inner = items.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner;
|
||||
}
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.options.closer) {
|
||||
this.options.closer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DialogBox;
|
||||
@@ -1,51 +0,0 @@
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, renderItems} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogForm {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`action`**`: function(FormData)`
|
||||
// : The submission action to run when the form is submitted.
|
||||
// **`canceler`**`: function`
|
||||
// : The cancel action to run when the form is cancelled.
|
||||
constructor(content, options) {
|
||||
this.options = options || {};
|
||||
this.content = Array.isArray(content) ? content : [content];
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderItems(this.content, view)
|
||||
|
||||
const formButtonCancel = crel("button", {class: prefix + "-dialog-button", type: "button"}, "Cancel");
|
||||
const formButtonSave = crel("button", {class: prefix + "-dialog-button", type: "submit"}, "Save");
|
||||
const footer = crel("div", {class: prefix + "-dialog-footer"}, formButtonCancel, formButtonSave);
|
||||
const form = crel("form", {class: prefix + "-dialog-form", action: '#'}, items.dom, footer);
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
if (this.options.action) {
|
||||
this.options.action(new FormData(form));
|
||||
}
|
||||
});
|
||||
|
||||
formButtonCancel.addEventListener('click', event => {
|
||||
if (this.options.canceler) {
|
||||
this.options.canceler();
|
||||
}
|
||||
});
|
||||
|
||||
function update(state) {
|
||||
return items.update(state);
|
||||
}
|
||||
|
||||
return {dom: form, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogForm;
|
||||
@@ -1,42 +0,0 @@
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogInput {
|
||||
// :: (?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show for the input.
|
||||
// **`id`**`: string`
|
||||
// : The id to use for this input
|
||||
// **`attrs`**`: Object`
|
||||
// : The attributes to add to the input element.
|
||||
// **`value`**`: function(state) -> string`
|
||||
// : The getter for the input value.
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const id = randHtmlId();
|
||||
const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {})
|
||||
const input = crel("input", inputAttrs);
|
||||
const label = crel("label", {for: id}, this.options.label);
|
||||
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-form-row'}, label, input);
|
||||
|
||||
const update = (state) => {
|
||||
input.value = this.options.value(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogInput;
|
||||
@@ -1,53 +0,0 @@
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogRadioOptions {
|
||||
/**
|
||||
* Given inputOptions should be keyed by label, with values being values.
|
||||
* Values of empty string will be treated as null.
|
||||
* @param {Object} inputOptions
|
||||
* @param {{label: string, id: string, attrs?: Object, value: function(PmEditorState): string|null}} options
|
||||
*/
|
||||
constructor(inputOptions, options) {
|
||||
this.inputOptions = inputOptions;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const inputs = [];
|
||||
const optionInputLabels = Object.keys(this.inputOptions).map(label => {
|
||||
const inputAttrs = Object.assign({
|
||||
type: "radio",
|
||||
name: this.options.id,
|
||||
value: this.inputOptions[label],
|
||||
class: prefix + '-dialog-radio-option',
|
||||
}, this.options.attrs || {});
|
||||
const input = crel("input", inputAttrs);
|
||||
inputs.push(input);
|
||||
return crel("label", input, label);
|
||||
});
|
||||
|
||||
const optionInputWrap = crel("div", {class: prefix + '-dialog-radio-option-wrap'}, optionInputLabels);
|
||||
|
||||
const label = crel("label", {}, this.options.label);
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-form-row'}, label, optionInputWrap);
|
||||
|
||||
const update = (state) => {
|
||||
const value = this.options.value(state);
|
||||
for (const input of inputs) {
|
||||
input.checked = (input.value === value || (value === null && input.value === ""));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogRadioOptions;
|
||||
@@ -1,42 +0,0 @@
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogTextArea {
|
||||
// :: (?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show for the input.
|
||||
// **`id`**`: string`
|
||||
// : The id to use for this input
|
||||
// **`attrs`**`: Object`
|
||||
// : The attributes to add to the input element.
|
||||
// **`value`**`: function(state) -> string`
|
||||
// : The getter for the input value.
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const id = randHtmlId();
|
||||
const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {})
|
||||
const input = crel("textarea", inputAttrs);
|
||||
const label = this.options.label ? crel("label", {for: id}, this.options.label) : null;
|
||||
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-textarea-wrap'}, label, input);
|
||||
|
||||
const update = (state) => {
|
||||
input.value = this.options.value(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogTextArea;
|
||||
@@ -1,86 +0,0 @@
|
||||
import crel from "crelt"
|
||||
import {prefix} from "./menu-utils";
|
||||
import {insertTable} from "../commands";
|
||||
|
||||
class TableCreatorGrid {
|
||||
|
||||
constructor() {
|
||||
this.size = 10;
|
||||
this.label = null;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const gridItems = [];
|
||||
for (let y = 0; y < this.size; y++) {
|
||||
for (let x = 0; x < this.size; x++) {
|
||||
const elem = crel("div", {class: prefix + "-table-creator-grid-item"});
|
||||
gridItems.push(elem);
|
||||
elem.addEventListener('mouseenter', event => {
|
||||
this.updateGridItemActiveStatus(elem, gridItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const gridWrap = crel("div", {
|
||||
class: prefix + "-table-creator-grid",
|
||||
style: `grid-template-columns: repeat(${this.size}, 14px);`,
|
||||
}, gridItems);
|
||||
|
||||
gridWrap.addEventListener('mouseleave', event => {
|
||||
this.updateGridItemActiveStatus(null, gridItems);
|
||||
});
|
||||
gridWrap.addEventListener('click', event => {
|
||||
if (event.target.classList.contains(prefix + "-table-creator-grid-item")) {
|
||||
const {x, y} = this.getPositionOfGridItem(event.target, gridItems);
|
||||
insertTable(y + 1, x + 1, {
|
||||
style: 'width: 100%;',
|
||||
})(view.state, view.dispatch);
|
||||
}
|
||||
});
|
||||
|
||||
const gridLabel = crel("div", {class: prefix + "-table-creator-grid-label"});
|
||||
this.label = gridLabel;
|
||||
const wrap = crel("div", {class: prefix + "-table-creator-grid-container"}, [gridWrap, gridLabel]);
|
||||
|
||||
function update(state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element|null} newTarget
|
||||
* @param {Element[]} gridItems
|
||||
*/
|
||||
updateGridItemActiveStatus(newTarget, gridItems) {
|
||||
const {x: xPos, y: yPos} = this.getPositionOfGridItem(newTarget, gridItems);
|
||||
|
||||
for (let y = 0; y < this.size; y++) {
|
||||
for (let x = 0; x < this.size; x++) {
|
||||
const active = x <= xPos && y <= yPos;
|
||||
const index = (y * this.size) + x;
|
||||
gridItems[index].classList.toggle(prefix + "-table-creator-grid-item-active", active);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.textContent = (xPos + yPos < 0) ? '' : `${xPos + 1} x ${yPos + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} gridItem
|
||||
* @param {Element[]} gridItems
|
||||
* @return {{x: number, y: number}}
|
||||
*/
|
||||
getPositionOfGridItem(gridItem, gridItems) {
|
||||
const index = gridItems.indexOf(gridItem);
|
||||
const y = Math.floor(index / this.size);
|
||||
const x = index % this.size;
|
||||
return {x, y};
|
||||
}
|
||||
}
|
||||
|
||||
export default TableCreatorGrid;
|
||||
@@ -1,162 +0,0 @@
|
||||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
// :: Object
|
||||
// A set of basic editor-related icons. Contains the properties
|
||||
// `join`, `lift`, `selectParentNode`, `undo`, `redo`, `strong`, `em`,
|
||||
// `code`, `link`, `bulletList`, `orderedList`, and `blockquote`, each
|
||||
// holding an object that can be used as the `icon` option to
|
||||
// `MenuItem`.
|
||||
export const icons = {
|
||||
undo: {
|
||||
width: 24, height: 24,
|
||||
path: "M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"
|
||||
},
|
||||
redo: {
|
||||
width: 24, height: 24,
|
||||
path: "M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"
|
||||
},
|
||||
strong: {
|
||||
width: 24, height: 24,
|
||||
path: "M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"
|
||||
},
|
||||
em: {
|
||||
width: 24, height: 24,
|
||||
path: "M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"
|
||||
},
|
||||
link: {
|
||||
width: 24, height: 24,
|
||||
path: "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"
|
||||
},
|
||||
bullet_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"
|
||||
},
|
||||
ordered_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"
|
||||
},
|
||||
task_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,7h-9v2h9V7z M22,15h-9v2h9V15z M5.54,11L2,7.46l1.41-1.41l2.12,2.12l4.24-4.24l1.41,1.41L5.54,11z M5.54,19L2,15.46 l1.41-1.41l2.12,2.12l4.24-4.24l1.41,1.41L5.54,19z"
|
||||
},
|
||||
underline: {
|
||||
width: 24, height: 24,
|
||||
path: "M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z"
|
||||
},
|
||||
strike: {
|
||||
width: 24, height: 24,
|
||||
path: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"
|
||||
},
|
||||
superscript: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,7h-2v1h3v1h-4V7c0-0.55,0.45-1,1-1h2V5h-3V4h3c0.55,0,1,0.45,1,1v1C23,6.55,22.55,7,22,7z M5.88,20h2.66l3.4-5.42h0.12 l3.4,5.42h2.66l-4.65-7.27L17.81,6h-2.68l-3.07,4.99h-0.12L8.85,6H6.19l4.32,6.73L5.88,20z"
|
||||
},
|
||||
subscript: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,18h-2v1h3v1h-4v-2c0-0.55,0.45-1,1-1h2v-1h-3v-1h3c0.55,0,1,0.45,1,1v1C23,17.55,22.55,18,22,18z M5.88,18h2.66 l3.4-5.42h0.12l3.4,5.42h2.66l-4.65-7.27L17.81,4h-2.68l-3.07,4.99h-0.12L8.85,4H6.19l4.32,6.73L5.88,18z"
|
||||
},
|
||||
text_color: {
|
||||
width: 24, height: 24,
|
||||
path: "M2,20h20v4H2V20z M5.49,17h2.42l1.27-3.58h5.65L16.09,17h2.42L13.25,3h-2.5L5.49,17z M9.91,11.39l2.03-5.79h0.12l2.03,5.79 H9.91z"
|
||||
},
|
||||
background_color: {
|
||||
width: 24, height: 24,
|
||||
path: "M16.56,8.94L7.62,0L6.21,1.41l2.38,2.38L3.44,8.94c-0.59,0.59-0.59,1.54,0,2.12l5.5,5.5C9.23,16.85,9.62,17,10,17 s0.77-0.15,1.06-0.44l5.5-5.5C17.15,10.48,17.15,9.53,16.56,8.94z M5.21,10L10,5.21L14.79,10H5.21z M19,11.5c0,0-2,2.17-2,3.5 c0,1.1,0.9,2,2,2s2-0.9,2-2C21,13.67,19,11.5,19,11.5z M2,20h20v4H2V20z"
|
||||
},
|
||||
align_left: {
|
||||
width: 24, height: 24,
|
||||
path: "M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_right: {
|
||||
width: 24, height: 24,
|
||||
path: "M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_center: {
|
||||
width: 24, height: 24,
|
||||
path: "M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_justify: {
|
||||
width: 24, height: 24,
|
||||
path: "M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z"
|
||||
},
|
||||
horizontal_rule: {
|
||||
width: 24, height: 24,
|
||||
path: "m 4,11 h 16 v 2 H 4 Z"
|
||||
},
|
||||
format_clear: {
|
||||
width: 24, height: 24,
|
||||
path: "M3.27 5L2 6.27l6.97 6.97L6.5 19h3l1.57-3.66L16.73 21 18 19.73 3.55 5.27 3.27 5zM6 5v.18L8.82 8h2.4l-.72 1.68 2.1 2.1L14.21 8H20V5H6z"
|
||||
},
|
||||
close: {
|
||||
width: 24, height: 24,
|
||||
path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
||||
},
|
||||
source_code: {
|
||||
width: 24, height: 24,
|
||||
path: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z",
|
||||
},
|
||||
table: {
|
||||
width: 24, height: 24,
|
||||
path: "M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z",
|
||||
},
|
||||
iframe: {
|
||||
width: 24, height: 24,
|
||||
path: "m 22.71,18.43 c 0.03,-0.29 0.04,-0.58 0.01,-0.86 l 1.07,-0.85 c 0.1,-0.08 0.12,-0.21 0.06,-0.32 L 22.82,14.61 C 22.76,14.5 22.63,14.46 22.51,14.5 L 21.23,15 C 21,14.83 20.75,14.69 20.48,14.58 l -0.2,-1.36 C 20.26,13.09 20.16,13 20.03,13 h -2.07 c -0.12,0 -0.23,0.09 -0.25,0.21 l -0.2,1.36 c -0.26,0.11 -0.51,0.26 -0.74,0.42 l -1.28,-0.5 c -0.12,-0.05 -0.25,0 -0.31,0.11 l -1.03,1.79 c -0.06,0.11 -0.04,0.24 0.06,0.32 l 1.07,0.86 c -0.03,0.29 -0.04,0.58 -0.01,0.86 l -1.07,0.85 c -0.1,0.08 -0.12,0.21 -0.06,0.32 l 1.03,1.79 c 0.06,0.11 0.19,0.15 0.31,0.11 L 16.75,21 c 0.23,0.17 0.48,0.31 0.75,0.42 l 0.2,1.36 c 0.02,0.12 0.12,0.21 0.25,0.21 h 2.07 c 0.12,0 0.23,-0.09 0.25,-0.21 l 0.2,-1.36 c 0.26,-0.11 0.51,-0.26 0.74,-0.42 l 1.28,0.5 c 0.12,0.05 0.25,0 0.31,-0.11 l 1.03,-1.79 c 0.06,-0.11 0.04,-0.24 -0.06,-0.32 z M 19,19.5 c -0.83,0 -1.5,-0.67 -1.5,-1.5 0,-0.83 0.67,-1.5 1.5,-1.5 0.83,0 1.5,0.67 1.5,1.5 0,0.83 -0.67,1.5 -1.5,1.5 z M 15,12 9,8 v 8 z M 3,6 h 18 v 5 h 2 V 6 C 23,4.9 22.1,4 21,4 H 3 C 1.9,4 1,4.9 1,6 v 12 c 0,1.1 0.9,2 2,2 h 9 V 18 H 3 Z",
|
||||
},
|
||||
details: {
|
||||
width: 24, height: 24,
|
||||
path: "m 7,10 5,5 5,-5 z M 19,2.5 H 5 c -1.11,0 -2,0.9 -2,2 v 14 c 0,1.1 0.89,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 v -14 c 0,-1.1 -0.89,-2 -2,-2 z m 0,16 H 5 v -12 h 14 z",
|
||||
}
|
||||
};
|
||||
|
||||
const SVG = "http://www.w3.org/2000/svg"
|
||||
const XLINK = "http://www.w3.org/1999/xlink"
|
||||
|
||||
const prefix = "ProseMirror-icon"
|
||||
|
||||
function hashPath(path) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < path.length; i++)
|
||||
hash = (((hash << 5) - hash) + path.charCodeAt(i)) | 0
|
||||
return hash
|
||||
}
|
||||
|
||||
export function getIcon(icon) {
|
||||
let node = document.createElement("div")
|
||||
node.className = prefix
|
||||
if (icon.path) {
|
||||
let name = "pm-icon-" + hashPath(icon.path).toString(16)
|
||||
if (!document.getElementById(name)) buildSVG(name, icon)
|
||||
let svg = node.appendChild(document.createElementNS(SVG, "svg"))
|
||||
svg.style.width = (icon.width / icon.height) + "em"
|
||||
let use = svg.appendChild(document.createElementNS(SVG, "use"))
|
||||
use.setAttributeNS(XLINK, "href", /([^#]*)/.exec(document.location)[1] + "#" + name)
|
||||
} else if (icon.dom) {
|
||||
node.appendChild(icon.dom.cloneNode(true))
|
||||
} else {
|
||||
node.appendChild(document.createElement("span")).textContent = icon.text || ''
|
||||
if (icon.css) node.firstChild.style.cssText = icon.css
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
function buildSVG(name, data) {
|
||||
let collection = document.getElementById(prefix + "-collection")
|
||||
if (!collection) {
|
||||
collection = document.createElementNS(SVG, "svg")
|
||||
collection.id = prefix + "-collection"
|
||||
collection.style.display = "none"
|
||||
document.body.insertBefore(collection, document.body.firstChild)
|
||||
}
|
||||
let sym = document.createElementNS(SVG, "symbol")
|
||||
sym.id = name
|
||||
sym.setAttribute("viewBox", "0 0 " + data.width + " " + data.height)
|
||||
let path = sym.appendChild(document.createElementNS(SVG, "path"))
|
||||
path.setAttribute("d", data.path)
|
||||
collection.appendChild(sym)
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
import {
|
||||
MenuItem, Dropdown, DropdownSubmenu, renderGrouped, joinUpItem, liftItem, selectParentNodeItem,
|
||||
undoItem, redoItem, wrapItem, blockTypeItem, setAttrItem, insertBlockBeforeItem,
|
||||
} from "./menu"
|
||||
import {icons} from "./icons";
|
||||
import ColorPickerGrid from "./ColorPickerGrid";
|
||||
import TableCreatorGrid from "./TableCreatorGrid";
|
||||
import {toggleMark} from "prosemirror-commands";
|
||||
import {menuBar} from "./menubar"
|
||||
import schema from "../schema";
|
||||
import {removeMarks} from "../commands";
|
||||
|
||||
import itemAnchorButtonItem from "./item-anchor-button";
|
||||
import itemHtmlSourceButton from "./item-html-source-button";
|
||||
import itemIframeButton from "./item-iframe-button";
|
||||
|
||||
|
||||
function cmdItem(cmd, options) {
|
||||
const passedOptions = {
|
||||
label: options.title,
|
||||
run: cmd
|
||||
};
|
||||
for (const prop in options) {
|
||||
passedOptions[prop] = options[prop];
|
||||
}
|
||||
if ((!options.enable || options.enable === true) && !options.select) {
|
||||
passedOptions[options.enable ? "enable" : "select"] = function (state) {
|
||||
return cmd(state);
|
||||
};
|
||||
}
|
||||
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
function markActive(state, type) {
|
||||
const ref = state.selection;
|
||||
const from = ref.from;
|
||||
const $from = ref.$from;
|
||||
const to = ref.to;
|
||||
const empty = ref.empty;
|
||||
if (empty) {
|
||||
return type.isInSet(state.storedMarks || $from.marks())
|
||||
} else {
|
||||
return state.doc.rangeHasMark(from, to, type)
|
||||
}
|
||||
}
|
||||
|
||||
function markItem(markType, options) {
|
||||
const passedOptions = {
|
||||
active: function active(state) {
|
||||
return markActive(state, markType)
|
||||
},
|
||||
enable: true
|
||||
};
|
||||
for (const prop in options) {
|
||||
passedOptions[prop] = options[prop];
|
||||
}
|
||||
|
||||
return cmdItem(toggleMark(markType, passedOptions.attrs), passedOptions)
|
||||
}
|
||||
|
||||
const inlineStyles = [
|
||||
markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
|
||||
markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
|
||||
markItem(schema.marks.underline, {title: "Underline", icon: icons.underline}),
|
||||
markItem(schema.marks.strike, {title: "Strikethrough", icon: icons.strike}),
|
||||
markItem(schema.marks.superscript, {title: "Superscript", icon: icons.superscript}),
|
||||
markItem(schema.marks.subscript, {title: "Subscript", icon: icons.subscript}),
|
||||
];
|
||||
|
||||
const formats = [
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Large",
|
||||
attrs: {level: 2}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Medium",
|
||||
attrs: {level: 3}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Small",
|
||||
attrs: {level: 4}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Tiny",
|
||||
attrs: {level: 5}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.paragraph, {
|
||||
label: "Paragraph",
|
||||
attrs: {}
|
||||
}),
|
||||
markItem(schema.marks.code, {
|
||||
label: "Inline Code",
|
||||
attrs: {}
|
||||
}),
|
||||
new DropdownSubmenu([
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Info Callout",
|
||||
attrs: {type: 'info'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Danger Callout",
|
||||
attrs: {type: 'danger'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Success Callout",
|
||||
attrs: {type: 'success'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Warning Callout",
|
||||
attrs: {type: 'warning'}
|
||||
})
|
||||
], { label: 'Callouts' }),
|
||||
];
|
||||
|
||||
const alignments = [
|
||||
setAttrItem('align', 'left', {
|
||||
icon: icons.align_left
|
||||
}),
|
||||
setAttrItem('align', 'center', {
|
||||
icon: icons.align_center
|
||||
}),
|
||||
setAttrItem('align', 'right', {
|
||||
icon: icons.align_right
|
||||
}),
|
||||
setAttrItem('align', 'justify', {
|
||||
icon: icons.align_justify
|
||||
}),
|
||||
];
|
||||
|
||||
const colorOptions = ["#000000","#993300","#333300","#003300","#003366","#000080","#333399","#333333","#800000","#FF6600","#808000","#008000","#008080","#0000FF","#666699","#808080","#FF0000","#FF9900","#99CC00","#339966","#33CCCC","#3366FF","#800080","#999999","#FF00FF","#FFCC00","#FFFF00","#00FF00","#00FFFF","#00CCFF","#993366","#FFFFFF","#FF99CC","#FFCC99","#FFFF99","#CCFFCC","#CCFFFF","#99CCFF","#CC99FF"];
|
||||
|
||||
const colors = [
|
||||
new DropdownSubmenu([
|
||||
new ColorPickerGrid(schema.marks.text_color, 'color', colorOptions),
|
||||
], {icon: icons.text_color}),
|
||||
new DropdownSubmenu([
|
||||
new ColorPickerGrid(schema.marks.background_color, 'color', colorOptions),
|
||||
], {icon: icons.background_color}),
|
||||
];
|
||||
|
||||
const lists = [
|
||||
wrapItem(schema.nodes.bullet_list, {
|
||||
title: "Bullet List",
|
||||
icon: icons.bullet_list,
|
||||
}),
|
||||
wrapItem(schema.nodes.ordered_list, {
|
||||
title: "Ordered List",
|
||||
icon: icons.ordered_list,
|
||||
}),
|
||||
];
|
||||
|
||||
const inserts = [
|
||||
itemAnchorButtonItem(),
|
||||
insertBlockBeforeItem(schema.nodes.horizontal_rule, {
|
||||
title: "Horizontal Rule",
|
||||
icon: icons.horizontal_rule,
|
||||
}),
|
||||
new DropdownSubmenu([
|
||||
new TableCreatorGrid()
|
||||
], {icon: icons.table}),
|
||||
itemIframeButton(),
|
||||
wrapItem(schema.nodes.details, {
|
||||
title: "Dropdown Block",
|
||||
icon: icons.details,
|
||||
})
|
||||
];
|
||||
|
||||
const utilities = [
|
||||
new MenuItem({
|
||||
title: 'Clear Formatting',
|
||||
icon: icons.format_clear,
|
||||
run: removeMarks(),
|
||||
enable: state => true,
|
||||
}),
|
||||
itemHtmlSourceButton(),
|
||||
];
|
||||
|
||||
const menu = menuBar({
|
||||
floating: false,
|
||||
content: [
|
||||
[undoItem, redoItem],
|
||||
[new DropdownSubmenu(formats, { label: 'Formats' })],
|
||||
inlineStyles,
|
||||
colors,
|
||||
alignments,
|
||||
lists,
|
||||
inserts,
|
||||
utilities,
|
||||
],
|
||||
});
|
||||
|
||||
export default menu;
|
||||
|
||||
// !! This module defines a number of building blocks for ProseMirror
|
||||
// menus, along with a [menu bar](#menu.menuBar) implementation.
|
||||
|
||||
// MenuElement:: interface
|
||||
// The types defined in this module aren't the only thing you can
|
||||
// display in your menu. Anything that conforms to this interface can
|
||||
// be put into a menu structure.
|
||||
//
|
||||
// render:: (pm: EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Render the element for display in the menu. Must return a DOM
|
||||
// element and a function that can be used to update the element to
|
||||
// a new state. The `update` function will return false if the
|
||||
// update hid the entire element.
|
||||
@@ -1,120 +0,0 @@
|
||||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogInput from "./DialogInput";
|
||||
import DialogRadioOptions from "./DialogRadioOptions";
|
||||
import schema from "../schema";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {expandSelectionToMark, nullifyEmptyValues} from "../util";
|
||||
|
||||
/**
|
||||
* @param {PmMarkType} markType
|
||||
* @param {String} attribute
|
||||
* @return {(function(PmEditorState): (string|null))}
|
||||
*/
|
||||
function getMarkAttribute(markType, attribute) {
|
||||
return function (state) {
|
||||
const marks = state.selection.$head.marks();
|
||||
for (const mark of marks) {
|
||||
if (mark.type === markType) {
|
||||
return mark.attrs[attribute];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogInput({
|
||||
label: 'URL',
|
||||
id: 'href',
|
||||
value: getMarkAttribute(schema.marks.link, 'href'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Hover Label',
|
||||
id: 'title',
|
||||
value: getMarkAttribute(schema.marks.link, 'title'),
|
||||
}),
|
||||
new DialogRadioOptions({
|
||||
"Same tab or window": "",
|
||||
"New tab or window": "_blank",
|
||||
}, {
|
||||
label: 'Behaviour',
|
||||
id: 'target',
|
||||
value: getMarkAttribute(schema.marks.link, 'target'),
|
||||
})
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'Insert Link',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function applyLink(formData, state, dispatch) {
|
||||
const selection = state.selection;
|
||||
const attrs = nullifyEmptyValues(Object.fromEntries(formData));
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const {from, to} = expandSelectionToMark(state, selection, schema.marks.link);
|
||||
|
||||
if (attrs.href) {
|
||||
tr.addMark(from, to, schema.marks.link.create(attrs));
|
||||
} else {
|
||||
tr.removeMark(from, to, schema.marks.link);
|
||||
}
|
||||
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
applyLink(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function anchorButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "Insert/Edit Anchor Link",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
icon: icons.link,
|
||||
});
|
||||
}
|
||||
|
||||
export default anchorButtonItem;
|
||||
@@ -1,87 +0,0 @@
|
||||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogTextArea from "./DialogTextArea";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {htmlToDoc, stateToHtml} from "../util";
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogTextArea({
|
||||
id: 'source',
|
||||
value: stateToHtml,
|
||||
attrs: {
|
||||
rows: 10,
|
||||
cols: 50,
|
||||
}
|
||||
}),
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'View/Edit HTML Source',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function replaceEditorHtml(formData, state, dispatch) {
|
||||
const html = formData.get('source');
|
||||
|
||||
if (dispatch) {
|
||||
const tr = state.tr;
|
||||
|
||||
const newDoc = htmlToDoc(html);
|
||||
tr.replaceWith(0, state.doc.content.size, newDoc.content);
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
replaceEditorHtml(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function htmlSourceButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "View HTML Source",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
icon: icons.source_code,
|
||||
});
|
||||
}
|
||||
|
||||
export default htmlSourceButtonItem;
|
||||
@@ -1,115 +0,0 @@
|
||||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogInput from "./DialogInput";
|
||||
import schema from "../schema";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {nullifyEmptyValues} from "../util";
|
||||
|
||||
/**
|
||||
* @param {PmNodeType} nodeType
|
||||
* @param {String} attribute
|
||||
* @return {(function(PmEditorState): (string|null))}
|
||||
*/
|
||||
function getNodeAttribute(nodeType, attribute) {
|
||||
return function (state) {
|
||||
const node = state.selection.node;
|
||||
if (node && node.type === nodeType) {
|
||||
return node.attrs[attribute];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogInput({
|
||||
label: 'Source URL',
|
||||
id: 'src',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'src'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Hover Label',
|
||||
id: 'title',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'title'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Width',
|
||||
id: 'width',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'width'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Height',
|
||||
id: 'height',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'height'),
|
||||
}),
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'Insert Embedded Content',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function applyIframe(formData, state, dispatch) {
|
||||
const attrs = nullifyEmptyValues(Object.fromEntries(formData));
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const currentNodeAttrs = state.selection?.nodes?.attrs || {};
|
||||
const newAttrs = Object.assign({}, currentNodeAttrs, attrs);
|
||||
tr.replaceSelectionWith(schema.nodes.iframe.create(newAttrs));
|
||||
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
applyIframe(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function iframeButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "Embed Content",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
active: state => (state.selection.node || {type: ''}).type === schema.nodes.iframe,
|
||||
icon: icons.iframe,
|
||||
});
|
||||
}
|
||||
|
||||
export default iframeButtonItem;
|
||||
@@ -1,39 +0,0 @@
|
||||
import crel from "crelt";
|
||||
|
||||
export const prefix = "ProseMirror-menu";
|
||||
|
||||
export function renderDropdownItems(items, view) {
|
||||
let rendered = [], updates = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let {dom, update} = items[i].render(view)
|
||||
rendered.push(crel("div", {class: prefix + "-dropdown-item"}, dom))
|
||||
updates.push(update)
|
||||
}
|
||||
return {dom: rendered, update: combineUpdates(updates, rendered)}
|
||||
}
|
||||
|
||||
export function renderItems(items, view) {
|
||||
let rendered = [], updates = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let {dom, update} = items[i].render(view)
|
||||
rendered.push(dom);
|
||||
updates.push(update)
|
||||
}
|
||||
return {dom: rendered, update: combineUpdates(updates, rendered)}
|
||||
}
|
||||
|
||||
export function combineUpdates(updates, nodes) {
|
||||
return state => {
|
||||
let something = false
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
let up = updates[i](state)
|
||||
nodes[i].style.display = up ? "" : "none"
|
||||
if (up) something = true
|
||||
}
|
||||
return something
|
||||
}
|
||||
}
|
||||
|
||||
export function randHtmlId() {
|
||||
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 9);
|
||||
}
|
||||
@@ -1,419 +0,0 @@
|
||||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import crel from "crelt"
|
||||
import {lift, joinUp, selectParentNode, wrapIn, setBlockType, toggleMark} from "prosemirror-commands"
|
||||
import {undo, redo} from "prosemirror-history"
|
||||
import {setBlockAttr, insertBlockBefore} from "../commands";
|
||||
import {renderDropdownItems, combineUpdates} from "./menu-utils";
|
||||
|
||||
import {getIcon, icons} from "./icons"
|
||||
import {prefix} from "./menu-utils";
|
||||
|
||||
// ::- An icon or label that, when clicked, executes a command.
|
||||
export class MenuItem {
|
||||
// :: (MenuItemSpec)
|
||||
constructor(spec) {
|
||||
// :: MenuItemSpec
|
||||
// The spec used to create the menu item.
|
||||
this.spec = spec
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the icon according to its [display
|
||||
// spec](#menu.MenuItemSpec.display), and adds an event handler which
|
||||
// executes the command when the representation is clicked.
|
||||
render(view) {
|
||||
let spec = this.spec
|
||||
let dom = spec.render ? spec.render(view)
|
||||
: spec.icon ? getIcon(spec.icon)
|
||||
: spec.label ? crel("div", null, translate(view, spec.label))
|
||||
: null
|
||||
if (!dom) throw new RangeError("MenuItem without icon or label property")
|
||||
if (spec.title) {
|
||||
const title = (typeof spec.title === "function" ? spec.title(view.state) : spec.title)
|
||||
dom.setAttribute("title", translate(view, title))
|
||||
}
|
||||
if (spec.class) dom.classList.add(spec.class)
|
||||
if (spec.css) dom.style.cssText += spec.css
|
||||
|
||||
dom.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
if (!dom.classList.contains(prefix + "-disabled"))
|
||||
spec.run(view.state, view.dispatch, view, e)
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
if (spec.select) {
|
||||
let selected = spec.select(state)
|
||||
dom.style.display = selected ? "" : "none"
|
||||
if (!selected) return false
|
||||
}
|
||||
let enabled = true
|
||||
if (spec.enable) {
|
||||
enabled = spec.enable(state) || false
|
||||
setClass(dom, prefix + "-disabled", !enabled)
|
||||
}
|
||||
if (spec.active) {
|
||||
let active = enabled && spec.active(state) || false
|
||||
setClass(dom, prefix + "-active", active)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return {dom, update}
|
||||
}
|
||||
}
|
||||
|
||||
function translate(view, text) {
|
||||
return view._props.translate ? view._props.translate(text) : text
|
||||
}
|
||||
|
||||
// MenuItemSpec:: interface
|
||||
// The configuration object passed to the `MenuItem` constructor.
|
||||
//
|
||||
// run:: (EditorState, (Transaction), EditorView, dom.Event)
|
||||
// The function to execute when the menu item is activated.
|
||||
//
|
||||
// select:: ?(EditorState) → bool
|
||||
// Optional function that is used to determine whether the item is
|
||||
// appropriate at the moment. Deselected items will be hidden.
|
||||
//
|
||||
// enable:: ?(EditorState) → bool
|
||||
// Function that is used to determine if the item is enabled. If
|
||||
// given and returning false, the item will be given a disabled
|
||||
// styling.
|
||||
//
|
||||
// active:: ?(EditorState) → bool
|
||||
// A predicate function to determine whether the item is 'active' (for
|
||||
// example, the item for toggling the strong mark might be active then
|
||||
// the cursor is in strong text).
|
||||
//
|
||||
// render:: ?(EditorView) → dom.Node
|
||||
// A function that renders the item. You must provide either this,
|
||||
// [`icon`](#menu.MenuItemSpec.icon), or [`label`](#MenuItemSpec.label).
|
||||
//
|
||||
// icon:: ?Object
|
||||
// Describes an icon to show for this item. The object may specify
|
||||
// an SVG icon, in which case its `path` property should be an [SVG
|
||||
// path
|
||||
// spec](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d),
|
||||
// and `width` and `height` should provide the viewbox in which that
|
||||
// path exists. Alternatively, it may have a `text` property
|
||||
// specifying a string of text that makes up the icon, with an
|
||||
// optional `css` property giving additional CSS styling for the
|
||||
// text. _Or_ it may contain `dom` property containing a DOM node.
|
||||
//
|
||||
// label:: ?string
|
||||
// Makes the item show up as a text label. Mostly useful for items
|
||||
// wrapped in a [drop-down](#menu.Dropdown) or similar menu. The object
|
||||
// should have a `label` property providing the text to display.
|
||||
//
|
||||
// title:: ?union<string, (EditorState) → string>
|
||||
// Defines DOM title (mouseover) text for the item.
|
||||
//
|
||||
// class:: ?string
|
||||
// Optionally adds a CSS class to the item's DOM representation.
|
||||
//
|
||||
// css:: ?string
|
||||
// Optionally adds a string of inline CSS to the item's DOM
|
||||
// representation.
|
||||
|
||||
let lastMenuEvent = {time: 0, node: null}
|
||||
function markMenuEvent(e) {
|
||||
lastMenuEvent.time = Date.now()
|
||||
lastMenuEvent.node = e.target
|
||||
}
|
||||
function isMenuEvent(wrapper) {
|
||||
return Date.now() - 100 < lastMenuEvent.time &&
|
||||
lastMenuEvent.node && wrapper.contains(lastMenuEvent.node)
|
||||
}
|
||||
|
||||
// ::- A drop-down menu, displayed as a label with a downwards-pointing
|
||||
// triangle to the right of it.
|
||||
export class Dropdown {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// Create a dropdown wrapping the elements. Options may include
|
||||
// the following properties:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the drop-down control.
|
||||
//
|
||||
// **`title`**`: string`
|
||||
// : Sets the
|
||||
// [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title)
|
||||
// attribute given to the menu control.
|
||||
//
|
||||
// **`class`**`: string`
|
||||
// : When given, adds an extra CSS class to the menu control.
|
||||
//
|
||||
// **`css`**`: string`
|
||||
// : When given, adds an extra set of CSS styles to the menu control.
|
||||
constructor(content, options) {
|
||||
this.options = options || {}
|
||||
this.content = Array.isArray(content) ? content : [content]
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState)}
|
||||
// Render the dropdown menu and sub-items.
|
||||
render(view) {
|
||||
let content = renderDropdownItems(this.content, view)
|
||||
|
||||
let label = crel("div", {class: prefix + "-dropdown " + (this.options.class || ""),
|
||||
style: this.options.css},
|
||||
translate(view, this.options.label))
|
||||
if (this.options.title) label.setAttribute("title", translate(view, this.options.title))
|
||||
let wrap = crel("div", {class: prefix + "-dropdown-wrap"}, label)
|
||||
let open = null, listeningOnClose = null
|
||||
let close = () => {
|
||||
if (open && open.close()) {
|
||||
open = null
|
||||
window.removeEventListener("mousedown", listeningOnClose)
|
||||
}
|
||||
}
|
||||
label.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
markMenuEvent(e)
|
||||
if (open) {
|
||||
close()
|
||||
} else {
|
||||
open = this.expand(wrap, content.dom)
|
||||
window.addEventListener("mousedown", listeningOnClose = () => {
|
||||
if (!isMenuEvent(wrap)) close()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
let inner = content.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
expand(dom, items) {
|
||||
let menuDOM = crel("div", {class: prefix + "-dropdown-menu " + (this.options.class || "")}, items)
|
||||
|
||||
let done = false
|
||||
function close() {
|
||||
if (done) return
|
||||
done = true
|
||||
dom.removeChild(menuDOM)
|
||||
return true
|
||||
}
|
||||
dom.appendChild(menuDOM)
|
||||
return {close, node: menuDOM}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
export class DropdownSubmenu {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// Creates a submenu for the given group of menu elements. The
|
||||
// following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the submenu.
|
||||
constructor(content, options) {
|
||||
this.options = options || {}
|
||||
this.content = Array.isArray(content) ? content : [content]
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderDropdownItems(this.content, view)
|
||||
|
||||
const handleContent = this.options.icon ? getIcon(this.options.icon) : crel("div", {class: prefix + "-submenu-label"}, translate(view, this.options.label));
|
||||
const wrap = crel("div", {class: prefix + "-submenu-wrap"}, handleContent,
|
||||
crel("div", {class: prefix + "-submenu"}, items.dom))
|
||||
let listeningOnClose = null
|
||||
handleContent.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
markMenuEvent(e)
|
||||
setClass(wrap, prefix + "-submenu-wrap-active")
|
||||
if (!listeningOnClose)
|
||||
window.addEventListener("mousedown", listeningOnClose = () => {
|
||||
if (!isMenuEvent(wrap)) {
|
||||
wrap.classList.remove(prefix + "-submenu-wrap-active")
|
||||
window.removeEventListener("mousedown", listeningOnClose)
|
||||
listeningOnClose = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
let inner = items.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner
|
||||
}
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
}
|
||||
|
||||
// :: (EditorView, [[MenuElement]]) → {dom: dom.DocumentFragment, update: (EditorState) → bool}
|
||||
// Render the given, possibly nested, array of menu elements into a
|
||||
// document fragment, placing separators between them (and ensuring no
|
||||
// superfluous separators appear when some of the groups turn out to
|
||||
// be empty).
|
||||
export function renderGrouped(view, content) {
|
||||
let result = document.createDocumentFragment()
|
||||
let updates = [], separators = []
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let items = content[i], localUpdates = [], localNodes = []
|
||||
for (let j = 0; j < items.length; j++) {
|
||||
let {dom, update} = items[j].render(view)
|
||||
let span = crel("span", {class: prefix + "item"}, dom)
|
||||
result.appendChild(span)
|
||||
localNodes.push(span)
|
||||
localUpdates.push(update)
|
||||
}
|
||||
if (localUpdates.length) {
|
||||
updates.push(combineUpdates(localUpdates, localNodes))
|
||||
if (i < content.length - 1)
|
||||
separators.push(result.appendChild(separator()))
|
||||
}
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
let something = false, needSep = false
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
let hasContent = updates[i](state)
|
||||
if (i) separators[i - 1].style.display = needSep && hasContent ? "" : "none"
|
||||
needSep = hasContent
|
||||
if (hasContent) something = true
|
||||
}
|
||||
return something
|
||||
}
|
||||
return {dom: result, update}
|
||||
}
|
||||
|
||||
function separator() {
|
||||
return crel("span", {class: prefix + "separator"})
|
||||
}
|
||||
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `joinUp` command.
|
||||
export const joinUpItem = new MenuItem({
|
||||
title: "Join with above block",
|
||||
run: joinUp,
|
||||
select: state => joinUp(state),
|
||||
icon: icons.join
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `lift` command.
|
||||
export const liftItem = new MenuItem({
|
||||
title: "Lift out of enclosing block",
|
||||
run: lift,
|
||||
select: state => lift(state),
|
||||
icon: icons.lift
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `selectParentNode` command.
|
||||
export const selectParentNodeItem = new MenuItem({
|
||||
title: "Select parent node",
|
||||
run: selectParentNode,
|
||||
select: state => selectParentNode(state),
|
||||
icon: icons.selectParentNode
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `undo` command.
|
||||
export let undoItem = new MenuItem({
|
||||
title: "Undo last change",
|
||||
run: undo,
|
||||
enable: state => undo(state),
|
||||
icon: icons.undo
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `redo` command.
|
||||
export let redoItem = new MenuItem({
|
||||
title: "Redo last undone change",
|
||||
run: redo,
|
||||
enable: state => redo(state),
|
||||
icon: icons.redo
|
||||
})
|
||||
|
||||
// :: (NodeType, Object) → MenuItem
|
||||
// Build a menu item for wrapping the selection in a given node type.
|
||||
// Adds `run` and `select` properties to the ones present in
|
||||
// `options`. `options.attrs` may be an object or a function.
|
||||
export function wrapItem(nodeType, options) {
|
||||
let passedOptions = {
|
||||
run(state, dispatch) {
|
||||
// FIXME if (options.attrs instanceof Function) options.attrs(state, attrs => wrapIn(nodeType, attrs)(state))
|
||||
return wrapIn(nodeType, options.attrs)(state, dispatch)
|
||||
},
|
||||
select(state) {
|
||||
return wrapIn(nodeType, options.attrs instanceof Function ? null : options.attrs)(state)
|
||||
}
|
||||
}
|
||||
for (let prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
// :: (NodeType, Object) → MenuItem
|
||||
// Build a menu item for changing the type of the textblock around the
|
||||
// selection to the given type. Provides `run`, `active`, and `select`
|
||||
// properties. Others must be given in `options`. `options.attrs` may
|
||||
// be an object to provide the attributes for the textblock node.
|
||||
export function blockTypeItem(nodeType, options) {
|
||||
let command = setBlockType(nodeType, options.attrs)
|
||||
let passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
let {$from, to, node} = state.selection
|
||||
if (node) return node.hasMarkup(nodeType, options.attrs)
|
||||
return to <= $from.end() && $from.parent.hasMarkup(nodeType, options.attrs)
|
||||
}
|
||||
}
|
||||
for (let prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
export function setAttrItem(attrName, attrValue, options) {
|
||||
const command = setBlockAttr(attrName, attrValue);
|
||||
const passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
const {$from, to, node} = state.selection
|
||||
if (node) return node.attrs[attrValue] === attrValue;
|
||||
return to <= $from.end() && $from.parent.attrs[attrValue] === attrValue;
|
||||
}
|
||||
}
|
||||
for (const prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
export function insertBlockBeforeItem(blockType, options) {
|
||||
const command = insertBlockBefore(blockType);
|
||||
const passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions);
|
||||
}
|
||||
|
||||
// Work around classList.toggle being broken in IE11
|
||||
function setClass(dom, cls, on) {
|
||||
if (on) dom.classList.add(cls)
|
||||
else dom.classList.remove(cls)
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import crel from "crelt"
|
||||
import {Plugin} from "prosemirror-state"
|
||||
|
||||
import {renderGrouped} from "./menu"
|
||||
|
||||
const prefix = "ProseMirror-menubar"
|
||||
|
||||
function isIOS() {
|
||||
if (typeof navigator == "undefined") return false
|
||||
let agent = navigator.userAgent
|
||||
return !/Edge\/\d/.test(agent) && /AppleWebKit/.test(agent) && /Mobile\/\w+/.test(agent)
|
||||
}
|
||||
|
||||
// :: (Object) → Plugin
|
||||
// A plugin that will place a menu bar above the editor. Note that
|
||||
// this involves wrapping the editor in an additional `<div>`.
|
||||
//
|
||||
// options::-
|
||||
// Supports the following options:
|
||||
//
|
||||
// content:: [[MenuElement]]
|
||||
// Provides the content of the menu, as a nested array to be
|
||||
// passed to `renderGrouped`.
|
||||
//
|
||||
// floating:: ?bool
|
||||
// Determines whether the menu floats, i.e. whether it sticks to
|
||||
// the top of the viewport when the editor is partially scrolled
|
||||
// out of view.
|
||||
export function menuBar(options) {
|
||||
return new Plugin({
|
||||
view(editorView) { return new MenuBarView(editorView, options) }
|
||||
})
|
||||
}
|
||||
|
||||
class MenuBarView {
|
||||
constructor(editorView, options) {
|
||||
this.editorView = editorView
|
||||
this.options = options
|
||||
|
||||
this.wrapper = crel("div", {class: prefix + "-wrapper"})
|
||||
this.menu = this.wrapper.appendChild(crel("div", {class: prefix}))
|
||||
this.menu.className = prefix
|
||||
this.spacer = null
|
||||
|
||||
if (editorView.dom.parentNode)
|
||||
editorView.dom.parentNode.replaceChild(this.wrapper, editorView.dom)
|
||||
this.wrapper.appendChild(editorView.dom)
|
||||
|
||||
this.maxHeight = 0
|
||||
this.widthForMaxHeight = 0
|
||||
this.floating = false
|
||||
|
||||
let {dom, update} = renderGrouped(this.editorView, this.options.content)
|
||||
this.contentUpdate = update
|
||||
this.menu.appendChild(dom)
|
||||
this.update()
|
||||
|
||||
if (options.floating && !isIOS()) {
|
||||
this.updateFloat()
|
||||
let potentialScrollers = getAllWrapping(this.wrapper)
|
||||
this.scrollFunc = (e) => {
|
||||
let root = this.editorView.root
|
||||
if (!(root.body || root).contains(this.wrapper)) {
|
||||
potentialScrollers.forEach(el => el.removeEventListener("scroll", this.scrollFunc))
|
||||
} else {
|
||||
this.updateFloat(e.target.getBoundingClientRect && e.target)
|
||||
}
|
||||
}
|
||||
potentialScrollers.forEach(el => el.addEventListener('scroll', this.scrollFunc))
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.contentUpdate(this.editorView.state)
|
||||
|
||||
if (this.floating) {
|
||||
this.updateScrollCursor()
|
||||
} else {
|
||||
if (this.menu.offsetWidth != this.widthForMaxHeight) {
|
||||
this.widthForMaxHeight = this.menu.offsetWidth
|
||||
this.maxHeight = 0
|
||||
}
|
||||
if (this.menu.offsetHeight > this.maxHeight) {
|
||||
this.maxHeight = this.menu.offsetHeight
|
||||
this.menu.style.minHeight = this.maxHeight + "px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollCursor() {
|
||||
let selection = this.editorView.root.getSelection()
|
||||
if (!selection.focusNode) return
|
||||
let rects = selection.getRangeAt(0).getClientRects()
|
||||
let selRect = rects[selectionIsInverted(selection) ? 0 : rects.length - 1]
|
||||
if (!selRect) return
|
||||
let menuRect = this.menu.getBoundingClientRect()
|
||||
if (selRect.top < menuRect.bottom && selRect.bottom > menuRect.top) {
|
||||
let scrollable = findWrappingScrollable(this.wrapper)
|
||||
if (scrollable) scrollable.scrollTop -= (menuRect.bottom - selRect.top)
|
||||
}
|
||||
}
|
||||
|
||||
updateFloat(scrollAncestor) {
|
||||
let parent = this.wrapper, editorRect = parent.getBoundingClientRect(),
|
||||
top = scrollAncestor ? Math.max(0, scrollAncestor.getBoundingClientRect().top) : 0
|
||||
|
||||
if (this.floating) {
|
||||
if (editorRect.top >= top || editorRect.bottom < this.menu.offsetHeight + 10) {
|
||||
this.floating = false
|
||||
this.menu.style.position = this.menu.style.left = this.menu.style.top = this.menu.style.width = ""
|
||||
this.menu.style.display = ""
|
||||
this.spacer.parentNode.removeChild(this.spacer)
|
||||
this.spacer = null
|
||||
} else {
|
||||
let border = (parent.offsetWidth - parent.clientWidth) / 2
|
||||
this.menu.style.left = (editorRect.left + border) + "px"
|
||||
this.menu.style.display = (editorRect.top > window.innerHeight ? "none" : "")
|
||||
if (scrollAncestor) this.menu.style.top = top + "px"
|
||||
}
|
||||
} else {
|
||||
if (editorRect.top < top && editorRect.bottom >= this.menu.offsetHeight + 10) {
|
||||
this.floating = true
|
||||
let menuRect = this.menu.getBoundingClientRect()
|
||||
this.menu.style.left = menuRect.left + "px"
|
||||
this.menu.style.width = menuRect.width + "px"
|
||||
if (scrollAncestor) this.menu.style.top = top + "px"
|
||||
this.menu.style.position = "fixed"
|
||||
this.spacer = crel("div", {class: prefix + "-spacer", style: `height: ${menuRect.height}px`})
|
||||
parent.insertBefore(this.spacer, this.menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.wrapper.parentNode)
|
||||
this.wrapper.parentNode.replaceChild(this.editorView.dom, this.wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
// Not precise, but close enough
|
||||
function selectionIsInverted(selection) {
|
||||
if (selection.anchorNode == selection.focusNode) return selection.anchorOffset > selection.focusOffset
|
||||
return selection.anchorNode.compareDocumentPosition(selection.focusNode) == Node.DOCUMENT_POSITION_FOLLOWING
|
||||
}
|
||||
|
||||
function findWrappingScrollable(node) {
|
||||
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||||
if (cur.scrollHeight > cur.clientHeight) return cur
|
||||
}
|
||||
|
||||
function getAllWrapping(node) {
|
||||
let res = [window]
|
||||
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||||
res.push(cur)
|
||||
return res
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
class IframeView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.classList.add('ProseMirror-iframewrap');
|
||||
|
||||
this.iframe = document.createElement("iframe");
|
||||
for (const [key, value] of Object.entries(node.attrs)) {
|
||||
if (value) {
|
||||
this.iframe.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.dom.appendChild(this.iframe);
|
||||
}
|
||||
|
||||
stopEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default IframeView;
|
||||
@@ -1,197 +0,0 @@
|
||||
import {positionHandlesAtCorners, removeHandles, renderHandlesAtCorners} from "./node-view-utils";
|
||||
import {NodeSelection} from "prosemirror-state";
|
||||
|
||||
class ImageView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.classList.add('ProseMirror-imagewrap');
|
||||
|
||||
this.image = document.createElement("img");
|
||||
this.image.src = node.attrs.src;
|
||||
this.image.alt = node.attrs.alt;
|
||||
if (node.attrs.width) {
|
||||
this.image.width = node.attrs.width;
|
||||
}
|
||||
if (node.attrs.height) {
|
||||
this.image.height = node.attrs.height;
|
||||
}
|
||||
|
||||
this.dom.appendChild(this.image);
|
||||
|
||||
this.handles = [];
|
||||
this.handleDragStartInfo = null;
|
||||
this.handleDragMoveDimensions = null;
|
||||
this.removeHandlesListener = this.removeHandlesListener.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
|
||||
this.dom.addEventListener("click", event => {
|
||||
this.showHandles();
|
||||
});
|
||||
|
||||
// Show handles if selected
|
||||
if (view.state.selection.node === node) {
|
||||
window.setTimeout(() => {
|
||||
this.showHandles();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.updateImageDimensions = function (width, height) {
|
||||
const attrs = Object.assign({}, node.attrs, {width, height});
|
||||
let tr = view.state.tr;
|
||||
const position = getPos();
|
||||
tr = tr.setNodeMarkup(position, null, attrs)
|
||||
tr = tr.setSelection(NodeSelection.create(tr.doc, position));
|
||||
view.dispatch(tr);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
showHandles() {
|
||||
if (this.handles.length === 0) {
|
||||
this.image.dataset.showHandles = 'true';
|
||||
window.addEventListener('click', this.removeHandlesListener);
|
||||
this.handles = renderHandlesAtCorners(this.image);
|
||||
for (const handle of this.handles) {
|
||||
handle.addEventListener('mousedown', this.handleMouseDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeHandlesListener(event) {
|
||||
if (!this.dom.contains(event.target)) {
|
||||
this.removeHandles();
|
||||
this.handles = [];
|
||||
}
|
||||
}
|
||||
|
||||
removeHandles() {
|
||||
removeHandles(this.handles);
|
||||
window.removeEventListener('click', this.removeHandlesListener);
|
||||
delete this.image.dataset.showHandles;
|
||||
}
|
||||
|
||||
stopEvent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseDown(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const imageBounds = this.image.getBoundingClientRect();
|
||||
const handle = event.target;
|
||||
this.handleDragStartInfo = {
|
||||
x: event.screenX,
|
||||
y: event.screenY,
|
||||
ratio: imageBounds.width / imageBounds.height,
|
||||
bounds: imageBounds,
|
||||
handleX: handle.dataset.x,
|
||||
handleY: handle.dataset.y,
|
||||
};
|
||||
|
||||
this.createDragDummy(imageBounds);
|
||||
this.dom.appendChild(this.dragDummy);
|
||||
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMRect} bounds
|
||||
*/
|
||||
createDragDummy(bounds) {
|
||||
this.dragDummy = this.image.cloneNode();
|
||||
this.dragDummy.style.opacity = '0.5';
|
||||
this.dragDummy.classList.add('ProseMirror-dragdummy');
|
||||
this.dragDummy.style.width = bounds.width + 'px';
|
||||
this.dragDummy.style.height = bounds.height + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseUp(event) {
|
||||
if (this.handleDragMoveDimensions) {
|
||||
const {width, height} = this.handleDragMoveDimensions;
|
||||
this.updateImageDimensions(String(width), String(height));
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
this.handleDragStartInfo = null;
|
||||
this.handleDragMoveDimensions = null;
|
||||
this.dragDummy.remove();
|
||||
positionHandlesAtCorners(this.image, this.handles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseMove(event) {
|
||||
const originalBounds = this.handleDragStartInfo.bounds;
|
||||
|
||||
// Calculate change in x & y, flip amounts depending on handle
|
||||
let xChange = event.screenX - this.handleDragStartInfo.x;
|
||||
if (this.handleDragStartInfo.handleX === 'left') {
|
||||
xChange = -xChange;
|
||||
}
|
||||
let yChange = event.screenY - this.handleDragStartInfo.y;
|
||||
if (this.handleDragStartInfo.handleY === 'top') {
|
||||
yChange = -yChange;
|
||||
}
|
||||
|
||||
// Prevent images going too small or into negative bounds
|
||||
if (originalBounds.width + xChange < 10) {
|
||||
xChange = -originalBounds.width + 10;
|
||||
}
|
||||
if (originalBounds.height + yChange < 10) {
|
||||
yChange = -originalBounds.height + 10;
|
||||
}
|
||||
|
||||
// Choose the larger dimension change and align the other to keep
|
||||
// image aspect ratio, aligning growth/reduction direction
|
||||
if (Math.abs(xChange) > Math.abs(yChange)) {
|
||||
yChange = Math.floor(xChange * this.handleDragStartInfo.ratio);
|
||||
if (yChange * xChange < 0) {
|
||||
yChange = -yChange;
|
||||
}
|
||||
} else {
|
||||
xChange = Math.floor(yChange / this.handleDragStartInfo.ratio);
|
||||
if (xChange * yChange < 0) {
|
||||
xChange = -xChange;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate our new sizes
|
||||
const newWidth = originalBounds.width + xChange;
|
||||
const newHeight = originalBounds.height + yChange;
|
||||
|
||||
// Apply the sizes and positioning to our ghost dummy
|
||||
this.dragDummy.style.width = `${newWidth}px`;
|
||||
if (this.handleDragStartInfo.handleX === 'left') {
|
||||
this.dragDummy.style.left = `${-xChange}px`;
|
||||
}
|
||||
this.dragDummy.style.height = `${newHeight}px`;
|
||||
if (this.handleDragStartInfo.handleY === 'top') {
|
||||
this.dragDummy.style.top = `${-yChange}px`;
|
||||
}
|
||||
|
||||
// Update corners and track dimension changes for later application
|
||||
positionHandlesAtCorners(this.dragDummy, this.handles);
|
||||
this.handleDragMoveDimensions = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageView;
|
||||
@@ -1,21 +0,0 @@
|
||||
class TableView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement("div")
|
||||
this.dom.className = "ProseMirror-tableWrapper"
|
||||
this.table = this.dom.appendChild(document.createElement("table"));
|
||||
this.table.setAttribute('style', node.attrs.style);
|
||||
this.colgroup = this.table.appendChild(document.createElement("colgroup"));
|
||||
this.contentDOM = this.table.appendChild(document.createElement("tbody"));
|
||||
}
|
||||
|
||||
ignoreMutation(record) {
|
||||
return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target))
|
||||
}
|
||||
}
|
||||
|
||||
export default TableView;
|
||||
@@ -1,11 +0,0 @@
|
||||
import ImageView from "./ImageView";
|
||||
import IframeView from "./IframeView";
|
||||
import TableView from "./TableView";
|
||||
|
||||
const views = {
|
||||
image: (node, view, getPos) => new ImageView(node, view, getPos),
|
||||
iframe: (node, view, getPos) => new IframeView(node, view, getPos),
|
||||
table: (node, view, getPos) => new TableView(node, view, getPos),
|
||||
};
|
||||
|
||||
export default views;
|
||||
@@ -1,58 +0,0 @@
|
||||
import crel from "crelt";
|
||||
|
||||
/**
|
||||
* Render grab handles at the corners of the given element.
|
||||
* @param {Element} elem
|
||||
* @return {Element[]}
|
||||
*/
|
||||
export function renderHandlesAtCorners(elem) {
|
||||
const handles = [];
|
||||
const baseClass = 'ProseMirror-grabhandle';
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const y = (i < 2) ? 'top' : 'bottom';
|
||||
const x = (i === 0 || i === 3) ? 'left' : 'right';
|
||||
const handle = crel('div', {
|
||||
class: `${baseClass} ${baseClass}-${x}-${y}`,
|
||||
});
|
||||
handle.dataset.y = y;
|
||||
handle.dataset.x = x;
|
||||
handles.push(handle);
|
||||
elem.parentNode.appendChild(handle);
|
||||
}
|
||||
|
||||
positionHandlesAtCorners(elem, handles);
|
||||
return handles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element[]} handles
|
||||
*/
|
||||
export function removeHandles(handles) {
|
||||
for (const handle of handles) {
|
||||
handle.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {[Element, Element, Element, Element]}handles
|
||||
*/
|
||||
export function positionHandlesAtCorners(element, handles) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
const parentBounds = element.parentElement.getBoundingClientRect();
|
||||
const positions = [
|
||||
{x: bounds.left - parentBounds.left, y: bounds.top - parentBounds.top},
|
||||
{x: bounds.right - parentBounds.left, y: bounds.top - parentBounds.top},
|
||||
{x: bounds.right - parentBounds.left, y: bounds.bottom - parentBounds.top},
|
||||
{x: bounds.left - parentBounds.left, y: bounds.bottom - parentBounds.top},
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const {x, y} = positions[i];
|
||||
const handle = handles[i];
|
||||
handle.style.left = (x - 6) + 'px';
|
||||
handle.style.top = (y - 6) + 'px';
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-tables
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import {Plugin, PluginKey} from "prosemirror-state"
|
||||
import {Decoration, DecorationSet} from "prosemirror-view"
|
||||
import {
|
||||
cellAround,
|
||||
pointsAtCell,
|
||||
setAttr,
|
||||
TableMap,
|
||||
} from "prosemirror-tables";
|
||||
|
||||
export const key = new PluginKey("tableColumnResizing")
|
||||
|
||||
export function columnResizing(options = {}) {
|
||||
const {
|
||||
handleWidth, cellMinWidth, lastColumnResizable
|
||||
} = Object.assign({
|
||||
handleWidth: 5,
|
||||
cellMinWidth: 25,
|
||||
lastColumnResizable: true
|
||||
}, options);
|
||||
|
||||
let plugin = new Plugin({
|
||||
key,
|
||||
state: {
|
||||
init(_, state) {
|
||||
return new ResizeState(-1, false)
|
||||
},
|
||||
apply(tr, prev) {
|
||||
return prev.apply(tr)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
attributes(state) {
|
||||
let pluginState = key.getState(state)
|
||||
return pluginState.activeHandle > -1 ? {class: "resize-cursor"} : null
|
||||
},
|
||||
|
||||
handleDOMEvents: {
|
||||
mousemove(view, event) {
|
||||
handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable)
|
||||
},
|
||||
mouseleave(view) {
|
||||
handleMouseLeave(view)
|
||||
},
|
||||
mousedown(view, event) {
|
||||
handleMouseDown(view, event, cellMinWidth)
|
||||
}
|
||||
},
|
||||
|
||||
decorations(state) {
|
||||
let pluginState = key.getState(state)
|
||||
if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle)
|
||||
},
|
||||
|
||||
nodeViews: {}
|
||||
}
|
||||
})
|
||||
return plugin
|
||||
}
|
||||
|
||||
class ResizeState {
|
||||
constructor(activeHandle, dragging) {
|
||||
this.activeHandle = activeHandle
|
||||
this.dragging = dragging
|
||||
}
|
||||
|
||||
apply(tr) {
|
||||
let state = this, action = tr.getMeta(key)
|
||||
if (action && action.setHandle != null)
|
||||
return new ResizeState(action.setHandle, null)
|
||||
if (action && action.setDragging !== undefined)
|
||||
return new ResizeState(state.activeHandle, action.setDragging)
|
||||
if (state.activeHandle > -1 && tr.docChanged) {
|
||||
let handle = tr.mapping.map(state.activeHandle, -1)
|
||||
if (!pointsAtCell(tr.doc.resolve(handle))) handle = null
|
||||
state = new ResizeState(handle, state.dragging)
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) {
|
||||
let pluginState = key.getState(view.state)
|
||||
|
||||
if (!pluginState.dragging) {
|
||||
let target = domCellAround(event.target), cell = -1
|
||||
if (target) {
|
||||
let {left, right} = target.getBoundingClientRect()
|
||||
if (event.clientX - left <= handleWidth)
|
||||
cell = edgeCell(view, event, "left")
|
||||
else if (right - event.clientX <= handleWidth)
|
||||
cell = edgeCell(view, event, "right")
|
||||
}
|
||||
|
||||
if (cell != pluginState.activeHandle) {
|
||||
if (!lastColumnResizable && cell !== -1) {
|
||||
let $cell = view.state.doc.resolve(cell)
|
||||
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||
|
||||
if (col == map.width - 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updateHandle(view, cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave(view) {
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1)
|
||||
}
|
||||
|
||||
function handleMouseDown(view, event, cellMinWidth) {
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.activeHandle == -1 || pluginState.dragging) return false
|
||||
|
||||
let cell = view.state.doc.nodeAt(pluginState.activeHandle)
|
||||
let width = currentColWidth(view, pluginState.activeHandle, cell.attrs)
|
||||
view.dispatch(view.state.tr.setMeta(key, {setDragging: {startX: event.clientX, startWidth: width}}))
|
||||
|
||||
function finish(event) {
|
||||
window.removeEventListener("mouseup", finish)
|
||||
window.removeEventListener("mousemove", move)
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.dragging) {
|
||||
updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth))
|
||||
view.dispatch(view.state.tr.setMeta(key, {setDragging: null}))
|
||||
}
|
||||
}
|
||||
|
||||
function move(event) {
|
||||
if (!event.which) return finish(event)
|
||||
let pluginState = key.getState(view.state)
|
||||
let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth)
|
||||
displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth)
|
||||
}
|
||||
|
||||
window.addEventListener("mouseup", finish)
|
||||
window.addEventListener("mousemove", move)
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
function currentColWidth(view, cellPos, {colspan, colwidth}) {
|
||||
let width = colwidth && colwidth[colwidth.length - 1]
|
||||
if (width) return width
|
||||
let dom = view.domAtPos(cellPos)
|
||||
let node = dom.node.childNodes[dom.offset]
|
||||
let domWidth = node.offsetWidth, parts = colspan
|
||||
if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) {
|
||||
domWidth -= colwidth[i]
|
||||
parts--
|
||||
}
|
||||
return domWidth / parts
|
||||
}
|
||||
|
||||
function domCellAround(target) {
|
||||
while (target && target.nodeName != "TD" && target.nodeName != "TH")
|
||||
target = target.classList.contains("ProseMirror") ? null : target.parentNode
|
||||
return target
|
||||
}
|
||||
|
||||
function edgeCell(view, event, side) {
|
||||
let found = view.posAtCoords({left: event.clientX, top: event.clientY})
|
||||
if (!found) return -1
|
||||
let {pos} = found
|
||||
let $cell = cellAround(view.state.doc.resolve(pos))
|
||||
if (!$cell) return -1
|
||||
if (side == "right") return $cell.pos
|
||||
let map = TableMap.get($cell.node(-1)), start = $cell.start(-1)
|
||||
let index = map.map.indexOf($cell.pos - start)
|
||||
return index % map.width == 0 ? -1 : start + map.map[index - 1]
|
||||
}
|
||||
|
||||
function draggedWidth(dragging, event, cellMinWidth) {
|
||||
let offset = event.clientX - dragging.startX
|
||||
return Math.max(cellMinWidth, dragging.startWidth + offset)
|
||||
}
|
||||
|
||||
function updateHandle(view, value) {
|
||||
view.dispatch(view.state.tr.setMeta(key, {setHandle: value}))
|
||||
}
|
||||
|
||||
function updateColumnWidth(view, cell, width) {
|
||||
let $cell = view.state.doc.resolve(cell);
|
||||
let table = $cell.node(-1);
|
||||
let map = TableMap.get(table);
|
||||
let start = $cell.start(-1);
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
|
||||
let tr = view.state.tr;
|
||||
|
||||
for (let row = 0; row < map.height; row++) {
|
||||
let mapIndex = row * map.width + col;
|
||||
// Rowspanning cell that has already been handled
|
||||
if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue
|
||||
let pos = map.map[mapIndex]
|
||||
let {attrs} = table.nodeAt(pos);
|
||||
const newWidth = (attrs.colspan * width) + 'px';
|
||||
|
||||
tr.setNodeMarkup(start + pos, null, setAttr(attrs, "width", newWidth));
|
||||
}
|
||||
|
||||
if (tr.docChanged) view.dispatch(tr)
|
||||
}
|
||||
|
||||
function displayColumnWidth(view, cell, width, cellMinWidth) {
|
||||
const $cell = view.state.doc.resolve(cell)
|
||||
const table = $cell.node(-1);
|
||||
const start = $cell.start(-1);
|
||||
const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||
let dom = view.domAtPos($cell.start(-1)).node
|
||||
while (dom.nodeName !== "TABLE") {
|
||||
dom = dom.parentNode
|
||||
}
|
||||
updateColumnsOnResize(view, table, dom, cellMinWidth, col, width)
|
||||
}
|
||||
|
||||
|
||||
function updateColumnsOnResize(view, tableNode, tableDom, cellMinWidth, overrideCol, overrideValue) {
|
||||
console.log({tableNode, tableDom, cellMinWidth, overrideCol, overrideValue});
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
const rows = tableDom.querySelectorAll('tr');
|
||||
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cell = row.children[overrideCol];
|
||||
cell.style.width = `${overrideValue}px`;
|
||||
if (y === 0) {
|
||||
for (let x = 0; x < row.children.length; x++) {
|
||||
const cell = row.children[x];
|
||||
if (cell.style.width) {
|
||||
const width = Number(cell.style.width.replace('px', ''));
|
||||
totalWidth += width || cellMinWidth;
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
totalWidth += cellMinWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(totalWidth);
|
||||
if (fixedWidth) {
|
||||
tableDom.style.width = totalWidth + "px"
|
||||
tableDom.style.minWidth = ""
|
||||
} else {
|
||||
tableDom.style.width = ""
|
||||
tableDom.style.minWidth = totalWidth + "px"
|
||||
}
|
||||
}
|
||||
|
||||
function zeroes(n) {
|
||||
let result = []
|
||||
for (let i = 0; i < n; i++) result.push(0)
|
||||
return result
|
||||
}
|
||||
|
||||
function handleDecorations(state, cell) {
|
||||
let decorations = []
|
||||
let $cell = state.doc.resolve(cell)
|
||||
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan
|
||||
for (let row = 0; row < map.height; row++) {
|
||||
let index = col + row * map.width - 1
|
||||
// For positions that are have either a different cell or the end
|
||||
// of the table to their right, and either the top of the table or
|
||||
// a different cell above them, add a decoration
|
||||
if ((col == map.width || map.map[index] != map.map[index + 1]) &&
|
||||
(row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) {
|
||||
let cellPos = map.map[index]
|
||||
let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1
|
||||
let dom = document.createElement("div")
|
||||
dom.className = "column-resize-handle"
|
||||
decorations.push(Decoration.widget(pos, dom))
|
||||
}
|
||||
}
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
const link = {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: {default: null},
|
||||
target: {default: null}
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{
|
||||
tag: "a[href]", getAttrs: function getAttrs(dom) {
|
||||
return {
|
||||
href: dom.getAttribute("href"),
|
||||
title: dom.getAttribute("title"),
|
||||
target: dom.getAttribute("target"),
|
||||
}
|
||||
}
|
||||
}],
|
||||
toDOM: function toDOM(node) {
|
||||
const ref = node.attrs;
|
||||
const href = ref.href;
|
||||
const title = ref.title;
|
||||
const target = ref.target;
|
||||
return ["a", {href, title, target}, 0]
|
||||
}
|
||||
};
|
||||
|
||||
const em = {
|
||||
parseDOM: [{tag: "i"}, {tag: "em"}, {style: "font-style=italic"}],
|
||||
toDOM() {
|
||||
return ["em", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const strong = {
|
||||
parseDOM: [{tag: "strong"},
|
||||
// This works around a Google Docs misbehavior where
|
||||
// pasted content will be inexplicably wrapped in `<b>`
|
||||
// tags with a font-weight normal.
|
||||
{
|
||||
tag: "b", getAttrs: function (node) {
|
||||
return node.style.fontWeight != "normal" && null;
|
||||
}
|
||||
},
|
||||
{
|
||||
style: "font-weight", getAttrs: function (value) {
|
||||
return /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null;
|
||||
}
|
||||
}],
|
||||
toDOM() {
|
||||
return ["strong", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const code = {
|
||||
parseDOM: [{tag: "code"}],
|
||||
toDOM() {
|
||||
return ["code", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const underline = {
|
||||
parseDOM: [{tag: "u"}, {style: "text-decoration=underline"}],
|
||||
toDOM() {
|
||||
return ["span", {style: "text-decoration: underline;"}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const strike = {
|
||||
parseDOM: [{tag: "s"}, {tag: "strike"}, {style: "text-decoration=line-through"}],
|
||||
toDOM() {
|
||||
return ["span", {style: "text-decoration: line-through;"}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const superscript = {
|
||||
parseDOM: [{tag: "sup"}],
|
||||
toDOM() {
|
||||
return ["sup", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const subscript = {
|
||||
parseDOM: [{tag: "sub"}],
|
||||
toDOM() {
|
||||
return ["sub", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const text_color = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
parseDOM: [{
|
||||
style: 'color',
|
||||
getAttrs(color) {
|
||||
return {color}
|
||||
}
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ['span', {style: `color: ${node.attrs.color};`}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const background_color = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
parseDOM: [{
|
||||
style: 'background-color',
|
||||
getAttrs(color) {
|
||||
return {color}
|
||||
}
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ['span', {style: `background-color: ${node.attrs.color};`}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const marks = {
|
||||
link,
|
||||
em,
|
||||
strong,
|
||||
code,
|
||||
underline,
|
||||
strike,
|
||||
superscript,
|
||||
subscript,
|
||||
text_color,
|
||||
background_color,
|
||||
};
|
||||
|
||||
export default marks;
|
||||
@@ -1,383 +0,0 @@
|
||||
import {orderedList, bulletList, listItem} from "prosemirror-schema-list";
|
||||
import {Fragment} from "prosemirror-model";
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @return {string|null}
|
||||
*/
|
||||
function getAlignAttrFromDomNode(node) {
|
||||
const classList = node.classList;
|
||||
const styles = node.style || {};
|
||||
const alignments = ['right', 'left', 'center', 'justify'];
|
||||
for (const alignment of alignments) {
|
||||
if (classList.contains('align-' + alignment) || styles.textAlign === alignment) {
|
||||
return alignment;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node
|
||||
* @param {Object} attrs
|
||||
* @return {Object}
|
||||
*/
|
||||
function addAlignmentAttr(node, attrs) {
|
||||
const positions = ['right', 'left', 'center', 'justify'];
|
||||
for (const position of positions) {
|
||||
if (node.attrs.align === position) {
|
||||
return addClassToAttrs('align-' + position, attrs);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function getAttrsParserForAlignment(node) {
|
||||
return {
|
||||
align: getAlignAttrFromDomNode(node),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} className
|
||||
* @param {Object} attrs
|
||||
* @return {Object}
|
||||
*/
|
||||
function addClassToAttrs(className, attrs) {
|
||||
return Object.assign({}, attrs, {
|
||||
class: attrs.class ? attrs.class + ' ' + className : className,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String[]} attrNames
|
||||
* @return {function(Element): {}}
|
||||
*/
|
||||
function domAttrsToAttrsParser(attrNames) {
|
||||
return function (node) {
|
||||
const attrs = {};
|
||||
for (const attr of attrNames) {
|
||||
attrs[attr] = node.hasAttribute(attr) ? node.getAttribute(attr) : null;
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {String[]} attrNames
|
||||
*/
|
||||
function extractAttrsForDom(node, attrNames) {
|
||||
const domAttrs = {};
|
||||
for (const attr of attrNames) {
|
||||
if (node.attrs[attr]) {
|
||||
domAttrs[attr] = node.attrs[attr];
|
||||
}
|
||||
}
|
||||
return domAttrs;
|
||||
}
|
||||
|
||||
const doc = {
|
||||
content: "block+",
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "p",
|
||||
getAttrs: getAttrsParserForAlignment,
|
||||
}
|
||||
],
|
||||
attrs: {
|
||||
align: {
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
toDOM(node) {
|
||||
return ["p", addAlignmentAttr(node, {}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const blockquote = {
|
||||
content: "block+",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [{tag: "blockquote", getAttrs: getAttrsParserForAlignment}],
|
||||
attrs: {
|
||||
align: {
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
toDOM(node) {
|
||||
return ["blockquote", addAlignmentAttr(node, {}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const horizontal_rule = {
|
||||
group: "block",
|
||||
parseDOM: [{tag: "hr"}],
|
||||
toDOM() {
|
||||
return ["hr"];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const headingParseGetAttrs = (level) => {
|
||||
return function (node) {
|
||||
return {level, align: getAlignAttrFromDomNode(node)};
|
||||
};
|
||||
};
|
||||
const heading = {
|
||||
attrs: {level: {default: 1}, align: {default: null}},
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [
|
||||
{tag: "h1", getAttrs: headingParseGetAttrs(1)},
|
||||
{tag: "h2", getAttrs: headingParseGetAttrs(2)},
|
||||
{tag: "h3", getAttrs: headingParseGetAttrs(3)},
|
||||
{tag: "h4", getAttrs: headingParseGetAttrs(4)},
|
||||
{tag: "h5", getAttrs: headingParseGetAttrs(5)},
|
||||
{tag: "h6", getAttrs: headingParseGetAttrs(6)},
|
||||
],
|
||||
toDOM(node) {
|
||||
return ["h" + node.attrs.level, addAlignmentAttr(node, {}), 0]
|
||||
}
|
||||
};
|
||||
|
||||
const code_block = {
|
||||
content: "text*",
|
||||
marks: "",
|
||||
group: "block",
|
||||
code: true,
|
||||
defining: true,
|
||||
parseDOM: [{tag: "pre", preserveWhitespace: "full"}],
|
||||
toDOM() {
|
||||
return ["pre", ["code", 0]];
|
||||
}
|
||||
};
|
||||
|
||||
const text = {
|
||||
group: "inline"
|
||||
};
|
||||
|
||||
const image = {
|
||||
inline: true,
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {default: null},
|
||||
title: {default: null},
|
||||
height: {default: null},
|
||||
width: {default: null},
|
||||
},
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
parseDOM: [{
|
||||
tag: "img[src]", getAttrs: function getAttrs(dom) {
|
||||
return {
|
||||
src: dom.getAttribute("src"),
|
||||
title: dom.getAttribute("title"),
|
||||
alt: dom.getAttribute("alt"),
|
||||
height: dom.getAttribute("height"),
|
||||
width: dom.getAttribute("width"),
|
||||
}
|
||||
}
|
||||
}],
|
||||
toDOM: function toDOM(node) {
|
||||
const ref = node.attrs;
|
||||
const src = ref.src;
|
||||
const alt = ref.alt;
|
||||
const title = ref.title;
|
||||
const width = ref.width;
|
||||
const height = ref.height;
|
||||
return ["img", {src, alt, title, width, height}]
|
||||
}
|
||||
};
|
||||
|
||||
const iframe = {
|
||||
attrs: {
|
||||
src: {},
|
||||
height: {default: null},
|
||||
width: {default: null},
|
||||
title: {default: null},
|
||||
allow: {default: null},
|
||||
sandbox: {default: null},
|
||||
},
|
||||
group: "block",
|
||||
draggable: true,
|
||||
parseDOM: [{
|
||||
tag: "iframe",
|
||||
getAttrs: domAttrsToAttrsParser(["src", "width", "height", "title", "allow", "sandbox"]),
|
||||
}],
|
||||
toDOM(node) {
|
||||
const attrs = extractAttrsForDom(node, ["src", "width", "height", "title", "allow", "sandbox"])
|
||||
return ["iframe", attrs];
|
||||
}
|
||||
};
|
||||
|
||||
const hard_break = {
|
||||
inline: true,
|
||||
group: "inline",
|
||||
selectable: false,
|
||||
parseDOM: [{tag: "br"}],
|
||||
toDOM() {
|
||||
return ["br"];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const calloutParseGetAttrs = (type) => {
|
||||
return function (node) {
|
||||
return {type, align: getAlignAttrFromDomNode(node)};
|
||||
};
|
||||
};
|
||||
const callout = {
|
||||
attrs: {
|
||||
type: {default: 'info'},
|
||||
align: {default: null},
|
||||
},
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [
|
||||
{tag: 'p.callout.info', getAttrs: calloutParseGetAttrs('info'), priority: 75},
|
||||
{tag: 'p.callout.success', getAttrs: calloutParseGetAttrs('success'), priority: 75},
|
||||
{tag: 'p.callout.danger', getAttrs: calloutParseGetAttrs('danger'), priority: 75},
|
||||
{tag: 'p.callout.warning', getAttrs: calloutParseGetAttrs('warning'), priority: 75},
|
||||
{tag: 'p.callout', getAttrs: calloutParseGetAttrs('info'), priority: 75},
|
||||
],
|
||||
toDOM(node) {
|
||||
const type = node.attrs.type || 'info';
|
||||
return ['p', addAlignmentAttr(node, {class: 'callout ' + type}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const ordered_list = Object.assign({}, orderedList, {content: "list_item+", group: "block"});
|
||||
const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"});
|
||||
const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
|
||||
|
||||
const table = {
|
||||
content: "table_row+",
|
||||
attrs: {
|
||||
style: {default: null},
|
||||
},
|
||||
tableRole: "table",
|
||||
isolating: true,
|
||||
group: "block",
|
||||
parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
|
||||
toDOM(node) {
|
||||
return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
|
||||
}
|
||||
};
|
||||
|
||||
const table_row = {
|
||||
content: "(table_cell | table_header)*",
|
||||
tableRole: "row",
|
||||
parseDOM: [{tag: "tr"}],
|
||||
toDOM() { return ["tr", 0] }
|
||||
};
|
||||
|
||||
let cellAttrs = {
|
||||
colspan: {default: 1},
|
||||
rowspan: {default: 1},
|
||||
width: {default: null},
|
||||
height: {default: null},
|
||||
};
|
||||
|
||||
function getCellAttrs(dom) {
|
||||
return {
|
||||
colspan: Number(dom.getAttribute("colspan") || 1),
|
||||
rowspan: Number(dom.getAttribute("rowspan") || 1),
|
||||
width: dom.style.width || null,
|
||||
height: dom.style.height || null,
|
||||
};
|
||||
}
|
||||
|
||||
function setCellAttrs(node) {
|
||||
let attrs = {};
|
||||
|
||||
const styles = [];
|
||||
if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan;
|
||||
if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan;
|
||||
if (node.attrs.width) styles.push(`width: ${node.attrs.width}`);
|
||||
if (node.attrs.height) styles.push(`height: ${node.attrs.height}`);
|
||||
if (styles) {
|
||||
attrs.style = styles.join(';');
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
const table_cell = {
|
||||
content: "block+",
|
||||
attrs: cellAttrs,
|
||||
tableRole: "cell",
|
||||
isolating: true,
|
||||
parseDOM: [{tag: "td", getAttrs: dom => getCellAttrs(dom)}],
|
||||
toDOM(node) { return ["td", setCellAttrs(node), 0] }
|
||||
};
|
||||
|
||||
const table_header = {
|
||||
content: "block+",
|
||||
attrs: cellAttrs,
|
||||
tableRole: "header_cell",
|
||||
isolating: true,
|
||||
parseDOM: [{tag: "th", getAttrs: dom => getCellAttrs(dom)}],
|
||||
toDOM(node) { return ["th", setCellAttrs(node), 0] }
|
||||
};
|
||||
|
||||
|
||||
const details = {
|
||||
content: "details_summary block*",
|
||||
isolating: true,
|
||||
group: "block",
|
||||
parseDOM: [{
|
||||
tag: "details",
|
||||
getAttrs(domNode) {
|
||||
return {}
|
||||
},
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ["details", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const details_summary = {
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
parseDOM: [{
|
||||
tag: "details summary",
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ["summary", 0];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const nodes = {
|
||||
doc,
|
||||
paragraph,
|
||||
blockquote,
|
||||
horizontal_rule,
|
||||
heading,
|
||||
code_block,
|
||||
text,
|
||||
image,
|
||||
iframe,
|
||||
hard_break,
|
||||
callout,
|
||||
ordered_list,
|
||||
bullet_list,
|
||||
list_item,
|
||||
table,
|
||||
table_row,
|
||||
table_cell,
|
||||
table_header,
|
||||
details,
|
||||
details_summary,
|
||||
};
|
||||
|
||||
export default nodes;
|
||||
@@ -1,12 +0,0 @@
|
||||
import {Schema} from "prosemirror-model";
|
||||
|
||||
import nodes from "./schema-nodes";
|
||||
import marks from "./schema-marks";
|
||||
|
||||
/** @var {PmSchema} schema */
|
||||
const schema = new Schema({
|
||||
nodes,
|
||||
marks,
|
||||
});
|
||||
|
||||
export default schema;
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* @typedef {Object} PmEditorState
|
||||
* @property {PmNode} doc
|
||||
* @property {PmSelection} selection
|
||||
* @property {PmMark[]|null} storedMarks
|
||||
* @property {PmSchema} schema
|
||||
* @property {PmTransaction} tr
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmNode
|
||||
* @property {PmNodeType} type
|
||||
* @property {Object} attrs
|
||||
* @property {PmFragment} content
|
||||
* @property {PmMark[]} marks
|
||||
* @property {String|null} text
|
||||
* @property {Number} nodeSize
|
||||
* @property {Number} childCount
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmNodeType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMark
|
||||
* @property {PmMarkType} type
|
||||
* @property {Object} attrs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMarkType
|
||||
* @property {String} name
|
||||
* @property {PmSchema} schema
|
||||
* @property {PmMarkSpec} spec
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMarkSpec
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSchema
|
||||
* @property {PmSchema} schema
|
||||
* @property {Object<PmNodeType>} nodes
|
||||
* @property {Object<PmMarkType>} marks
|
||||
* @property {PmNodeType} topNodeType
|
||||
* @property {Object} cached
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSelection
|
||||
* @property {PmSelectionRange[]} ranges
|
||||
* @property {PmResolvedPos} $anchor
|
||||
* @property {PmResolvedPos} $head
|
||||
* @property {Number} anchor
|
||||
* @property {Number} head
|
||||
* @property {Number} from
|
||||
* @property {Number} to
|
||||
* @property {PmResolvedPos} $from
|
||||
* @property {PmResolvedPos} $to
|
||||
* @property {Boolean} empty
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmResolvedPos
|
||||
* @property {Number} pos
|
||||
* @property {Number} depth
|
||||
* @property {Number} parentOffset
|
||||
* @property {PmNode} parent
|
||||
* @property {PmNode} doc
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSelectionRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmTransaction
|
||||
* @property {Number} time
|
||||
* @property {PmMark[]|null} storedMarks
|
||||
* @property {PmSelection} selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmFragment
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Function} PmCommandHandler
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Function} PmDispatchFunction
|
||||
* @param {PmTransaction} tr
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmView
|
||||
* @param {PmEditorState} state
|
||||
* @param {Element} dom
|
||||
* @param {Boolean} editable
|
||||
* @param {Boolean} composing
|
||||
*/
|
||||
@@ -1,131 +0,0 @@
|
||||
import schema from "./schema";
|
||||
import {DOMParser, DOMSerializer} from "prosemirror-model";
|
||||
|
||||
/**
|
||||
* @param {String} html
|
||||
* @return {PmNode}
|
||||
*/
|
||||
export function htmlToDoc(html) {
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.innerHTML = html;
|
||||
return DOMParser.fromSchema(schema).parse(renderDoc.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNode} doc
|
||||
* @return {string}
|
||||
*/
|
||||
export function docToHtml(doc) {
|
||||
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.appendChild(fragment);
|
||||
return renderDoc.body.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @return {String}
|
||||
*/
|
||||
export function stateToHtml(state) {
|
||||
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content);
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.appendChild(fragment);
|
||||
return renderDoc.body.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} object
|
||||
* @return {{}}
|
||||
*/
|
||||
export function nullifyEmptyValues(object) {
|
||||
const clean = {};
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
clean[key] = (value === "") ? null : value;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmSelection} selection
|
||||
* @param {PmMarkType} markType
|
||||
* @return {{from: Number, to: Number}}
|
||||
*/
|
||||
export function expandSelectionToMark(state, selection, markType) {
|
||||
let {from, to} = selection;
|
||||
const noRange = (from === to);
|
||||
if (noRange) {
|
||||
const markRange = markRangeAtPosition(state, markType, from);
|
||||
if (markRange.from !== -1) {
|
||||
from = markRange.from;
|
||||
to = markRange.to;
|
||||
}
|
||||
}
|
||||
return {from, to};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmMarkType} markType
|
||||
* @param {Number} pos
|
||||
* @return {{from: Number, to: Number}}
|
||||
*/
|
||||
export function markRangeAtPosition(state, markType, pos) {
|
||||
const $pos = state.doc.resolve(pos);
|
||||
|
||||
const {parent, parentOffset} = $pos;
|
||||
const start = parent.childAfter(parentOffset);
|
||||
if (!start.node) return {from: -1, to: -1};
|
||||
|
||||
const mark = start.node.marks.find((mark) => mark.type === markType);
|
||||
if (!mark) return {from: -1, to: -1};
|
||||
|
||||
let startIndex = $pos.index();
|
||||
let startPos = $pos.start() + start.offset;
|
||||
let endIndex = startIndex + 1;
|
||||
let endPos = startPos + start.node.nodeSize;
|
||||
while (startIndex > 0 && mark.isInSet(parent.child(startIndex - 1).marks)) {
|
||||
startIndex -= 1;
|
||||
startPos -= parent.child(startIndex).nodeSize;
|
||||
}
|
||||
while (endIndex < parent.childCount && mark.isInSet(parent.child(endIndex).marks)) {
|
||||
endPos += parent.child(endIndex).nodeSize;
|
||||
endIndex += 1;
|
||||
}
|
||||
return {from: startPos, to: endPos};
|
||||
}
|
||||
|
||||
/**
|
||||
* @class KeyedMultiStack
|
||||
* Holds many stacks, seperated via a key, with a simple
|
||||
* interface to pop and push values to the stacks.
|
||||
*/
|
||||
export class KeyedMultiStack {
|
||||
|
||||
constructor() {
|
||||
this.stack = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @return {undefined|*}
|
||||
*/
|
||||
pop(key) {
|
||||
if (Array.isArray(this.stack[key])) {
|
||||
return this.stack[key].pop();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
*/
|
||||
push(key, value) {
|
||||
if (this.stack[key] === undefined) {
|
||||
this.stack[key] = [];
|
||||
}
|
||||
|
||||
this.stack[key].push(value);
|
||||
}
|
||||
}
|
||||
@@ -211,9 +211,9 @@ function wysiwygView(elem) {
|
||||
const doc = elem.ownerDocument;
|
||||
const codeElem = elem.querySelector('code');
|
||||
|
||||
let lang = getLanguageFromCssClasses(elem.className || '');
|
||||
if (!lang && codeElem) {
|
||||
lang = getLanguageFromCssClasses(codeElem.className || '');
|
||||
let lang = (elem.className || '').replace('language-', '');
|
||||
if (lang === '' && codeElem) {
|
||||
lang = (codeElem.className || '').replace('language-', '')
|
||||
}
|
||||
|
||||
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
|
||||
@@ -228,7 +228,7 @@ function wysiwygView(elem) {
|
||||
elem.parentNode.replaceChild(newWrap, elem);
|
||||
|
||||
newWrap.appendChild(newTextArea);
|
||||
newWrap.contentEditable = 'false';
|
||||
newWrap.contentEditable = false;
|
||||
newTextArea.textContent = content;
|
||||
|
||||
let cm = CodeMirror(function(elt) {
|
||||
@@ -245,16 +245,6 @@ function wysiwygView(elem) {
|
||||
return {wrap: newWrap, editor: cm};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the code language from the given css classes.
|
||||
* @param {String} classes
|
||||
* @return {String}
|
||||
*/
|
||||
function getLanguageFromCssClasses(classes) {
|
||||
const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
|
||||
return (langClasses[0] || '').replace('language-', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CodeMirror instance to show in the WYSIWYG pop-up editor
|
||||
* @param {HTMLElement} elem
|
||||
|
||||
@@ -7,41 +7,41 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'تم إنشاء صفحة',
|
||||
'page_create_notification' => 'Page successfully created',
|
||||
'page_create_notification' => 'تم إنشاء الصفحة بنجاح',
|
||||
'page_update' => 'تم تحديث الصفحة',
|
||||
'page_update_notification' => 'Page successfully updated',
|
||||
'page_update_notification' => 'تم تحديث الصفحة بنجاح',
|
||||
'page_delete' => 'تم حذف الصفحة',
|
||||
'page_delete_notification' => 'Page successfully deleted',
|
||||
'page_delete_notification' => 'تم حذف الصفحة بنجاح',
|
||||
'page_restore' => 'تمت استعادة الصفحة',
|
||||
'page_restore_notification' => 'Page successfully restored',
|
||||
'page_restore_notification' => 'تمت استعادة الصفحة بنجاح',
|
||||
'page_move' => 'تم نقل الصفحة',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'تم إنشاء فصل',
|
||||
'chapter_create_notification' => 'Chapter successfully created',
|
||||
'chapter_create_notification' => 'تم إنشاء فصل بنجاح',
|
||||
'chapter_update' => 'تم تحديث الفصل',
|
||||
'chapter_update_notification' => 'Chapter successfully updated',
|
||||
'chapter_update_notification' => 'تم تحديث الفصل بنجاح',
|
||||
'chapter_delete' => 'تم حذف الفصل',
|
||||
'chapter_delete_notification' => 'Chapter successfully deleted',
|
||||
'chapter_delete_notification' => 'تم حذف الفصل بنجاح',
|
||||
'chapter_move' => 'تم نقل الفصل',
|
||||
|
||||
// Books
|
||||
'book_create' => 'تم إنشاء كتاب',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_notification' => 'تم إنشاء كتاب بنجاح',
|
||||
'book_update' => 'تم تحديث الكتاب',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_update_notification' => 'تم تحديث الكتاب بنجاح',
|
||||
'book_delete' => 'تم حذف الكتاب',
|
||||
'book_delete_notification' => 'Book successfully deleted',
|
||||
'book_delete_notification' => 'تم حذف الكتاب بنجاح',
|
||||
'book_sort' => 'تم سرد الكتاب',
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
'book_sort_notification' => 'أُعِيدَ سرد الكتاب بنجاح',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create' => 'تم إنشاء رف الكتب',
|
||||
'bookshelf_create_notification' => 'تم إنشاء الرف بنجاح',
|
||||
'bookshelf_update' => 'تم تحديث الرف',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_update_notification' => 'تم تحديث الرف بنجاح',
|
||||
'bookshelf_delete' => 'تم تحديث الرف',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'created webhook',
|
||||
'webhook_create_notification' => 'Webhook successfully created',
|
||||
'webhook_update' => 'updated webhook',
|
||||
'webhook_update_notification' => 'Webhook successfully updated',
|
||||
'webhook_delete' => 'deleted webhook',
|
||||
'webhook_delete_notification' => 'Webhook successfully deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'تم التعليق',
|
||||
'permissions_update' => 'تحديث الأذونات',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'البريد الإلكتروني',
|
||||
'password' => 'كلمة المرور',
|
||||
'password_confirm' => 'تأكيد كلمة المرور',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'password_hint' => 'يجب أن تكون أكثر من 7 حروف',
|
||||
'forgot_password' => 'نسيت كلمة المرور؟',
|
||||
'remember_me' => 'تذكرني',
|
||||
'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
|
||||
@@ -79,7 +79,7 @@ return [
|
||||
'mfa_setup_configured' => 'Already configured',
|
||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
||||
'mfa_setup_action' => 'إعداد (تنصيب)',
|
||||
'mfa_setup_action' => 'Setup',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||
'mfa_option_totp_title' => 'Mobile App',
|
||||
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'عرض منسدل',
|
||||
'default' => 'افتراضي',
|
||||
'breadcrumb' => 'شريط التنقل',
|
||||
'status' => 'Status',
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'عرض القائمة',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'الفصول الأخيرة',
|
||||
'books_sort_show_other' => 'عرض كتب أخرى',
|
||||
'books_sort_save' => 'حفظ الترتيب الجديد',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'فصل',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'نقل الفصل',
|
||||
'chapters_move_named' => 'نقل فصل :chapterName',
|
||||
'chapter_move_success' => 'تم نقل الفصل إلى :bookName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_permissions' => 'أذونات الفصل',
|
||||
'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً',
|
||||
'chapters_permissions_active' => 'أذونات الفصل مفعلة',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذه المراجعة؟ سيتم استبدال محتوى الصفحة الحالية.',
|
||||
'revision_delete_success' => 'تم حذف المراجعة',
|
||||
'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
];
|
||||
|
||||
@@ -174,7 +174,7 @@ return [
|
||||
'users_role' => 'أدوار المستخدمين',
|
||||
'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.',
|
||||
'users_password' => 'كلمة مرور المستخدم',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'قم بتعيين كلمة مرور مستخدمة لتسجيل الدخول إلى التطبيق. يجب ألا يقل طول هذه الكلمة عن 6 أحرف.',
|
||||
'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة المرور الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.',
|
||||
'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم',
|
||||
'users_external_auth_id' => 'ربط الحساب بمواقع التواصل',
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف رمز API؟',
|
||||
'user_api_token_delete_success' => 'تم حذف رمز الـ API بنجاح',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooks',
|
||||
'webhooks_create' => 'Create New Webhook',
|
||||
'webhooks_none_created' => 'No webhooks have yet been created.',
|
||||
'webhooks_edit' => 'Edit Webhook',
|
||||
'webhooks_save' => 'Save Webhook',
|
||||
'webhooks_details' => 'Webhook Details',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Webhook Events',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'All system events',
|
||||
'webhooks_name' => 'Webhook Name',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook Active',
|
||||
'webhook_events_table_header' => 'Events',
|
||||
'webhooks_delete' => 'Delete Webhook',
|
||||
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
|
||||
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
|
||||
'webhooks_format_example' => 'Webhook Format Example',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
@@ -7,41 +7,41 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'създадена страница',
|
||||
'page_create_notification' => 'Page successfully created',
|
||||
'page_create_notification' => 'Страницата беше успешно създадена',
|
||||
'page_update' => 'обновена страница',
|
||||
'page_update_notification' => 'Page successfully updated',
|
||||
'page_update_notification' => 'Страницата успешно обновена',
|
||||
'page_delete' => 'изтрита страница',
|
||||
'page_delete_notification' => 'Page successfully deleted',
|
||||
'page_delete_notification' => 'Страницата беше успешно изтрита',
|
||||
'page_restore' => 'възстановена страница',
|
||||
'page_restore_notification' => 'Page successfully restored',
|
||||
'page_restore_notification' => 'Страницата беше успешно възстановена',
|
||||
'page_move' => 'преместена страница',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'създадена страница',
|
||||
'chapter_create_notification' => 'Chapter successfully created',
|
||||
'chapter_create_notification' => 'Главата беше успешно създадена',
|
||||
'chapter_update' => 'обновена глава',
|
||||
'chapter_update_notification' => 'Chapter successfully updated',
|
||||
'chapter_update_notification' => 'Главата беше успешно обновена',
|
||||
'chapter_delete' => 'изтрита глава',
|
||||
'chapter_delete_notification' => 'Chapter successfully deleted',
|
||||
'chapter_delete_notification' => 'Главата беше успешно изтрита',
|
||||
'chapter_move' => 'преместена глава',
|
||||
|
||||
// Books
|
||||
'book_create' => 'създадена книга',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_notification' => 'Книгата беше успешно създадена',
|
||||
'book_update' => 'обновена книга',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_update_notification' => 'Книгата беше успешно обновена',
|
||||
'book_delete' => 'изтрита книга',
|
||||
'book_delete_notification' => 'Book successfully deleted',
|
||||
'book_delete_notification' => 'Книгата беше успешно изтрита',
|
||||
'book_sort' => 'сортирана книга',
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
'book_sort_notification' => 'Книгата беше успешно преподредена',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create' => 'създаден рафт',
|
||||
'bookshelf_create_notification' => 'Рафтът беше успешно създаден',
|
||||
'bookshelf_update' => 'обновен рафт',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_update_notification' => 'Рафтът беше успешно обновен',
|
||||
'bookshelf_delete' => 'изтрит рафт',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_delete_notification' => 'Рафтът беше успешно изтрит',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'created webhook',
|
||||
'webhook_create_notification' => 'Webhook successfully created',
|
||||
'webhook_update' => 'updated webhook',
|
||||
'webhook_update_notification' => 'Webhook successfully updated',
|
||||
'webhook_delete' => 'deleted webhook',
|
||||
'webhook_delete_notification' => 'Webhook successfully deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'коментирано на',
|
||||
'permissions_update' => 'updated permissions',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'Имейл',
|
||||
'password' => 'Парола',
|
||||
'password_confirm' => 'Потвърди паролата',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'password_hint' => 'Трябва да бъде поне 7 символа',
|
||||
'forgot_password' => 'Забравена парола?',
|
||||
'remember_me' => 'Запомни ме',
|
||||
'ldap_email_hint' => 'Моля въведете емейл, който да използвате за дадения акаунт.',
|
||||
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'Изглед списък',
|
||||
'default' => 'Основен',
|
||||
'breadcrumb' => 'Трасиране',
|
||||
'status' => 'Status',
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Expand Header Menu',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'Последна глава',
|
||||
'books_sort_show_other' => 'Покажи други книги',
|
||||
'books_sort_save' => 'Запази новата подредба',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Глава',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'Премести глава',
|
||||
'chapters_move_named' => 'Премести глава :chapterName',
|
||||
'chapter_move_success' => 'Главата беше преместена в :bookName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_permissions' => 'Настойки за достъп на главата',
|
||||
'chapters_empty' => 'Няма създадени страници в тази глава.',
|
||||
'chapters_permissions_active' => 'Настройките за достъп до глава са активни',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'Сигурни ли сте, че искате да изтриете тази версия? Настоящата страница ще бъде заместена.',
|
||||
'revision_delete_success' => 'Версията беше изтрита',
|
||||
'revision_cannot_delete_latest' => 'Не може да изтриете последната версия.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
];
|
||||
|
||||
@@ -174,7 +174,7 @@ return [
|
||||
'users_role' => 'User Roles',
|
||||
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
|
||||
'users_password' => 'User Password',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
|
||||
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||
'users_send_invite_option' => 'Send user invite email',
|
||||
'users_external_auth_id' => 'External Authentication ID',
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
|
||||
'user_api_token_delete_success' => 'API token successfully deleted',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooks',
|
||||
'webhooks_create' => 'Create New Webhook',
|
||||
'webhooks_none_created' => 'No webhooks have yet been created.',
|
||||
'webhooks_edit' => 'Edit Webhook',
|
||||
'webhooks_save' => 'Save Webhook',
|
||||
'webhooks_details' => 'Webhook Details',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Webhook Events',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'All system events',
|
||||
'webhooks_name' => 'Webhook Name',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook Active',
|
||||
'webhook_events_table_header' => 'Events',
|
||||
'webhooks_delete' => 'Delete Webhook',
|
||||
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
|
||||
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
|
||||
'webhooks_format_example' => 'Webhook Format Example',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
@@ -7,41 +7,41 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'je kreirao/la stranicu',
|
||||
'page_create_notification' => 'Page successfully created',
|
||||
'page_create_notification' => 'Stranica Uspješno Kreirana',
|
||||
'page_update' => 'je ažurirao/la stranicu',
|
||||
'page_update_notification' => 'Page successfully updated',
|
||||
'page_update_notification' => 'Stranica Uspješno Ažurirana',
|
||||
'page_delete' => 'je izbrisao/la stranicu',
|
||||
'page_delete_notification' => 'Page successfully deleted',
|
||||
'page_delete_notification' => 'Stranica Uspješno Izbrisana',
|
||||
'page_restore' => 'je vratio/la stranicu',
|
||||
'page_restore_notification' => 'Page successfully restored',
|
||||
'page_restore_notification' => 'Stranica Uspješno Vraćena',
|
||||
'page_move' => 'je premjestio/la stranicu',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'je kreirao/la poglavlje',
|
||||
'chapter_create_notification' => 'Chapter successfully created',
|
||||
'chapter_create_notification' => 'Poglavlje Uspješno Kreirano',
|
||||
'chapter_update' => 'je ažurirao/la poglavlje',
|
||||
'chapter_update_notification' => 'Chapter successfully updated',
|
||||
'chapter_update_notification' => 'Poglavlje Uspješno Ažurirano',
|
||||
'chapter_delete' => 'je izbrisao/la poglavlje',
|
||||
'chapter_delete_notification' => 'Chapter successfully deleted',
|
||||
'chapter_delete_notification' => 'Poglavlje Uspješno Izbrisano',
|
||||
'chapter_move' => 'je premjestio/la poglavlje',
|
||||
|
||||
// Books
|
||||
'book_create' => 'je kreirao/la knjigu',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_notification' => 'Knjiga Uspješno Kreirana',
|
||||
'book_update' => 'je ažurirao/la knjigu',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_update_notification' => 'Knjiga Uspješno Ažurirana',
|
||||
'book_delete' => 'je izbrisao/la knjigu',
|
||||
'book_delete_notification' => 'Book successfully deleted',
|
||||
'book_delete_notification' => 'Knjiga Uspješno Izbrisana',
|
||||
'book_sort' => 'je sortirao/la knjigu',
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
'book_sort_notification' => 'Knjiga Uspješno Ponovno Sortirana',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create' => 'je kreirao/la Policu za knjige',
|
||||
'bookshelf_create_notification' => 'Polica za knjige Uspješno Kreirana',
|
||||
'bookshelf_update' => 'je ažurirao/la policu za knjige',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_update_notification' => 'Polica za knjige Uspješno Ažurirana',
|
||||
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" je dodan u tvoje favorite',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'created webhook',
|
||||
'webhook_create_notification' => 'Webhook successfully created',
|
||||
'webhook_update' => 'updated webhook',
|
||||
'webhook_update_notification' => 'Webhook successfully updated',
|
||||
'webhook_delete' => 'deleted webhook',
|
||||
'webhook_delete_notification' => 'Webhook successfully deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'je komentarisao/la na',
|
||||
'permissions_update' => 'je ažurirao/la dozvole',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'E-mail',
|
||||
'password' => 'Lozinka',
|
||||
'password_confirm' => 'Potvrdi lozinku',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'password_hint' => 'Mora imati više od 7 karaktera',
|
||||
'forgot_password' => 'Zaboravljena lozinka?',
|
||||
'remember_me' => 'Zapamti me',
|
||||
'ldap_email_hint' => 'Unesite e-mail koji će se koristiti za ovaj račun.',
|
||||
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'Prikaz liste',
|
||||
'default' => 'Početne postavke',
|
||||
'breadcrumb' => 'Navigacijske stavke',
|
||||
'status' => 'Status',
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Otvori meni u zaglavlju',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'Poglavlja zadnja',
|
||||
'books_sort_show_other' => 'Prikaži druge knjige',
|
||||
'books_sort_save' => 'Spremi trenutni poredak',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Poglavlje',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'Premjesti poglavlje',
|
||||
'chapters_move_named' => 'Premjesti poglavlje :chapterName',
|
||||
'chapter_move_success' => 'Poglavlje premješteno u :bookName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_permissions' => 'Dozvole poglavlja',
|
||||
'chapters_empty' => 'U ovom poglavlju trenutno nema stranica.',
|
||||
'chapters_permissions_active' => 'Dozvole za poglavlje su aktivne',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
|
||||
'revision_delete_success' => 'Revision deleted',
|
||||
'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
];
|
||||
|
||||
@@ -174,7 +174,7 @@ return [
|
||||
'users_role' => 'User Roles',
|
||||
'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
|
||||
'users_password' => 'User Password',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 6 characters long.',
|
||||
'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
|
||||
'users_send_invite_option' => 'Send user invite email',
|
||||
'users_external_auth_id' => 'External Authentication ID',
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
|
||||
'user_api_token_delete_success' => 'API token successfully deleted',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooks',
|
||||
'webhooks_create' => 'Create New Webhook',
|
||||
'webhooks_none_created' => 'No webhooks have yet been created.',
|
||||
'webhooks_edit' => 'Edit Webhook',
|
||||
'webhooks_save' => 'Save Webhook',
|
||||
'webhooks_details' => 'Webhook Details',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Webhook Events',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'All system events',
|
||||
'webhooks_name' => 'Webhook Name',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook Active',
|
||||
'webhook_events_table_header' => 'Events',
|
||||
'webhooks_delete' => 'Delete Webhook',
|
||||
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
|
||||
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
|
||||
'webhooks_format_example' => 'Webhook Format Example',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
@@ -7,41 +7,41 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'ha creat la pàgina',
|
||||
'page_create_notification' => 'Page successfully created',
|
||||
'page_create_notification' => 'Pàgina creada correctament',
|
||||
'page_update' => 'ha actualitzat la pàgina',
|
||||
'page_update_notification' => 'Page successfully updated',
|
||||
'page_update_notification' => 'Pàgina actualitzada correctament',
|
||||
'page_delete' => 'ha suprimit una pàgina',
|
||||
'page_delete_notification' => 'Page successfully deleted',
|
||||
'page_delete_notification' => 'Pàgina suprimida correctament',
|
||||
'page_restore' => 'ha restaurat la pàgina',
|
||||
'page_restore_notification' => 'Page successfully restored',
|
||||
'page_restore_notification' => 'Pàgina restaurada correctament',
|
||||
'page_move' => 'ha mogut la pàgina',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'ha creat el capítol',
|
||||
'chapter_create_notification' => 'Chapter successfully created',
|
||||
'chapter_create_notification' => 'Capítol creat correctament',
|
||||
'chapter_update' => 'ha actualitzat el capítol',
|
||||
'chapter_update_notification' => 'Chapter successfully updated',
|
||||
'chapter_update_notification' => 'Capítol actualitzat correctament',
|
||||
'chapter_delete' => 'ha suprimit un capítol',
|
||||
'chapter_delete_notification' => 'Chapter successfully deleted',
|
||||
'chapter_delete_notification' => 'Capítol suprimit correctament',
|
||||
'chapter_move' => 'ha mogut el capítol',
|
||||
|
||||
// Books
|
||||
'book_create' => 'ha creat el llibre',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_notification' => 'Llibre creat correctament',
|
||||
'book_update' => 'ha actualitzat el llibre',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_update_notification' => 'Llibre actualitzat correctament',
|
||||
'book_delete' => 'ha suprimit un llibre',
|
||||
'book_delete_notification' => 'Book successfully deleted',
|
||||
'book_delete_notification' => 'Llibre suprimit correctament',
|
||||
'book_sort' => 'ha ordenat el llibre',
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
'book_sort_notification' => 'Llibre reordenat correctament',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create' => 'ha creat el prestatge',
|
||||
'bookshelf_create_notification' => 'Prestatge creat correctament',
|
||||
'bookshelf_update' => 'ha actualitzat el prestatge',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_update_notification' => 'Prestatge actualitzat correctament',
|
||||
'bookshelf_delete' => 'ha suprimit un prestatge',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_delete_notification' => 'Prestatge suprimit correctament',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
|
||||
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'created webhook',
|
||||
'webhook_create_notification' => 'Webhook successfully created',
|
||||
'webhook_update' => 'updated webhook',
|
||||
'webhook_update_notification' => 'Webhook successfully updated',
|
||||
'webhook_delete' => 'deleted webhook',
|
||||
'webhook_delete_notification' => 'Webhook successfully deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'ha comentat a',
|
||||
'permissions_update' => 'ha actualitzat els permisos',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'Adreça electrònica',
|
||||
'password' => 'Contrasenya',
|
||||
'password_confirm' => 'Confirmeu la contrasenya',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'password_hint' => 'Cal que tingui més de 7 caràcters',
|
||||
'forgot_password' => 'Heu oblidat la contrasenya?',
|
||||
'remember_me' => 'Recorda\'m',
|
||||
'ldap_email_hint' => 'Introduïu una adreça electrònica per a aquest compte.',
|
||||
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'Visualització en llista',
|
||||
'default' => 'Per defecte',
|
||||
'breadcrumb' => 'Ruta de navegació',
|
||||
'status' => 'Status',
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Expand Header Menu',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'Els capítols al final',
|
||||
'books_sort_show_other' => 'Mostra altres llibres',
|
||||
'books_sort_save' => 'Desa l\'ordre nou',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Capítol',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'Mou el capítol',
|
||||
'chapters_move_named' => 'Mou el capítol :chapterName',
|
||||
'chapter_move_success' => 'S\'ha mogut el capítol a :bookName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_permissions' => 'Permisos del capítol',
|
||||
'chapters_empty' => 'De moment, aquest capítol no conté cap pàgina.',
|
||||
'chapters_permissions_active' => 'S\'han activat els permisos del capítol',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'Segur que voleu restaurar aquesta revisió? Se substituirà el contingut de la pàgina actual.',
|
||||
'revision_delete_success' => 'S\'ha suprimit la revisió',
|
||||
'revision_cannot_delete_latest' => 'No es pot suprimir la darrera revisió.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
];
|
||||
|
||||
@@ -174,7 +174,7 @@ return [
|
||||
'users_role' => 'Rols de l\'usuari',
|
||||
'users_role_desc' => 'Seleccioneu a quins rols s\'assignarà l\'usuari. Si un usuari s\'assigna a múltiples rols, els permisos dels rols s\'acumularan i l\'usuari rebrà tots els permisos dels rols assignats.',
|
||||
'users_password' => 'Contrasenya de l\'usuari',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'Definiu una contrasenya per a iniciar la sessió a l\'aplicació. Cal que tingui un mínim de 6 caràcters.',
|
||||
'users_send_invite_text' => 'Podeu elegir enviar un correu d\'invitació a aquest usuari, la qual cosa li permetrà definir la seva contrasenya, o podeu definir-li una contrasenya vós.',
|
||||
'users_send_invite_option' => 'Envia un correu d\'invitació a l\'usuari',
|
||||
'users_external_auth_id' => 'Identificador d\'autenticació extern',
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'Segur que voleu suprimir aquest testimoni d\'API?',
|
||||
'user_api_token_delete_success' => 'Testimoni d\'API suprimit correctament',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooks',
|
||||
'webhooks_create' => 'Create New Webhook',
|
||||
'webhooks_none_created' => 'No webhooks have yet been created.',
|
||||
'webhooks_edit' => 'Edit Webhook',
|
||||
'webhooks_save' => 'Save Webhook',
|
||||
'webhooks_details' => 'Webhook Details',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Webhook Events',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'All system events',
|
||||
'webhooks_name' => 'Webhook Name',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook Active',
|
||||
'webhook_events_table_header' => 'Events',
|
||||
'webhooks_delete' => 'Delete Webhook',
|
||||
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
|
||||
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
|
||||
'webhooks_format_example' => 'Webhook Format Example',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
@@ -11,7 +11,7 @@ return [
|
||||
'page_update' => 'aktualizoval/a stránku',
|
||||
'page_update_notification' => 'Stránka byla úspěšně aktualizována',
|
||||
'page_delete' => 'odstranil/a stránku',
|
||||
'page_delete_notification' => 'Stránka byla úspěšně smazána',
|
||||
'page_delete_notification' => 'Stránka byla odstraněna',
|
||||
'page_restore' => 'obnovil/a stránku',
|
||||
'page_restore_notification' => 'Stránka byla úspěšně obnovena',
|
||||
'page_move' => 'přesunul/a stránku',
|
||||
@@ -22,18 +22,18 @@ return [
|
||||
'chapter_update' => 'aktualizoval/a kapitolu',
|
||||
'chapter_update_notification' => 'Kapitola byla úspěšně aktualizována',
|
||||
'chapter_delete' => 'odstranila/a kapitolu',
|
||||
'chapter_delete_notification' => 'Kapitola byla úspěšně odstraněna',
|
||||
'chapter_delete_notification' => 'Kapitola byla odstraněna',
|
||||
'chapter_move' => 'přesunul/a kapitolu',
|
||||
|
||||
// Books
|
||||
'book_create' => 'vytvořil/a knihu',
|
||||
'book_create_notification' => 'Kniha byla úspěšně vytvořena',
|
||||
'book_create_notification' => 'Kniha byla vytvořena',
|
||||
'book_update' => 'aktualizoval/a knihu',
|
||||
'book_update_notification' => 'Kniha byla úspěšně aktualizována',
|
||||
'book_update_notification' => 'Kniha byla aktualizována',
|
||||
'book_delete' => 'odstranil/a knihu',
|
||||
'book_delete_notification' => 'Kniha byla úspěšně odstraněna',
|
||||
'book_delete_notification' => 'Kniha byla odstraněna',
|
||||
'book_sort' => 'seřadil/a knihu',
|
||||
'book_sort_notification' => 'Kniha byla úspěšně seřazena',
|
||||
'book_sort_notification' => 'Kniha byla seřazena',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'vytvořil/a knihovnu',
|
||||
@@ -41,7 +41,7 @@ return [
|
||||
'bookshelf_update' => 'aktualizoval/a knihovnu',
|
||||
'bookshelf_update_notification' => 'Knihovna byla úspěšně aktualizována',
|
||||
'bookshelf_delete' => 'odstranil/a knihovnu',
|
||||
'bookshelf_delete_notification' => 'Knihovna byla úspěšně smazána',
|
||||
'bookshelf_delete_notification' => 'Knihovna byla odstraněna',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" byla přidána do Vašich oblíbených',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Vícefaktorová metoda byla úspěšně nakonfigurována',
|
||||
'mfa_remove_method_notification' => 'Vícefaktorová metoda byla úspěšně odstraněna',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'vytvořil/a webhook',
|
||||
'webhook_create_notification' => 'Webhook byl úspěšně vytvořen',
|
||||
'webhook_update' => 'aktualizoval/a webhook',
|
||||
'webhook_update_notification' => 'Webhook byl úspěšně aktualizován',
|
||||
'webhook_delete' => 'odstranil/a webhook',
|
||||
'webhook_delete_notification' => 'Webhook byl úspěšně odstraněn',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'okomentoval/a',
|
||||
'permissions_update' => 'oprávnění upravena',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'E-mail',
|
||||
'password' => 'Heslo',
|
||||
'password_confirm' => 'Potvrzení hesla',
|
||||
'password_hint' => 'Musí mít alespoň 8 znaků',
|
||||
'password_hint' => 'Musí mít víc než 7 znaků',
|
||||
'forgot_password' => 'Zapomenuté heslo?',
|
||||
'remember_me' => 'Zapamatovat si mě',
|
||||
'ldap_email_hint' => 'Zadejte email, který chcete přiřadit k tomuto účtu.',
|
||||
@@ -54,7 +54,7 @@ return [
|
||||
'email_confirm_text' => 'Prosíme potvrďte svou e-mailovou adresu kliknutím na níže uvedené tlačítko:',
|
||||
'email_confirm_action' => 'Potvrdit e-mail',
|
||||
'email_confirm_send_error' => 'Potvrzení e-mailu je vyžadováno, ale systém nemohl odeslat e-mail. Obraťte se na správce, abyste se ujistili, že je e-mail správně nastaven.',
|
||||
'email_confirm_success' => 'Váš email byl ověřen! Nyní byste měli být schopni se touto emailovou adresou přihlásit.',
|
||||
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
|
||||
'email_confirm_resent' => 'E-mail s potvrzením byl znovu odeslán. Zkontrolujte svou příchozí poštu.',
|
||||
|
||||
'email_not_confirmed' => 'E-mailová adresa nebyla potvrzena',
|
||||
@@ -71,40 +71,40 @@ return [
|
||||
'user_invite_page_welcome' => 'Vítejte v :appName!',
|
||||
'user_invite_page_text' => 'Pro dokončení vašeho účtu a získání přístupu musíte nastavit heslo, které bude použito k přihlášení do :appName při dalších návštěvách.',
|
||||
'user_invite_page_confirm_button' => 'Potvrdit heslo',
|
||||
'user_invite_success_login' => 'Heslo bylo nasteaveno, nyní byste měli být schopni přihlásit se nastaveným heslem do aplikace :appName!',
|
||||
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
|
||||
|
||||
// Multi-factor Authentication
|
||||
'mfa_setup' => 'Nastavit vícefaktorové ověření',
|
||||
'mfa_setup_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',
|
||||
'mfa_setup_configured' => 'Již nastaveno',
|
||||
'mfa_setup_reconfigure' => 'Přenastavit',
|
||||
'mfa_setup_remove_confirmation' => 'Opravdu chcete odstranit tuto metodu vícefaktorového ověřování?',
|
||||
'mfa_setup_action' => 'Nastavit',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'Zbývá vám méně než 5 záložních kódů. Před vypršením kódu si prosím vygenerujte a uložte novou sadu, abyste se vyhnuli zablokování vašeho účtu.',
|
||||
'mfa_setup' => 'Setup Multi-Factor Authentication',
|
||||
'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
|
||||
'mfa_setup_configured' => 'Already configured',
|
||||
'mfa_setup_reconfigure' => 'Reconfigure',
|
||||
'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
|
||||
'mfa_setup_action' => 'Setup',
|
||||
'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
|
||||
'mfa_option_totp_title' => 'Mobilní aplikace',
|
||||
'mfa_option_totp_desc' => 'Pro použití vícefaktorového ověření budete potřebovat mobilní aplikaci, která podporuje TOTP jako např. Google Authenticator, Authy nebo Microsoft Authenticator.',
|
||||
'mfa_option_backup_codes_title' => 'Záložní kódy',
|
||||
'mfa_option_backup_codes_desc' => 'Bezpečně si uložte sadu jednorázových záložních kódů, které můžete použít pro ověření vaší identity.',
|
||||
'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
'mfa_option_backup_codes_title' => 'Backup Codes',
|
||||
'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
|
||||
'mfa_gen_confirm_and_enable' => 'Potvrdit a povolit',
|
||||
'mfa_gen_backup_codes_title' => 'Nastavení záložních kódů',
|
||||
'mfa_gen_backup_codes_desc' => 'Uložte níže uvedený seznam kódů na bezpečné místo. Při přístupu k systému budete moci použít jeden z kódů jako druhou metodu ověření.',
|
||||
'mfa_gen_backup_codes_download' => 'Stáhnout kódy',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Každý kód může být použit pouze jednou',
|
||||
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
|
||||
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
|
||||
'mfa_gen_backup_codes_download' => 'Download Codes',
|
||||
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
|
||||
'mfa_gen_totp_title' => 'Nastavení mobilní aplikace',
|
||||
'mfa_gen_totp_desc' => 'Pro použití vícefaktorového ověření budete potřebovat mobilní aplikaci, která podporuje TOTP jako např. Google Authenticator, Authy nebo Microsoft Authenticator.',
|
||||
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
|
||||
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
|
||||
'mfa_gen_totp_verify_setup' => 'Ověřit nastavení',
|
||||
'mfa_gen_totp_verify_setup' => 'Verify Setup',
|
||||
'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
|
||||
'mfa_gen_totp_provide_code_here' => 'Zde zadejte kód vygenerovaný vaší aplikací',
|
||||
'mfa_verify_access' => 'Ověřit přístup',
|
||||
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
|
||||
'mfa_verify_access' => 'Verify Access',
|
||||
'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
|
||||
'mfa_verify_no_methods' => 'Nejsou nastaveny žádné metody',
|
||||
'mfa_verify_no_methods' => 'No Methods Configured',
|
||||
'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
|
||||
'mfa_verify_use_totp' => 'Ověřit pomocí mobilní aplikace',
|
||||
'mfa_verify_use_backup_codes' => 'Ověřit pomocí záložního kódu',
|
||||
'mfa_verify_backup_code' => 'Záložní kód',
|
||||
'mfa_verify_backup_code_desc' => 'Níže zadejte jeden z vašich zbývajících záložních kódů:',
|
||||
'mfa_verify_backup_code_enter_here' => 'Zde zadejte záložní kód',
|
||||
'mfa_verify_totp_desc' => 'Níže zadejte kód, který jste si vygenerovali pomocí mobilní aplikace:',
|
||||
'mfa_verify_use_totp' => 'Verify using a mobile app',
|
||||
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
|
||||
'mfa_verify_backup_code' => 'Backup Code',
|
||||
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
|
||||
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
|
||||
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
|
||||
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
|
||||
];
|
||||
|
||||
@@ -45,8 +45,8 @@ return [
|
||||
'unfavourite' => 'Odebrat z oblíbených',
|
||||
'next' => 'Další',
|
||||
'previous' => 'Předchozí',
|
||||
'filter_active' => 'Aktivní filtr:',
|
||||
'filter_clear' => 'Zrušit filtr',
|
||||
'filter_active' => 'Active Filter:',
|
||||
'filter_clear' => 'Clear Filter',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Možnosti řazení',
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'Zobrazení seznamu',
|
||||
'default' => 'Výchozí',
|
||||
'breadcrumb' => 'Drobečková navigace',
|
||||
'status' => 'Stav',
|
||||
'status_active' => 'Aktivní',
|
||||
'status_inactive' => 'Neaktivní',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Rozbalit menu v záhlaví',
|
||||
@@ -82,7 +78,7 @@ return [
|
||||
'view_profile' => 'Zobrazit profil',
|
||||
'edit_profile' => 'Upravit profil',
|
||||
'dark_mode' => 'Tmavý režim',
|
||||
'light_mode' => 'Světlý režim',
|
||||
'light_mode' => 'Světelný režim',
|
||||
|
||||
// Layout tabs
|
||||
'tab_info' => 'Informace',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'Kapitoly jako poslední',
|
||||
'books_sort_show_other' => 'Zobrazit ostatní knihy',
|
||||
'books_sort_save' => 'Uložit nové pořadí',
|
||||
'books_copy' => 'Kopírovat knihu',
|
||||
'books_copy_success' => 'Kniha byla úspěšně zkopírována',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Kapitola',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'Přesunout kapitolu',
|
||||
'chapters_move_named' => 'Přesunout kapitolu :chapterName',
|
||||
'chapter_move_success' => 'Kapitola přesunuta do knihy :bookName',
|
||||
'chapters_copy' => 'Kopírovat kapitolu',
|
||||
'chapters_copy_success' => 'Kapitola byla úspěšně zkopírována',
|
||||
'chapters_permissions' => 'Oprávnění kapitoly',
|
||||
'chapters_empty' => 'Tato kapitola neobsahuje žádné stránky',
|
||||
'chapters_permissions_active' => 'Oprávnění kapitoly byla aktivována',
|
||||
@@ -262,16 +258,16 @@ return [
|
||||
'tags_explain' => "Přidejte si štítky pro lepší kategorizaci knih. \n Štítky mohou nést i hodnotu pro detailnější klasifikaci.",
|
||||
'tags_add' => 'Přidat další štítek',
|
||||
'tags_remove' => 'Odstranit tento štítek',
|
||||
'tags_usages' => 'Počet použití štítku',
|
||||
'tags_assigned_pages' => 'Přiřazeno ke stránkám',
|
||||
'tags_assigned_chapters' => 'Přiřazeno ke kapitolám',
|
||||
'tags_assigned_books' => 'Přiřazeno ke knihám',
|
||||
'tags_assigned_shelves' => 'Přiřazeno ke knihovnám',
|
||||
'tags_x_unique_values' => ':count jedinečných hodnot',
|
||||
'tags_all_values' => 'Všechny hodnoty',
|
||||
'tags_view_tags' => 'Zobrazit štítky',
|
||||
'tags_view_existing_tags' => 'Zobrazit existující štítky',
|
||||
'tags_list_empty_hint' => 'Štítky mohou být přiřazeny pomocí postranního panelu editoru stránky nebo při úpravách podrobností knihy, kapitoly nebo knihovny.',
|
||||
'tags_usages' => 'Total tag usages',
|
||||
'tags_assigned_pages' => 'Assigned to Pages',
|
||||
'tags_assigned_chapters' => 'Assigned to Chapters',
|
||||
'tags_assigned_books' => 'Assigned to Books',
|
||||
'tags_assigned_shelves' => 'Assigned to Shelves',
|
||||
'tags_x_unique_values' => ':count unique values',
|
||||
'tags_all_values' => 'All values',
|
||||
'tags_view_tags' => 'View Tags',
|
||||
'tags_view_existing_tags' => 'View existing tags',
|
||||
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
|
||||
'attachments' => 'Přílohy',
|
||||
'attachments_explain' => 'Nahrajte soubory nebo připojte odkazy, které se zobrazí na stránce. Budou k nalezení v postranní liště.',
|
||||
'attachments_explain_instant_save' => 'Změny zde provedené se okamžitě ukládají.',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'Jste si jisti, že chcete obnovit tuto revizi? Aktuální obsah stránky bude nahrazen.',
|
||||
'revision_delete_success' => 'Revize odstraněna',
|
||||
'revision_cannot_delete_latest' => 'Nelze odstranit poslední revizi.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Vlastní nastavení oprávnění nebudou zkopírovány.',
|
||||
'copy_consider_owner' => 'Stanete se vlastníkem veškerého kopírovaného obsahu.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Přílohy stránky nebudou zkopírovány.',
|
||||
'copy_consider_access' => 'Po změně umístění, vlastníka nebo oprávnění může dojít k tomu, že obsah může být přístupný těm, kteří přístup dříve něměli.',
|
||||
];
|
||||
|
||||
@@ -174,14 +174,14 @@ return [
|
||||
'users_role' => 'Uživatelské role',
|
||||
'users_role_desc' => 'Zvolte role, do kterých chcete uživatele zařadit. Pokud bude uživatel zařazen do více rolí, oprávnění z těchto rolí se sloučí a uživateli bude dovoleno vše, k čemu mají jednotlivé role oprávnění.',
|
||||
'users_password' => 'Heslo uživatele',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'Zadejte heslo pro přihlášení do aplikace. Heslo musí být nejméně 6 znaků dlouhé.',
|
||||
'users_send_invite_text' => 'Uživateli můžete poslat pozvánku e-mailem, která umožní uživateli, aby si zvolil sám svoje heslo do aplikace a nebo můžete zadat heslo sami.',
|
||||
'users_send_invite_option' => 'Poslat uživateli pozvánku e-mailem',
|
||||
'users_external_auth_id' => 'Přihlašovací identifikátor třetích stran',
|
||||
'users_external_auth_id_desc' => 'ID použité pro rozpoznání tohoto uživatele když komunikuje s externím přihlašovacím systémem.',
|
||||
'users_password_warning' => 'Vyplňujte pouze v případě, že chcete heslo změnit.',
|
||||
'users_system_public' => 'Symbolizuje každého nepřihlášeného návštěvníka, který navštívil aplikaci. Nelze ho použít k přihlášení ale je přiřazen automaticky nepřihlášeným.',
|
||||
'users_delete' => 'Odstranit uživatele',
|
||||
'users_delete' => 'Smazat uživatele',
|
||||
'users_delete_named' => 'Odstranit uživatele :userName',
|
||||
'users_delete_warning' => 'Uživatel \':userName\' bude zcela odstraněn ze systému.',
|
||||
'users_delete_confirm' => 'Opravdu chcete tohoto uživatele smazat?',
|
||||
@@ -206,10 +206,10 @@ return [
|
||||
'users_api_tokens_none' => 'Tento uživatel nemá vytvořené žádné API Tokeny',
|
||||
'users_api_tokens_create' => 'Vytvořit Token',
|
||||
'users_api_tokens_expires' => 'Vyprší',
|
||||
'users_api_tokens_docs' => 'Dokumentace API',
|
||||
'users_api_tokens_docs' => 'API Dokumentace',
|
||||
'users_mfa' => 'Vícefázové ověření',
|
||||
'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.',
|
||||
'users_mfa_x_methods' => ':count nastavená metoda|:count nastavených metod',
|
||||
'users_mfa_x_methods' => ':count method configured|:count methods configured',
|
||||
'users_mfa_configure' => 'Konfigurovat metody',
|
||||
|
||||
// API Tokens
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'Opravdu chcete odstranit tento API Token?',
|
||||
'user_api_token_delete_success' => 'API Token byl odstraněn',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooky',
|
||||
'webhooks_create' => 'Vytvořit nový webhook',
|
||||
'webhooks_none_created' => 'Žádné webhooky nebyly doposud vytvořeny.',
|
||||
'webhooks_edit' => 'Upravit webhook',
|
||||
'webhooks_save' => 'Uložit webhook',
|
||||
'webhooks_details' => 'Podrobnosti webhooku',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Události webhooku',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'Všechny události systému',
|
||||
'webhooks_name' => 'Název webhooku',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook aktivní',
|
||||
'webhook_events_table_header' => 'Události',
|
||||
'webhooks_delete' => 'Odstranit webhook',
|
||||
'webhooks_delete_warning' => 'Webhook s názvem \':webhookName\' bude úplně odstraněn ze systému.',
|
||||
'webhooks_delete_confirm' => 'Opravdu chcete odstranit tento webhook?',
|
||||
'webhooks_format_example' => 'Příklad formátu webhooku',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
@@ -7,41 +7,41 @@ return [
|
||||
|
||||
// Pages
|
||||
'page_create' => 'oprettede side',
|
||||
'page_create_notification' => 'Page successfully created',
|
||||
'page_create_notification' => 'Siden blev oprettet',
|
||||
'page_update' => 'opdaterede side',
|
||||
'page_update_notification' => 'Page successfully updated',
|
||||
'page_update_notification' => 'Siden blev opdateret',
|
||||
'page_delete' => 'slettede side',
|
||||
'page_delete_notification' => 'Page successfully deleted',
|
||||
'page_delete_notification' => 'Siden blev slettet',
|
||||
'page_restore' => 'gendannede side',
|
||||
'page_restore_notification' => 'Page successfully restored',
|
||||
'page_restore_notification' => 'Siden blev gendannet',
|
||||
'page_move' => 'flyttede side',
|
||||
|
||||
// Chapters
|
||||
'chapter_create' => 'oprettede kapitel',
|
||||
'chapter_create_notification' => 'Chapter successfully created',
|
||||
'chapter_create_notification' => 'Kapitel blev oprettet',
|
||||
'chapter_update' => 'opdaterede kapitel',
|
||||
'chapter_update_notification' => 'Chapter successfully updated',
|
||||
'chapter_update_notification' => 'Kapitlet blev opdateret',
|
||||
'chapter_delete' => 'slettede kapitel',
|
||||
'chapter_delete_notification' => 'Chapter successfully deleted',
|
||||
'chapter_delete_notification' => 'Kapitel blev slettet',
|
||||
'chapter_move' => 'flyttede kapitel',
|
||||
|
||||
// Books
|
||||
'book_create' => 'oprettede bog',
|
||||
'book_create_notification' => 'Book successfully created',
|
||||
'book_create_notification' => 'Bogen blev oprettet',
|
||||
'book_update' => 'opdaterede bog',
|
||||
'book_update_notification' => 'Book successfully updated',
|
||||
'book_update_notification' => 'Bogen blev opdateret',
|
||||
'book_delete' => 'slettede bog',
|
||||
'book_delete_notification' => 'Book successfully deleted',
|
||||
'book_delete_notification' => 'Bogen blev slettet',
|
||||
'book_sort' => 'sorterede bogen',
|
||||
'book_sort_notification' => 'Book successfully re-sorted',
|
||||
'book_sort_notification' => 'Bogen blev re-sorteret',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'created bookshelf',
|
||||
'bookshelf_create_notification' => 'Bookshelf successfully created',
|
||||
'bookshelf_create' => 'oprettede bogreol',
|
||||
'bookshelf_create_notification' => 'Bogreolen blev oprettet',
|
||||
'bookshelf_update' => 'opdaterede bogreolen',
|
||||
'bookshelf_update_notification' => 'Bookshelf successfully updated',
|
||||
'bookshelf_update_notification' => 'Bogreolen blev opdateret',
|
||||
'bookshelf_delete' => 'slettede bogreol',
|
||||
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
|
||||
'bookshelf_delete_notification' => 'Bogreolen blev opdateret',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" er blevet tilføjet til dine favoritter',
|
||||
@@ -51,14 +51,6 @@ return [
|
||||
'mfa_setup_method_notification' => 'Multi-faktor metode konfigureret',
|
||||
'mfa_remove_method_notification' => 'Multi-faktor metode fjernet',
|
||||
|
||||
// Webhooks
|
||||
'webhook_create' => 'created webhook',
|
||||
'webhook_create_notification' => 'Webhook successfully created',
|
||||
'webhook_update' => 'updated webhook',
|
||||
'webhook_update_notification' => 'Webhook successfully updated',
|
||||
'webhook_delete' => 'deleted webhook',
|
||||
'webhook_delete_notification' => 'Webhook successfully deleted',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'kommenterede til',
|
||||
'permissions_update' => 'Tilladelser opdateret',
|
||||
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
'email' => 'E-mail',
|
||||
'password' => 'Adgangskode',
|
||||
'password_confirm' => 'Bekræft adgangskode',
|
||||
'password_hint' => 'Must be at least 8 characters',
|
||||
'password_hint' => 'Skal være på mindst 7 karakterer',
|
||||
'forgot_password' => 'Glemt Adgangskode?',
|
||||
'remember_me' => 'Husk mig',
|
||||
'ldap_email_hint' => 'Angiv venligst din kontos e-mail.',
|
||||
|
||||
@@ -71,10 +71,6 @@ return [
|
||||
'list_view' => 'Listevisning',
|
||||
'default' => 'Standard',
|
||||
'breadcrumb' => 'Brødkrumme',
|
||||
'status' => 'Status',
|
||||
'status_active' => 'Active',
|
||||
'status_inactive' => 'Inactive',
|
||||
'never' => 'Never',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Udvid header menu',
|
||||
|
||||
@@ -143,8 +143,6 @@ return [
|
||||
'books_sort_chapters_last' => 'Kapitler sidst',
|
||||
'books_sort_show_other' => 'Vis andre bøger',
|
||||
'books_sort_save' => 'Gem ny ordre',
|
||||
'books_copy' => 'Copy Book',
|
||||
'books_copy_success' => 'Book successfully copied',
|
||||
|
||||
// Chapters
|
||||
'chapter' => 'Kapitel',
|
||||
@@ -163,8 +161,6 @@ return [
|
||||
'chapters_move' => 'Flyt kapitel',
|
||||
'chapters_move_named' => 'Flyt kapitel :chapterName',
|
||||
'chapter_move_success' => 'Kapitel flyttet til :bookName',
|
||||
'chapters_copy' => 'Copy Chapter',
|
||||
'chapters_copy_success' => 'Chapter successfully copied',
|
||||
'chapters_permissions' => 'Kapiteltilladelser',
|
||||
'chapters_empty' => 'Der er lige nu ingen sider i dette kapitel.',
|
||||
'chapters_permissions_active' => 'Aktive kapiteltilladelser',
|
||||
@@ -336,12 +332,4 @@ return [
|
||||
'revision_restore_confirm' => 'Er du sikker på at du ønsker at gendanne denne revision? Nuværende sideindhold vil blive erstattet.',
|
||||
'revision_delete_success' => 'Revision slettet',
|
||||
'revision_cannot_delete_latest' => 'Kan ikke slette seneste revision.',
|
||||
|
||||
// Copy view
|
||||
'copy_consider' => 'Please consider the below when copying content.',
|
||||
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
|
||||
'copy_consider_owner' => 'You will become the owner of all copied content.',
|
||||
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
|
||||
'copy_consider_attachments' => 'Page attachments will not be copied.',
|
||||
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
|
||||
];
|
||||
|
||||
@@ -174,7 +174,7 @@ return [
|
||||
'users_role' => 'Brugerroller',
|
||||
'users_role_desc' => 'Vælg hvilke roller denne bruger skal tildeles. Hvis en bruger er tildelt flere roller, sammenføres tilladelserne fra disse roller, og de får alle evnerne fra de tildelte roller.',
|
||||
'users_password' => 'Brugeradgangskode',
|
||||
'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
|
||||
'users_password_desc' => 'Sæt et kodeord, der bruges til at logge på applikationen. Dette skal være mindst 6 tegn langt.',
|
||||
'users_send_invite_text' => 'Du kan vælge at sende denne bruger en invitation på E-Mail, som giver dem mulighed for at indstille deres egen adgangskode, ellers kan du indstille deres adgangskode selv.',
|
||||
'users_send_invite_option' => 'Send bruger en invitationsmail',
|
||||
'users_external_auth_id' => 'Ekstern godkendelses ID',
|
||||
@@ -233,34 +233,6 @@ return [
|
||||
'user_api_token_delete_confirm' => 'Er du sikker på, at du vil slette denne API-token?',
|
||||
'user_api_token_delete_success' => 'API-token slettet',
|
||||
|
||||
// Webhooks
|
||||
'webhooks' => 'Webhooks',
|
||||
'webhooks_create' => 'Create New Webhook',
|
||||
'webhooks_none_created' => 'No webhooks have yet been created.',
|
||||
'webhooks_edit' => 'Edit Webhook',
|
||||
'webhooks_save' => 'Save Webhook',
|
||||
'webhooks_details' => 'Webhook Details',
|
||||
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
|
||||
'webhooks_events' => 'Webhook Events',
|
||||
'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
|
||||
'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
|
||||
'webhooks_events_all' => 'All system events',
|
||||
'webhooks_name' => 'Webhook Name',
|
||||
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
|
||||
'webhooks_endpoint' => 'Webhook Endpoint',
|
||||
'webhooks_active' => 'Webhook Active',
|
||||
'webhook_events_table_header' => 'Events',
|
||||
'webhooks_delete' => 'Delete Webhook',
|
||||
'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
|
||||
'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
|
||||
'webhooks_format_example' => 'Webhook Format Example',
|
||||
'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
|
||||
'webhooks_status' => 'Webhook Status',
|
||||
'webhooks_last_called' => 'Last Called:',
|
||||
'webhooks_last_errored' => 'Last Errored:',
|
||||
'webhooks_last_error_message' => 'Last Error Message:',
|
||||
|
||||
|
||||
//! If editing translations files directly please ignore this in all
|
||||
//! languages apart from en. Content will be auto-copied from en.
|
||||
//!////////////////////////////////
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user