Compare commits

..

303 Commits

Author SHA1 Message Date
Dan Brown
262f863981 Updated version and assets for release v21.12.3 2022-01-24 22:49:42 +00:00
Dan Brown
a4c94390a1 Merge branch 'master' into release 2022-01-24 22:49:31 +00:00
Dan Brown
7e6e1fca76 Fixed test broken by PdfGenerator changes 2022-01-24 22:24:41 +00:00
Dan Brown
aaa2205df1 Refreshed markdown cm instance layout on size change
Intended to fix positioning quirks caused by changing codemirror
instance size when you have lines that wrap and cause line height
changes. Often caused by editor toolbox expand/collapse.

This adds a debounced resize observer to refresh editor layout on size
change.
Also tweaks toolbox expand/collapse to more consistently set aria
attribute.

For #3186
2022-01-24 22:08:36 +00:00
Dan Brown
4aed3f8558 Patched gallery duplication on multi-image upload
Quick patch to clear the gallery display when getting the first page.
Duplication of the galler was occuring due to the mulitple upload events
loading the gallery mulitple times while only clearing the existing
gallery at the start of all refreshes.

A bit flashy in terms of user experience, as there will still be
mulitple load/clear events but fixes the duplication. Could be done more
elegently in future by communicating up image upload counts.

For #3160
2022-01-24 21:38:11 +00:00
Dan Brown
7b4086107c Added parent context to recently updated items
- Includes tests to cover
For #3183
2022-01-24 21:21:30 +00:00
Dan Brown
585bd0cc45 Updated translator attribution and StyleCI changes 2022-01-24 20:55:03 +00:00
Dan Brown
f18e2784be New Crowdin updates (#3158)
* New translations activities.php (Slovak)

* New translations activities.php (Slovak)

* New translations common.php (Russian)

* New translations settings.php (Russian)

* New translations common.php (Japanese)

* New translations settings.php (Japanese)

* New translations activities.php (Persian)

* New translations auth.php (Persian)

* New translations auth.php (Persian)

* New translations entities.php (Persian)

* New translations common.php (Persian)

* New translations auth.php (Persian)

* New translations entities.php (Persian)

* New translations settings.php (Persian)

* New translations entities.php (Persian)

* New translations validation.php (Persian)

* New translations settings.php (Persian)

* New translations settings.php (Spanish, Argentina)

* New translations common.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations activities.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Persian)

* New translations settings.php (Persian)

* New translations entities.php (Persian)

* New translations errors.php (Persian)

* New translations settings.php (French)

* New translations common.php (French)

* New translations settings.php (French)

* New translations entities.php (Persian)

* New translations settings.php (Persian)

* New translations entities.php (Persian)

* New translations errors.php (Persian)
2022-01-24 20:53:36 +00:00
Dan Brown
f88e6d1520 Updated HTMLDiff package to address multibtye issue
Addresses potential issue when using multibyte characters.
Couple of other packages seemed to have updates also since earlier.

For #3170
2022-01-24 20:27:14 +00:00
Dan Brown
872961ef7c Updated npm and php dependancies 2022-01-24 18:53:28 +00:00
Dan Brown
bbd8d63652 Merge pull request #3179 from Julesdevops/atomic-user-creation
When creating a user, do not persist the user on invitation sending failure
2022-01-24 18:48:00 +00:00
Dan Brown
af39ff15ac Merge branch 'show_more_informations_on_recently_updated_pages' 2022-01-24 18:23:47 +00:00
Dan Brown
aae3cd69d7 Added test to cover PR #3177 2022-01-24 18:23:16 +00:00
Dan Brown
2d3df955ae Merge branch 'master' of github.com:BookStackApp/BookStack 2022-01-24 17:26:17 +00:00
Dan Brown
8b5747eae2 Further adjusted linked image sizes on PDF export
Further fixes for #3120, Adds DOMPDF specific adjustments to prevent
full width linked images being cut-off as per last tweak.
This does not fix usage in smaller cases (tables) but tested on
master DOMPDF branch shows that will likely be fixed in next DOMPDF
upstream release.
DOMPDF fixes would break WKHTMLTOPDF presentation so system updated
to conditionally apply styles.
2022-01-24 17:24:00 +00:00
Dan Brown
6c699f7fab Merge pull request #3193 from Julesdevops/xdebug-docker-compose-setup
chore(dev): add xdebug support for docker setup
2022-01-24 16:11:29 +00:00
julesdevops
ac6eceb0e5 doc(dev): add xdebug informations 2022-01-23 14:26:01 +01:00
julesdevops
a2a2f3a4dd chore(dev): add xdebug support for docker setup 2022-01-22 17:43:29 +01:00
julesdevops
6db64763fe enh(recently updated): show updatedBy and updated_at 2022-01-19 21:49:45 +01:00
julesdevops
c9beacbfbf fix(User Creation): do not persist the user if invitation fails
- Wrap the user creation process in a transaction
- Add test
2022-01-19 20:46:38 +01:00
Dan Brown
53f3cca85d Updated version and assets for release v21.12.2 2022-01-10 18:23:44 +00:00
Dan Brown
ed08bbcecc Merge branch 'master' into release 2022-01-10 18:23:19 +00:00
Dan Brown
2aace16704 Updated translator attribution before release v21.12.2 2022-01-10 18:22:43 +00:00
Dan Brown
ade66dcf2f Applied latest styleci changes 2022-01-10 18:18:15 +00:00
Dan Brown
d3eaaf6457 New Crowdin updates (#3148)
* New translations common.php (Czech)

* New translations common.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations errors.php (Japanese)

* New translations entities.php (Japanese)

* New translations common.php (Japanese)

* New translations settings.php (Japanese)

* New translations entities.php (Japanese)

* New translations settings.php (Japanese)

* New translations auth.php (Japanese)

* New translations common.php (Portuguese, Brazilian)

* New translations activities.php (Portuguese, Brazilian)

* New translations activities.php (Portuguese, Brazilian)
2022-01-10 18:17:28 +00:00
Dan Brown
941217d9fb Improved loading for images with failed thumbnails
- A placeholder is now shown in the gallery.
- The page editors will use the original image url if the display
  thumbnail is missing.

For #3142
2022-01-10 18:13:48 +00:00
Dan Brown
4239d4c54d Fixed error on webhooks for recycle bin operations
Updated the getUrl method on deletions to not require any passed
params to align with usage in webhooks.
Probably better to have a proper interface but would require a wider
change.

Fixes #3154
2022-01-10 17:47:49 +00:00
Dan Brown
8d91f4369b Improved custom homepage check on item deletion
Custom homepage usage will now be checked before any actioning
of deletion rather than potentially causing an exception acting
during the deletion.

Previously a deletion could still be created, within the recycle bin,
for the parent which may lead to the page being deleted anyway.

For #3150
2022-01-10 17:04:01 +00:00
Dan Brown
722aa04577 Merge pull request #3153 from AitorMatxi/patch-1
Update auth.php
2022-01-10 16:10:39 +00:00
Aitor Matxinea
2d0abc4164 Update auth.php
Fix misspelled word "As" to "Has".
2022-01-10 11:45:48 +01:00
Dan Brown
de97ebf9b7 Updated version and assets for release v21.12.1 2022-01-06 12:20:37 +00:00
Dan Brown
f492a660a8 Merge branch 'master' into release 2022-01-06 12:20:26 +00:00
Dan Brown
09436836a5 Updated version and assets for release v21.12 2021-12-22 17:04:18 +00:00
Dan Brown
bb455d7788 Merge branch 'master' into release 2021-12-22 17:03:50 +00:00
Dan Brown
009212ab80 Updated version and assets for release v21.11.3 2021-12-15 14:08:37 +00:00
Dan Brown
ba9cb591c8 Merge branch 'master' into release 2021-12-15 14:08:17 +00:00
Dan Brown
d00ac2f34e Updated version and assets for release v21.11.2 2021-11-30 14:30:19 +00:00
Dan Brown
bd4dc6d463 Merge branch 'master' into release 2021-11-30 14:29:53 +00:00
Dan Brown
d91180a909 Updated version and assets for release v21.11.1 2021-11-23 20:44:36 +00:00
Dan Brown
bc2913a5cb Merge branch 'master' into release 2021-11-23 20:44:12 +00:00
Dan Brown
4802394562 Updated version and assets for release v21.11 2021-11-16 13:22:24 +00:00
Dan Brown
1755556468 Merge branch 'master' into release 2021-11-16 13:21:44 +00:00
Dan Brown
01cdbdb7ae Updated version and assets for release v21.10.3 2021-11-01 13:31:10 +00:00
Dan Brown
fc8bbf3eab Merge branch 'master' into release 2021-11-01 13:30:36 +00:00
Dan Brown
3cdab19319 Updated version and assets for release v21.10.2 2021-10-28 15:57:04 +01:00
Dan Brown
5661d20e87 Merge branch 'master' into release 2021-10-28 15:56:49 +01:00
Dan Brown
91f80123e8 Merge branch 'master' into release 2021-10-27 12:35:00 +01:00
Dan Brown
7a0636d0f8 Updated version and assets for release v21.10.1 2021-10-27 12:31:40 +01:00
Dan Brown
0fe5bdfbac Updated version and assets for release v21.10 2021-10-25 15:59:23 +01:00
Dan Brown
f88687e977 Merge branch 'master' into release 2021-10-25 15:58:59 +01:00
Dan Brown
68d437d05b Updated version and assets for release v21.08.6 2021-10-15 14:34:44 +01:00
Dan Brown
1e56aaea04 Merge branch 'master' into release 2021-10-15 14:34:23 +01:00
Dan Brown
dab170a6fe Updated version and assets for release v21.08.5 2021-10-08 22:25:36 +01:00
Dan Brown
a8de717d9b Merge branch 'master' into release 2021-10-08 22:25:05 +01:00
Dan Brown
78fe95b6fc Updated version and assets for release v21.08.4 2021-10-04 16:25:24 +01:00
Dan Brown
e0c24e41aa Merge branch 'master' into release 2021-10-04 16:24:54 +01:00
Dan Brown
fa8553839b Updated version and assets for release v21.08.3 2021-09-12 16:31:02 +01:00
Dan Brown
b8fcefc794 Merge branch 'master' into release 2021-09-12 16:30:35 +01:00
Dan Brown
88bcb68fcb Updated version and assets for release v21.08.2 2021-09-04 15:07:20 +01:00
Dan Brown
7c000553ae Merge branch 'master' into release 2021-09-04 15:06:33 +01:00
Dan Brown
391fa35c80 Updated version and assets for release v21.08.1 2021-09-02 21:13:09 +01:00
Dan Brown
c6773a8c9f Merge branch 'master' into release 2021-09-02 21:12:06 +01:00
Dan Brown
9b226e7d39 Updated version and assets for release v21.08 2021-08-31 22:07:53 +01:00
Dan Brown
9865446267 Merge branch 'master' into release 2021-08-31 22:07:23 +01:00
Dan Brown
926abbe776 Updated version and assets for release v21.05.4 2021-08-04 21:29:10 +01:00
Dan Brown
4fabef3a57 Merge branch 'v21.05.x' into release 2021-08-04 21:28:45 +01:00
Dan Brown
5ef4cd80c3 Updated version and assets for release v21.05.3 2021-07-03 11:59:52 +01:00
Dan Brown
e01f23583f Merge branch 'v21.05.x' into release 2021-07-03 11:59:21 +01:00
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
78886b1e67 Updated version and assets for release v21.04.1 2021-04-19 22:26:19 +01:00
Dan Brown
d9debaf032 Merge branch 'master' into release 2021-04-19 22:25:29 +01:00
Dan Brown
d4360d6347 Updated version and assets for release v21.04 2021-04-09 21:18:32 +01:00
Dan Brown
175b1785c0 Merge branch 'master' into release 2021-04-09 21:18:09 +01:00
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
0e799a3857 Updated version and assets for release v0.31.2 2021-01-10 14:05:16 +00:00
Dan Brown
b91d6e2bfa Merge branch 'master' into release 2021-01-10 14:04:59 +00:00
Dan Brown
ea16ad7e94 Updated version and assets for release v0.31.1 2021-01-04 18:41:55 +00:00
Dan Brown
ba6eb54552 Merge branch 'master' into release 2021-01-04 18:41:26 +00:00
Dan Brown
f705e7683b Updated assets for release v0.31.0 again 2021-01-03 22:33:36 +00:00
Dan Brown
dc996adb20 Merge branch 'master' into release 2021-01-03 22:32:40 +00:00
Dan Brown
a64c638ccc Updated version and assets for release v0.31.0 2021-01-03 21:52:37 +00:00
Dan Brown
359c067279 Merge branch 'master' into release 2021-01-03 21:52:00 +00:00
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
4818192a2a Updated version and assets for release v0.29.1 2020-04-28 12:30:31 +01:00
Dan Brown
965dd97f54 Merge branch 'master' into release 2020-04-28 12:30:09 +01:00
Dan Brown
195b74926c Updated version and assets for release v0.29.0 2020-04-13 16:10:23 +01:00
Dan Brown
2120db12b2 Merge branch 'master' into release 2020-04-13 16:10:11 +01:00
Dan Brown
ed563fef28 Updated version and assets for release v0.28.3 2020-03-14 22:31:42 +00:00
Dan Brown
0d31a8e3f1 Merge branch 'master' into release 2020-03-14 22:31:11 +00:00
Dan Brown
b8354b974b Updated version and assets for release v0.28.2 2020-02-15 22:36:08 +00:00
Dan Brown
034c1e289d Merge branch 'master' into release 2020-02-15 22:35:46 +00:00
Dan Brown
f31605a3de Updated version and assets for release v0.28.1 2020-02-15 22:08:06 +00:00
Dan Brown
e7cc75c74d Merge branch 'master' into release 2020-02-15 22:07:17 +00:00
Dan Brown
4b79d5e4e8 Updated version and assets for release v0.28.0 2020-02-03 22:44:45 +00:00
Dan Brown
34854915b3 Merge branch 'master' into release 2020-02-03 22:43:58 +00:00
Dan Brown
af6f34b529 Updated version and assets for release v0.27.5 2019-10-16 16:35:50 +01:00
Dan Brown
fb82a2b896 Merge branch 'patching-v0.27' into release 2019-10-16 16:35:10 +01:00
Dan Brown
5b464938b6 Updated version and assets for release v0.27.4 2019-09-07 13:30:08 +01:00
Dan Brown
81f954890d Merge branch 'patching-v0.27' into release 2019-09-07 13:29:53 +01:00
Dan Brown
0e2bbcec62 Updated version and assets for release v0.27.3 2019-09-03 21:50:12 +01:00
Dan Brown
fdd339f525 Merge branch 'master' into release 2019-09-03 21:49:46 +01:00
Dan Brown
8cf7d6a83d Updated version and assets for release v0.27.2 2019-09-01 12:12:23 +01:00
Dan Brown
58a5008718 Merge branch 'master' into release 2019-09-01 12:12:10 +01:00
Dan Brown
c44a8df55d Updated version and assets for release v0.27.1 2019-09-01 11:13:50 +01:00
Dan Brown
ff1494c519 Merge branch 'master' into release 2019-09-01 11:13:18 +01:00
Dan Brown
b8ce8fd852 Updated assets for release v0.27 2019-08-31 14:16:14 +01:00
Dan Brown
75e7454a5f Merge branch 'master' into release and set version 2019-08-31 14:15:18 +01:00
Dan Brown
2558ea8931 Updated version for release v0.26.4 2019-08-06 21:42:09 +01:00
Dan Brown
ac0f47a4b2 Merge branch 'v0.26' into release 2019-08-06 21:41:06 +01:00
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
b70a5c0cdb Updated version and assets for release v0.26.1 2019-05-07 23:05:47 +01:00
Dan Brown
9443ae9f40 Merge branch 'master' into release 2019-05-07 23:05:10 +01:00
Dan Brown
220c2a4102 Updated version and assets for release v0.26.0 2019-05-06 18:58:56 +01:00
Dan Brown
e9914eb301 Merge branch 'master' into release 2019-05-06 18:57:58 +01:00
Dan Brown
934512d09c Updated version and assets for release v0.25.5 2019-03-24 19:45:17 +00:00
Dan Brown
9102c90986 Merge branch 'master' into release 2019-03-24 19:45:00 +00:00
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
95b3e78573 Updated version and assets for release v0.25.0 2019-01-12 22:48:53 +00:00
Dan Brown
63a345bc93 Merge branch 'master' into release 2019-01-12 22:47:07 +00:00
Dan Brown
e093a172cb Updated assets and version for release v0.24.3 2018-11-27 21:52:20 +00:00
Dan Brown
4b01f8934b Merge branch 'master' into release 2018-11-27 21:51:32 +00:00
Dan Brown
bc116b45b5 Re-updated assets for release v0.24.2 2018-11-10 16:10:22 +00:00
Dan Brown
a059960b9e Merge branch 'master' into release 2018-11-10 16:09:14 +00:00
Dan Brown
7770966fed Updated assets for release v0.24.2 2018-11-10 16:01:55 +00:00
Dan Brown
d7adcf6c69 Merge branch 'master' into release 2018-11-10 16:01:01 +00:00
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
6ad21ce885 Added built assets for release 2015-11-30 21:59:34 +00:00
102 changed files with 1711 additions and 6092 deletions

View File

@@ -210,3 +210,8 @@ Tomáš Batelka (Vofy) :: Czech
Mundo Racional (ismael.mesquita) :: Portuguese, Brazilian
Zarik (3apuk) :: Russian
Ali Shaatani (a.shaatani) :: Arabic
ChacMaster :: Portuguese, Brazilian
Saeed (saeed205) :: Persian
Julesdevops :: French
peter cerny (posli.to.semka) :: Slovak
Pavel Karlin (pavelkarlin) :: Russian

6
.gitignore vendored
View File

@@ -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
View File

@@ -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.

View File

@@ -59,7 +59,7 @@ class Deletion extends Model implements Loggable
/**
* Get a URL for this specific deletion.
*/
public function getUrl($path): string
public function getUrl(string $path = 'restore'): string
{
return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/'));
}

View File

@@ -36,6 +36,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property string $slug
* @property Carbon $created_at
* @property Carbon $updated_at
* @property Carbon $deleted_at
* @property int $created_by
* @property int $updated_by
* @property bool $restricted

View File

@@ -46,19 +46,10 @@ class PageRevision extends Model
/**
* Get the url for this revision.
*
* @param null|string $path
*
* @return string
*/
public function getUrl($path = null)
public function getUrl(string $path = ''): string
{
$url = $this->page->getUrl() . '/revisions/' . $this->id;
if ($path) {
return $url . '/' . trim($path, '/');
}
return $url;
return $this->page->getUrl('/revisions/' . $this->id . '/' . ltrim($path, '/'));
}
/**

View File

@@ -92,6 +92,7 @@ class ExportFormatter
$html = view('pages.export', [
'page' => $page,
'format' => 'pdf',
'engine' => $this->pdfGenerator->getActiveEngine(),
])->render();
return $this->htmlToPdf($html);
@@ -113,6 +114,7 @@ class ExportFormatter
'chapter' => $chapter,
'pages' => $pages,
'format' => 'pdf',
'engine' => $this->pdfGenerator->getActiveEngine(),
])->render();
return $this->htmlToPdf($html);
@@ -130,6 +132,7 @@ class ExportFormatter
'book' => $book,
'bookChildren' => $bookTree,
'format' => 'pdf',
'engine' => $this->pdfGenerator->getActiveEngine(),
])->render();
return $this->htmlToPdf($html);

View File

@@ -7,14 +7,15 @@ use Barryvdh\Snappy\Facades\SnappyPdf;
class PdfGenerator
{
const ENGINE_DOMPDF = 'dompdf';
const ENGINE_WKHTML = 'wkhtml';
/**
* Generate PDF content from the given HTML content.
*/
public function fromHtml(string $html): string
{
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
if ($useWKHTML) {
if ($this->getActiveEngine() === self::ENGINE_WKHTML) {
$pdf = SnappyPDF::loadHTML($html);
$pdf->setOption('print-media-type', true);
} else {
@@ -23,4 +24,15 @@ class PdfGenerator
return $pdf->output();
}
/**
* Get the currently active PDF engine.
* Returns the value of an `ENGINE_` const on this class.
*/
public function getActiveEngine(): string
{
$useWKHTML = config('snappy.pdf.binary') !== false && config('app.allow_untrusted_server_fetching') === true;
return $useWKHTML ? self::ENGINE_WKHTML : self::ENGINE_DOMPDF;
}
}

View File

@@ -22,9 +22,12 @@ class TrashCan
{
/**
* Send a shelf to the recycle bin.
*
* @throws NotifyException
*/
public function softDestroyShelf(Bookshelf $shelf)
{
$this->ensureDeletable($shelf);
Deletion::createForEntity($shelf);
$shelf->delete();
}
@@ -36,6 +39,7 @@ class TrashCan
*/
public function softDestroyBook(Book $book)
{
$this->ensureDeletable($book);
Deletion::createForEntity($book);
foreach ($book->pages as $page) {
@@ -57,6 +61,7 @@ class TrashCan
public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
{
if ($recordDelete) {
$this->ensureDeletable($chapter);
Deletion::createForEntity($chapter);
}
@@ -77,19 +82,47 @@ class TrashCan
public function softDestroyPage(Page $page, bool $recordDelete = true)
{
if ($recordDelete) {
$this->ensureDeletable($page);
Deletion::createForEntity($page);
}
// Check if set as custom homepage & remove setting if not used or throw error if active
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
$page->delete();
}
/**
* Ensure the given entity is deletable.
* Is not for permissions, but logical conditions within the application.
* Will throw if not deletable.
*
* @throws NotifyException
*/
protected function ensureDeletable(Entity $entity): void
{
$customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
$customHomeActive = setting('app-homepage-type') === 'page';
$removeCustomHome = false;
// Check custom homepage usage for pages
if ($entity instanceof Page && $entity->id === $customHomeId) {
if ($customHomeActive) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
}
setting()->remove('app-homepage');
$removeCustomHome = true;
}
$page->delete();
// Check custom homepage usage within chapters or books
if ($entity instanceof Chapter || $entity instanceof Book) {
if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
if ($customHomeActive) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
}
$removeCustomHome = true;
}
}
if ($removeCustomHome) {
setting()->remove('app-homepage');
}
}
/**

View File

@@ -14,6 +14,7 @@ use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -364,15 +365,22 @@ class PageController extends Controller
*/
public function showRecentlyUpdated()
{
$pages = Page::visible()->orderBy('updated_at', 'desc')
$visibleBelongsScope = function (BelongsTo $query) {
$query->scopes('visible');
};
$pages = Page::visible()->with(['updatedBy', 'book' => $visibleBelongsScope, 'chapter' => $visibleBelongsScope])
->orderBy('updated_at', 'desc')
->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,
'title' => trans('entities.recently_updated_pages'),
'entities' => $pages,
'showUpdatedBy' => true,
'showPath' => true,
]);
}

View File

@@ -12,6 +12,7 @@ use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
@@ -99,20 +100,23 @@ class UserController extends Controller
}
$user->refreshSlug();
$user->save();
if ($sendInvite) {
$this->inviteService->sendInvitation($user);
}
DB::transaction(function () use ($user, $sendInvite, $request) {
$user->save();
if ($request->filled('roles')) {
$roles = $request->get('roles');
$this->userRepo->setUserRoles($user, $roles);
}
if ($sendInvite) {
$this->inviteService->sendInvitation($user);
}
$this->userRepo->downloadAndAssignUserAvatar($user);
if ($request->filled('roles')) {
$roles = $request->get('roles');
$this->userRepo->setUserRoles($user, $roles);
}
$this->logActivity(ActivityType::USER_CREATE, $user);
$this->userRepo->downloadAndAssignUserAvatar($user);
$this->logActivity(ActivityType::USER_CREATE, $user);
});
return redirect('/settings/users');
}

View File

@@ -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);
}
/**

View File

@@ -41,7 +41,7 @@
"socialiteproviders/okta": "^4.1",
"socialiteproviders/slack": "^4.1",
"socialiteproviders/twitch": "^5.3",
"ssddanbrown/htmldiff": "^1.0.1"
"ssddanbrown/htmldiff": "^1.0.2"
},
"require-dev": {
"fakerphp/faker": "^1.16",

849
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@ RUN apt-get update -y \
&& apt-get install -y git zip unzip libpng-dev libldap2-dev libzip-dev wait-for-it \
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu \
&& docker-php-ext-install pdo_mysql gd ldap zip \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug \
&& a2enmod rewrite \
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf

View File

@@ -0,0 +1,7 @@
zend_extension=xdebug
[xdebug]
xdebug.mode=debug
xdebug.client_host=host.docker.internal
xdebug.start_with_request=yes
xdebug.client_port=9090

View File

@@ -38,7 +38,10 @@ services:
- ${DEV_PORT:-8080}:80
volumes:
- ./:/app
- ./dev/docker/php/conf.d/xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
entrypoint: /app/dev/docker/entrypoint.app.sh
extra_hosts:
- "host.docker.internal:host-gateway"
node:
image: node:alpine
working_dir: /app

817
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -18,27 +16,18 @@
},
"devDependencies": {
"chokidar-cli": "^3.0",
"esbuild": "0.13.12",
"esbuild": "0.14.13",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.43.4"
"sass": "^1.49.0"
},
"dependencies": {
"clipboard": "^2.0.8",
"codemirror": "^5.63.3",
"crelt": "^1.0.5",
"codemirror": "^5.65.1",
"dropzone": "^5.9.3",
"markdown-it": "^12.2.0",
"markdown-it": "^12.3.2",
"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

File diff suppressed because one or more lines are too long

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
View 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

File diff suppressed because one or more lines are too long

BIN
public/loading_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -156,6 +156,11 @@ Once the database has been migrated & seeded, you can run the tests like so:
docker-compose run app php vendor/bin/phpunit
```
#### Debugging
The docker-compose setup ships with Xdebug, which you can listen to on port 9090.
NB : For some editors like Visual Studio Code, you might need to map your workspace folder to the /app folder within the docker container for this to work.
## 🌎 Translations
Translations for text within BookStack is managed through the [BookStack project on Crowdin](https://crowdin.com/project/bookstack). Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time. Crowdin is the preferred way to provide translations, otherwise the raw translations files can be found within the `resources/lang` path.

View File

@@ -41,7 +41,9 @@ class EditorToolbox {
if (cName === tabName) this.contentElements[i].style.display = 'block';
}
if (openToolbox) this.elem.classList.add('open');
if (openToolbox && !this.elem.classList.contains('open')) {
this.toggle();
}
}
}

View File

@@ -74,6 +74,10 @@ class ImageManager {
this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
this.listContainer.addEventListener('error', event => {
event.target.src = baseUrl('loading_error.png');
}, true);
onSelect(this.selectButton, () => {
if (this.callback) {
this.callback(this.lastSelected);
@@ -118,6 +122,9 @@ class ImageManager {
};
const {data: html} = await window.$http.get(`images/${this.type}`, params);
if (params.page === 1) {
this.listContainer.innerHTML = '';
}
this.addReturnedHtmlElementsToList(html);
removeLoading(this.listContainer);
}

View File

@@ -112,6 +112,11 @@ class MarkdownEditor {
if (scrollText) {
this.scrollToText(scrollText);
}
// Refresh CodeMirror on container resize
const resizeDebounced = debounce(() => code.updateLayout(this.cm), 100, false);
const observer = new ResizeObserver(resizeDebounced);
observer.observe(this.elem);
}
// Update the input content and render the display.
@@ -395,8 +400,9 @@ class MarkdownEditor {
actionInsertImage() {
const cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
const imageUrl = image.thumbs.display || image.url;
let selectedText = this.cm.getSelection();
let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);

View File

@@ -563,8 +563,9 @@ class WysiwygEditor {
}
// Replace the actively selected content with the linked image
const imageUrl = image.thumbs.display || image.url;
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += `<img src="${imageUrl}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
}, 'gallery');
@@ -723,8 +724,9 @@ class WysiwygEditor {
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.show(function (image) {
const imageUrl = image.thumbs.display || image.url;
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += `<img src="${imageUrl}" alt="${image.name}">`;
html += '</a>';
editor.execCommand('mceInsertContent', false, html);
}, 'gallery');

View File

@@ -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()
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';
}
}

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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);
}
}

View File

@@ -74,7 +74,7 @@ return [
'status' => 'Stav',
'status_active' => 'Aktivní',
'status_inactive' => 'Neaktivní',
'never' => 'Never',
'never' => 'Nikdy',
// Header
'header_menu_expand' => 'Rozbalit menu v záhlaví',

View File

@@ -64,7 +64,7 @@ return [
'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
// User Invite
'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
'user_invite_email_subject' => 'Has sido invitado a unirte a :appName!',
'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',

View File

@@ -6,7 +6,7 @@
return [
// Pages
'page_create' => 'página creada',
'page_create' => 'creó la página',
'page_create_notification' => 'Página creada correctamente',
'page_update' => 'página actualizada',
'page_update_notification' => 'Página actualizada correctamente',

View File

@@ -73,7 +73,7 @@ return [
'breadcrumb' => 'Miga de Pan',
'status' => 'Estado',
'status_active' => 'Activo',
'status_inactive' => 'Inactive',
'status_inactive' => 'Inactivo',
'never' => 'Nunca',
// Header

View File

@@ -341,7 +341,7 @@ return [
'copy_consider' => 'Por favor, tenga en cuenta lo siguiente al copiar el contenido.',
'copy_consider_permissions' => 'Los ajustes de permisos personalizados no serán copiados.',
'copy_consider_owner' => 'Usted se convertirá en el dueño de todo el contenido copiado.',
'copy_consider_images' => 'Los archivos de imagen de de las páginas no serán duplicados y las imágenes originales conservarán su relación con la página a la que fueron subidos originalmente.',
'copy_consider_images' => 'Los archivos de imagen de la página no serán duplicados y las imágenes originales conservarán su relación con la página a la que fueron subidos originalmente.',
'copy_consider_attachments' => 'Los archivos adjuntos de la página no serán copiados.',
'copy_consider_access' => 'Un cambio de ubicación, propietario o permisos puede resultar en que este contenido sea accesible para aquellos que anteriormente no tuvieran acceso.',
];

View File

@@ -236,26 +236,26 @@ return [
// Webhooks
'webhooks' => 'Webhooks',
'webhooks_create' => 'Crear Webhook',
'webhooks_create' => 'Crear nuevo Webhook',
'webhooks_none_created' => 'No hay webhooks creados.',
'webhooks_edit' => 'Editar Webhook',
'webhooks_save' => 'Guardar Webhook',
'webhooks_details' => 'Detalles del Webhook',
'webhooks_details_desc' => 'Proporcione un nombre y un punto final POST como destino para los datos del webhook que se enviarán.',
'webhooks_details_desc' => 'Proporcione un nombre y un punto final de POST como destino para enviar los datos del webhook.',
'webhooks_events' => 'Eventos del Webhook',
'webhooks_events_desc' => 'Seleccione todos los eventos que deberían activar este webhook.',
'webhooks_events_warning' => 'Tenga en cuenta que estos eventos se activarán para todos los eventos seleccionados, incluso si se aplican permisos personalizados. Asegúrese de que el uso de este webhook no exponga contenido confidencial.',
'webhooks_events_all' => 'Todos los eventos del sistema',
'webhooks_name' => 'Nombre del Webhook',
'webhooks_timeout' => 'Tiempo de Espera de Webhook (Segundos)',
'webhooks_timeout' => 'Tiempo de Espera de Solicitud del Webhook (Segundos)',
'webhooks_endpoint' => 'Punto final del Webhook',
'webhooks_active' => 'Webhook Activo',
'webhook_events_table_header' => 'Eventos',
'webhooks_delete' => 'Eliminar Webhook',
'webhooks_delete_warning' => 'Esto eliminará completamente este webhook, con el nombre \':webhookName\', del sistema.',
'webhooks_delete_confirm' => '¿Seguro que quieres eliminar este webhook?',
'webhooks_delete_warning' => 'Esto eliminará completamente del sistema este webhook con el nombre \':webhookName\'.',
'webhooks_delete_confirm' => '¿Está seguro que quiere eliminar este webhook?',
'webhooks_format_example' => 'Ejemplo de Formato de Webhook',
'webhooks_format_example_desc' => 'Los datos del Webhook se envían como una solicitud POST al punto final configurado como JSON siguiendo el formato mostrado a continuación. Las propiedades "related_item" y "url" son opcionales y dependerán del tipo de evento activado.',
'webhooks_format_example_desc' => 'Los datos del Webhook, en formato JSON, se envían como una solicitud POST al punto final siguiendo el formato mostrado a continuación. Las propiedades "related_item" y "url" son opcionales y dependerán del tipo de evento activado.',
'webhooks_status' => 'Estado del Webhook',
'webhooks_last_called' => 'Última Ejecución:',
'webhooks_last_errored' => 'Último error:',

View File

@@ -7,57 +7,57 @@ 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" به علاقه مندی های شما اضافه شد',
'favourite_remove_notification' => '":name" از علاقه مندی های شما حذف شد',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'روش چند فاکتوری با موفقیت پیکربندی شد',
'mfa_remove_method_notification' => 'روش چند فاکتوری با موفقیت حذف شد',
// 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',
'webhook_create' => 'ایجاد وب هوک',
'webhook_create_notification' => 'وب هوک با موفقیت ایجاد شد',
'webhook_update' => 'به روزرسانی وب هوک',
'webhook_update_notification' => 'وب هوک با موفقیت بروزرسانی شد',
'webhook_delete' => 'حذف وب هوک',
'webhook_delete_notification' => 'وب هوک با موفقیت حذف شد',
// Other
'commented_on' => 'ثبت دیدگاه',

View File

@@ -21,7 +21,7 @@ return [
'email' => 'پست الکترونیک',
'password' => 'کلمه عبور',
'password_confirm' => 'تایید کلمه عبور',
'password_hint' => 'Must be at least 8 characters',
'password_hint' => 'باید بیش از 8 کاراکتر باشد',
'forgot_password' => 'کلمه عبور خود را فراموش کرده اید؟',
'remember_me' => 'مرا به خاطر بسپار',
'ldap_email_hint' => 'لطفا برای استفاده از این حساب کاربری پست الکترونیک وارد نمایید.',
@@ -54,7 +54,7 @@ return [
'email_confirm_text' => 'لطفا با کلیک بر روی دکمه زیر پست الکترونیک خود را تایید نمایید:',
'email_confirm_action' => 'تایید پست الکترونیک',
'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.',
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
'email_confirm_success' => 'ایمیل شما تایید شد! اکنون باید بتوانید با استفاده از این آدرس ایمیل وارد شوید.',
'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.',
'email_not_confirmed' => 'پست الکترونیک تایید نشده است',
@@ -71,7 +71,7 @@ return [
'user_invite_page_welcome' => 'به :appName خوش آمدید!',
'user_invite_page_text' => 'برای نهایی کردن حساب کاربری خود در :appName و دسترسی به آن، می بایست یک کلمه عبور تنظیم نمایید.',
'user_invite_page_confirm_button' => 'تایید کلمه عبور',
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
'user_invite_success_login' => 'رمز عبور تنظیم شده است، اکنون باید بتوانید با استفاده از رمز عبور تعیین شده خود وارد شوید تا به :appName دسترسی پیدا کنید!',
// Multi-factor Authentication
'mfa_setup' => 'تنظیم احراز هویت چند مرحله‌ای',
@@ -80,31 +80,31 @@ return [
'mfa_setup_reconfigure' => 'تنظیم مجدد',
'mfa_setup_remove_confirmation' => 'از حذف احراز هویت چند مرحله ای اطمینان دارید؟',
'mfa_setup_action' => 'تنظیم',
'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.',
'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' => 'Confirm and Enable',
'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' => 'Mobile App Setup',
'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' => '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' => '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' => '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' => '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.',
'mfa_backup_codes_usage_limit_warning' => 'کمتر از 5 کد پشتیبان باقی مانده است، لطفاً قبل از تمام شدن کدها یک مجموعه جدید ایجاد و ذخیره کنید تا از قفل شدن حساب خود جلوگیری کنید.',
'mfa_option_totp_title' => 'برنامه ی موبایل',
'mfa_option_totp_desc' => 'برای استفاده از احراز هویت چند عاملی به یک برنامه موبایلی نیاز دارید که از TOTP پشتیبانی کند، مانند Google Authenticator، Authy یا Microsoft Authenticator.',
'mfa_option_backup_codes_title' => 'کدهای پشتیبان',
'mfa_option_backup_codes_desc' => 'مجموعه ای از کدهای پشتیبان یکبار مصرف را ایمن ذخیره کنید که می توانید برای تأیید هویت خود وارد کنید.',
'mfa_gen_confirm_and_enable' => 'تایید و فعال کنید',
'mfa_gen_backup_codes_title' => 'راه اندازی کدهای پشتیبان',
'mfa_gen_backup_codes_desc' => 'لیست کدهای زیر را در مکانی امن ذخیره کنید. هنگام دسترسی به سیستم، می توانید از یکی از کدها به عنوان مکانیزم احراز هویت دوم استفاده کنید.',
'mfa_gen_backup_codes_download' => 'دانلود کدها',
'mfa_gen_backup_codes_usage_warning' => 'هر کد فقط یک بار قابل استفاده است',
'mfa_gen_totp_title' => 'راه اندازی اپلیکیشن موبایل',
'mfa_gen_totp_desc' => 'برای استفاده از احراز هویت چند عاملی به یک برنامه موبایلی نیاز دارید که از TOTP پشتیبانی کند، مانند Google Authenticator، Authy یا Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'برای شروع، کد QR زیر را با استفاده از برنامه احراز هویت ترجیحی خود اسکن کنید.',
'mfa_gen_totp_verify_setup' => 'تأیید تنظیمات',
'mfa_gen_totp_verify_setup_desc' => 'با وارد کردن کدی که در برنامه احراز هویت شما ایجاد شده است، در کادر ورودی زیر، مطمئن شوید که همه کار می کنند:',
'mfa_gen_totp_provide_code_here' => 'کد تولید شده برنامه خود را در اینجا ارائه دهید',
'mfa_verify_access' => 'تأیید دسترسی',
'mfa_verify_access_desc' => 'قبل از اینکه به شما اجازه دسترسی داده شود، حساب کاربری شما از شما می خواهد که هویت خود را از طریق یک سطح تأیید اضافی تأیید کنید. برای ادامه، با استفاده از یکی از روش های پیکربندی شده خود، تأیید کنید.',
'mfa_verify_no_methods' => 'هیچ روشی پیکربندی نشده است',
'mfa_verify_no_methods_desc' => 'هیچ روش احراز هویت چند عاملی برای حساب شما یافت نشد. قبل از دسترسی، باید حداقل یک روش را تنظیم کنید.',
'mfa_verify_use_totp' => 'با استفاده از یک برنامه تلفن همراه تأیید کنید',
'mfa_verify_use_backup_codes' => 'با استفاده از یک کد پشتیبان تأیید کنید',
'mfa_verify_backup_code' => 'کد پشتیبان',
'mfa_verify_backup_code_desc' => 'یکی از کدهای پشتیبان باقی مانده خود را در زیر وارد کنید:',
'mfa_verify_backup_code_enter_here' => 'کد پشتیبان را در اینجا وارد کنید',
'mfa_verify_totp_desc' => 'کد ایجاد شده با استفاده از برنامه تلفن همراه خود را در زیر وارد کنید:',
'mfa_setup_login_notification' => 'روش چند عاملی پیکربندی شد، لطفاً اکنون دوباره با استفاده از روش پیکربندی شده وارد شوید.',
];

View File

@@ -39,14 +39,14 @@ return [
'reset' => 'بازنشانی',
'remove' => 'حذف',
'add' => 'ﺍﻓﺰﻭﺩﻥ',
'configure' => 'Configure',
'configure' => 'پیکربندی کنید',
'fullscreen' => 'تمام صفحه',
'favourite' => 'علاقه‌مندی',
'unfavourite' => 'Unfavourite',
'next' => 'بعدی',
'previous' => 'قبلى',
'filter_active' => 'Active Filter:',
'filter_clear' => 'Clear Filter',
'filter_active' => 'فیلتر فعال:',
'filter_clear' => 'پاک کردن فیلتر',
// Sort Options
'sort_options' => 'گزینه‌های مرتب سازی',
@@ -71,10 +71,10 @@ return [
'list_view' => 'نمای لیست',
'default' => 'پیش‎فرض',
'breadcrumb' => 'مسیر جاری',
'status' => 'Status',
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'status' => 'وضعیت',
'status_active' => 'فعال',
'status_inactive' => 'غیر فعال',
'never' => 'هرگز',
// Header
'header_menu_expand' => 'گسترش منو',

View File

@@ -60,288 +60,288 @@ return [
'search_options' => 'گزینه ها',
'search_viewed_by_me' => 'بازدید شده به وسیله من',
'search_not_viewed_by_me' => 'توسط من مشاهده نشده است',
'search_permissions_set' => 'Permissions set',
'search_created_by_me' => 'Created by me',
'search_updated_by_me' => 'Updated by me',
'search_owned_by_me' => 'Owned by me',
'search_date_options' => 'Date Options',
'search_updated_before' => 'Updated before',
'search_updated_after' => 'Updated after',
'search_created_before' => 'Created before',
'search_created_after' => 'Created after',
'search_set_date' => 'Set Date',
'search_update' => 'Update Search',
'search_permissions_set' => 'مجوزها تنظیم شده است',
'search_created_by_me' => 'ایجاد شده توسط من',
'search_updated_by_me' => 'به روز شده توسط من',
'search_owned_by_me' => 'متعلق به من است',
'search_date_options' => 'گزینه های تاریخ',
'search_updated_before' => 'قبلا به روز شده',
'search_updated_after' => 'پس از به روز رسانی',
'search_created_before' => 'قبلا ایجاد شده است',
'search_created_after' => 'ایجاد شده پس از',
'search_set_date' => 'تنظیم تاریخ',
'search_update' => 'جستجو را به روز کنید',
// Shelves
'shelf' => 'Shelf',
'shelves' => 'Shelves',
'x_shelves' => ':count Shelf|:count Shelves',
'shelves_long' => 'Bookshelves',
'shelves_empty' => 'No shelves have been created',
'shelf' => 'تاقچه',
'shelves' => 'قفسه ها',
'x_shelves' => ':count تاقچه|:count تاقچه',
'shelves_long' => 'قفسه کتاب',
'shelves_empty' => 'هیچ قفسه ای ایجاد نشده است',
'shelves_create' => 'Create New Shelf',
'shelves_popular' => 'Popular Shelves',
'shelves_new' => 'New Shelves',
'shelves_new_action' => 'New Shelf',
'shelves_popular_empty' => 'The most popular shelves will appear here.',
'shelves_new_empty' => 'The most recently created shelves will appear here.',
'shelves_save' => 'Save Shelf',
'shelves_books' => 'Books on this shelf',
'shelves_add_books' => 'Add books to this shelf',
'shelves_drag_books' => 'Drag books here to add them to this shelf',
'shelves_empty_contents' => 'This shelf has no books assigned to it',
'shelves_edit_and_assign' => 'Edit shelf to assign books',
'shelves_edit_named' => 'Edit Bookshelf :name',
'shelves_edit' => 'Edit Bookshelf',
'shelves_delete' => 'Delete Bookshelf',
'shelves_delete_named' => 'Delete Bookshelf :name',
'shelves_delete_explain' => "This will delete the bookshelf with the name ':name'. Contained books will not be deleted.",
'shelves_delete_confirmation' => 'Are you sure you want to delete this bookshelf?',
'shelves_permissions' => 'Bookshelf Permissions',
'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
'shelves_permissions_active' => 'Bookshelf Permissions Active',
'shelves_permissions_cascade_warning' => 'Permissions on bookshelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
'shelves_copy_permissions' => 'Copy Permissions',
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
'shelves_copy_permission_success' => 'Bookshelf permissions copied to :count books',
'shelves_popular' => 'قفسه های محبوب',
'shelves_new' => 'قفسه های جدید',
'shelves_new_action' => 'قفسه جدید',
'shelves_popular_empty' => 'محبوب ترین قفسه ها در اینجا ظاهر می شوند.',
'shelves_new_empty' => 'جدیدترین قفسه های ایجاد شده در اینجا ظاهر می شوند.',
'shelves_save' => 'ذخیره قفسه',
'shelves_books' => 'کتاب های موجود در این قفسه',
'shelves_add_books' => 'کتاب ها را به این قفسه اضافه کنید',
'shelves_drag_books' => 'کتاب‌ها را به اینجا بکشید تا به این قفسه اضافه شوند',
'shelves_empty_contents' => 'این قفسه هیچ کتابی به آن اختصاص داده نشده است',
'shelves_edit_and_assign' => 'برای اختصاص کتاب‌ها، قفسه را ویرایش کنید',
'shelves_edit_named' => 'ویرایش قفسه کتاب :name',
'shelves_edit' => 'ویرایش قفسه کتاب',
'shelves_delete' => 'حذف قفسه کتاب',
'shelves_delete_named' => 'حذف قفسه کتاب :name',
'shelves_delete_explain' => "با این کار قفسه کتاب با نام ':name' حذف می شود. کتاب های موجود حذف نمی شوند.",
'shelves_delete_confirmation' => 'آیا مطمئنید که می خواهید این قفسه کتاب را حذف کنید؟',
'shelves_permissions' => 'مجوزهای قفسه کتاب',
'shelves_permissions_updated' => 'مجوزهای قفسه کتاب به روز شد',
'shelves_permissions_active' => 'مجوزهای قفسه کتاب فعال است',
'shelves_permissions_cascade_warning' => 'مجوزهای موجود در قفسه‌های کتاب به طور خودکار به کتاب‌های حاوی آبشار نمی‌شوند. این به این دلیل است که یک کتاب می تواند در چندین قفسه وجود داشته باشد. با این حال، مجوزها را می‌توان با استفاده از گزینه زیر در کتاب‌های کودک کپی کرد.',
'shelves_copy_permissions_to_books' => 'کپی مجوزها در کتابها',
'shelves_copy_permissions' => 'مجوزهای کپی',
'shelves_copy_permissions_explain' => 'با این کار تنظیمات مجوز فعلی این قفسه کتاب برای همه کتاب‌های موجود در آن اعمال می‌شود. قبل از فعال کردن، مطمئن شوید که هر گونه تغییر در مجوزهای این قفسه کتاب ذخیره شده است.',
'shelves_copy_permission_success' => 'مجوزهای قفسه کتاب در :count books کپی شد',
// Books
'book' => 'Book',
'books' => 'Books',
'x_books' => ':count Book|:count Books',
'books_empty' => 'No books have been created',
'books_popular' => 'Popular Books',
'books_recent' => 'Recent Books',
'books_new' => 'New Books',
'books_new_action' => 'New Book',
'books_popular_empty' => 'The most popular books will appear here.',
'books_new_empty' => 'The most recently created books will appear here.',
'books_create' => 'Create New Book',
'books_delete' => 'Delete Book',
'books_delete_named' => 'Delete Book :bookName',
'books_delete_explain' => 'This will delete the book with the name \':bookName\'. All pages and chapters will be removed.',
'books_delete_confirmation' => 'Are you sure you want to delete this book?',
'books_edit' => 'Edit Book',
'books_edit_named' => 'Edit Book :bookName',
'books_form_book_name' => 'Book Name',
'books_save' => 'Save Book',
'books_permissions' => 'Book Permissions',
'books_permissions_updated' => 'Book Permissions Updated',
'books_empty_contents' => 'No pages or chapters have been created for this book.',
'books_empty_create_page' => 'Create a new page',
'books_empty_sort_current_book' => 'Sort the current book',
'books_empty_add_chapter' => 'Add a chapter',
'books_permissions_active' => 'Book Permissions Active',
'books_search_this' => 'Search this book',
'books_navigation' => 'Book Navigation',
'books_sort' => 'Sort Book Contents',
'books_sort_named' => 'Sort Book :bookName',
'books_sort_name' => 'Sort by Name',
'books_sort_created' => 'Sort by Created Date',
'books_sort_updated' => 'Sort by Updated Date',
'books_sort_chapters_first' => 'Chapters First',
'books_sort_chapters_last' => 'Chapters Last',
'books_sort_show_other' => 'Show Other Books',
'books_sort_save' => 'Save New Order',
'books_copy' => 'Copy Book',
'books_copy_success' => 'Book successfully copied',
'book' => 'کتاب',
'books' => 'کتابها',
'x_books' => ':count کتاب|:count کتاب',
'books_empty' => 'هیچ کتابی ایجاد نشده است',
'books_popular' => 'کتاب های محبوب',
'books_recent' => 'کتاب های اخیر',
'books_new' => 'کتاب های جدید',
'books_new_action' => 'کتاب جدید',
'books_popular_empty' => 'محبوب ترین کتاب ها در اینجا ظاهر می شوند.',
'books_new_empty' => 'جدیدترین کتاب‌های ایجاد شده در اینجا ظاهر می‌شوند.',
'books_create' => 'ایجاد کتاب جدید',
'books_delete' => 'حذف کتاب',
'books_delete_named' => 'حذف کتاب:bookName',
'books_delete_explain' => 'با این کار کتابی با نام \':bookName\' حذف می شود. تمام صفحات و فصل ها حذف خواهند شد.',
'books_delete_confirmation' => 'آیا مطمئن هستید که می خواهید این کتاب را حذف کنید؟',
'books_edit' => 'ویرایش کتاب',
'books_edit_named' => 'ویرایش کتاب:bookName',
'books_form_book_name' => 'نام کتاب',
'books_save' => 'ذخیره کتاب',
'books_permissions' => 'مجوزهای کتاب',
'books_permissions_updated' => 'مجوزهای کتاب به روز شد',
'books_empty_contents' => 'هیچ صفحه یا فصلی برای این کتاب ایجاد نشده است.',
'books_empty_create_page' => 'یک صفحه جدید ایجاد کنید',
'books_empty_sort_current_book' => 'کتاب فعلی را مرتب کنید',
'books_empty_add_chapter' => 'یک فصل اضافه کنید',
'books_permissions_active' => 'مجوزهای کتاب فعال است',
'books_search_this' => 'این کتاب را جستجو کنید',
'books_navigation' => 'ناوبری کتاب',
'books_sort' => 'مرتب سازی مطالب کتاب',
'books_sort_named' => 'مرتب سازی کتاب:bookName',
'books_sort_name' => 'مرتب سازی بر اساس نام',
'books_sort_created' => 'مرتب سازی بر اساس تاریخ ایجاد',
'books_sort_updated' => 'مرتب سازی بر اساس تاریخ به روز رسانی',
'books_sort_chapters_first' => 'فصل اول',
'books_sort_chapters_last' => 'فصل آخر',
'books_sort_show_other' => 'نمایش کتاب های دیگر',
'books_sort_save' => 'ذخیره سفارش جدید',
'books_copy' => 'کپی کتاب',
'books_copy_success' => 'کتاب با موفقیت کپی شد',
// Chapters
'chapter' => 'Chapter',
'chapters' => 'Chapters',
'x_chapters' => ':count Chapter|:count Chapters',
'chapters_popular' => 'Popular Chapters',
'chapters_new' => 'New Chapter',
'chapters_create' => 'Create New Chapter',
'chapters_delete' => 'Delete Chapter',
'chapters_delete_named' => 'Delete Chapter :chapterName',
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
'chapters_edit' => 'Edit Chapter',
'chapters_edit_named' => 'Edit Chapter :chapterName',
'chapters_save' => 'Save Chapter',
'chapters_move' => 'Move Chapter',
'chapters_move_named' => 'Move Chapter :chapterName',
'chapter_move_success' => 'Chapter moved to :bookName',
'chapters_copy' => 'Copy Chapter',
'chapters_copy_success' => 'Chapter successfully copied',
'chapters_permissions' => 'Chapter Permissions',
'chapters_empty' => 'No pages are currently in this chapter.',
'chapters_permissions_active' => 'Chapter Permissions Active',
'chapters_permissions_success' => 'Chapter Permissions Updated',
'chapters_search_this' => 'Search this chapter',
'chapter' => 'فصل',
'chapters' => 'فصل',
'x_chapters' => ':count فصل|:count فصل',
'chapters_popular' => 'فصل های محبوب',
'chapters_new' => 'فصل جدید',
'chapters_create' => 'ایجاد فصل جدید',
'chapters_delete' => 'حذف فصل',
'chapters_delete_named' => 'حذف فصل :chapterName',
'chapters_delete_explain' => 'با این کار فصلی با نام \':chapterName\' حذف می شود. تمامی صفحاتی که در این فصل وجود دارند نیز حذف خواهند شد.',
'chapters_delete_confirm' => 'آیا مطمئن هستید که می خواهید این فصل را حذف کنید؟',
'chapters_edit' => 'ویرایش فصل',
'chapters_edit_named' => 'ویرایش فصل :chapterName',
'chapters_save' => 'ذخیره فصل',
'chapters_move' => 'انتقال فصل',
'chapters_move_named' => 'انتقال فصل :chapterName',
'chapter_move_success' => 'فصل به :bookName منتقل شد',
'chapters_copy' => 'کپی کردن فصل',
'chapters_copy_success' => 'فصل با موفقیت کپی شد',
'chapters_permissions' => 'مجوزهای فصل',
'chapters_empty' => 'در حال حاضر هیچ صفحه ای در این فصل وجود ندارد.',
'chapters_permissions_active' => 'مجوزهای فصل فعال است',
'chapters_permissions_success' => 'مجوزهای فصل به روز شد',
'chapters_search_this' => 'این فصل را جستجو کنید',
// Pages
'page' => 'Page',
'pages' => 'Pages',
'x_pages' => ':count Page|:count Pages',
'pages_popular' => 'Popular Pages',
'pages_new' => 'New Page',
'pages_attachments' => 'Attachments',
'pages_navigation' => 'Page Navigation',
'pages_delete' => 'Delete Page',
'pages_delete_named' => 'Delete Page :pageName',
'pages_delete_draft_named' => 'Delete Draft Page :pageName',
'pages_delete_draft' => 'Delete Draft Page',
'pages_delete_success' => 'Page deleted',
'pages_delete_draft_success' => 'Draft page deleted',
'pages_delete_confirm' => 'Are you sure you want to delete this page?',
'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
'pages_editing_named' => 'Editing Page :pageName',
'pages_edit_draft_options' => 'Draft Options',
'pages_edit_save_draft' => 'Save Draft',
'pages_edit_draft' => 'Edit Page Draft',
'pages_editing_draft' => 'Editing Draft',
'pages_editing_page' => 'Editing Page',
'pages_edit_draft_save_at' => 'Draft saved at ',
'pages_edit_delete_draft' => 'Delete Draft',
'pages_edit_discard_draft' => 'Discard Draft',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
'pages_save' => 'Save Page',
'pages_title' => 'Page Title',
'pages_name' => 'Page Name',
'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Insert Image',
'pages_md_insert_link' => 'Insert Entity Link',
'pages_md_insert_drawing' => 'Insert Drawing',
'pages_not_in_chapter' => 'Page is not in a chapter',
'pages_move' => 'Move Page',
'pages_move_success' => 'Page moved to ":parentName"',
'pages_copy' => 'Copy Page',
'pages_copy_desination' => 'Copy Destination',
'pages_copy_success' => 'Page successfully copied',
'pages_permissions' => 'Page Permissions',
'pages_permissions_success' => 'Page permissions updated',
'pages_revision' => 'Revision',
'pages_revisions' => 'Page Revisions',
'pages_revisions_named' => 'Page Revisions for :pageName',
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revision_restored_from' => 'Restored from #:id; :summary',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'page' => 'صفحه',
'pages' => 'صفحات',
'x_pages' => ':count صفحه|:count صفحه',
'pages_popular' => 'صفحات محبوب',
'pages_new' => 'صفحه جدید',
'pages_attachments' => 'پیوست‌ها',
'pages_navigation' => 'پیمایش صفحه',
'pages_delete' => 'حذف صفحه',
'pages_delete_named' => 'حذف صفحه:pageName',
'pages_delete_draft_named' => 'حذف پیش نویس صفحه:pageName',
'pages_delete_draft' => 'حذف صفحه پیش نویس',
'pages_delete_success' => 'صفحه حذف شد',
'pages_delete_draft_success' => 'صفحه پیش نویس حذف شد',
'pages_delete_confirm' => 'آیا مطمئن هستید که می خواهید این صفحه را حذف کنید؟',
'pages_delete_draft_confirm' => 'آیا مطمئن هستید که می خواهید این صفحه پیش نویس را حذف کنید؟',
'pages_editing_named' => 'ویرایش صفحه :pageName',
'pages_edit_draft_options' => 'گزینه های پیش نویس',
'pages_edit_save_draft' => 'ذخیره پیش نویس',
'pages_edit_draft' => 'ویرایش پیش نویس صفحه',
'pages_editing_draft' => 'در حال ویرایش پیش نویس',
'pages_editing_page' => 'در حال ویرایش صفحه',
'pages_edit_draft_save_at' => 'پیش نویس ذخیره شده در',
'pages_edit_delete_draft' => 'حذف پیش نویس',
'pages_edit_discard_draft' => 'دور انداختن پیش نویس',
'pages_edit_set_changelog' => 'تنظیم تغییرات',
'pages_edit_enter_changelog_desc' => 'توضیح مختصری از تغییراتی که ایجاد کرده اید وارد کنید',
'pages_edit_enter_changelog' => 'وارد کردن تغییرات',
'pages_save' => 'ذخیره صفحه',
'pages_title' => 'عنوان صفحه',
'pages_name' => 'نام صفحه',
'pages_md_editor' => 'ویرایشگر',
'pages_md_preview' => 'پيش نمايش',
'pages_md_insert_image' => 'درج تصویر',
'pages_md_insert_link' => 'پیوند نهاد را درج کنید',
'pages_md_insert_drawing' => 'درج طرح',
'pages_not_in_chapter' => 'صفحه در یک فصل نیست',
'pages_move' => 'انتقال صفحه',
'pages_move_success' => 'صفحه به ":parentName" منتقل شد',
'pages_copy' => 'صفحه را کپی کنید',
'pages_copy_desination' => 'مقصد را کپی کنید',
'pages_copy_success' => 'صفحه با موفقیت کپی شد',
'pages_permissions' => 'مجوزهای صفحه',
'pages_permissions_success' => 'مجوزهای صفحه به روز شد',
'pages_revision' => 'تجدید نظر',
'pages_revisions' => 'ویرایش های صفحه',
'pages_revisions_named' => 'بازبینی صفحه برای :pageName',
'pages_revision_named' => 'ویرایش صفحه برای :pageName',
'pages_revision_restored_from' => 'بازیابی شده از #:id; :summary',
'pages_revisions_created_by' => 'ایجاد شده توسط',
'pages_revisions_date' => 'تاریخ تجدید نظر',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restore',
'pages_revisions_none' => 'This page has no revisions',
'pages_copy_link' => 'Copy Link',
'pages_edit_content_link' => 'Edit Content',
'pages_permissions_active' => 'Page Permissions Active',
'pages_initial_revision' => 'Initial publish',
'pages_initial_name' => 'New Page',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_revisions_numbered' => 'تجدید نظر #:id',
'pages_revisions_numbered_changes' => 'بازبینی #:id تغییرات',
'pages_revisions_changelog' => 'لیست تغییرات',
'pages_revisions_changes' => 'تغییرات',
'pages_revisions_current' => 'نسخه‌ی جاری',
'pages_revisions_preview' => 'پيش نمايش',
'pages_revisions_restore' => 'بازگرداندن',
'pages_revisions_none' => 'این صفحه هیچ ویرایشی ندارد',
'pages_copy_link' => 'کپی لینک',
'pages_edit_content_link' => 'ویرایش محتوا',
'pages_permissions_active' => 'مجوزهای صفحه فعال است',
'pages_initial_revision' => 'انتشار اولیه',
'pages_initial_name' => 'برگهٔ تازه',
'pages_editing_draft_notification' => 'شما در حال ویرایش پیش نویسی هستید که آخرین بار در :timeDiff ذخیره شده است.',
'pages_draft_edited_notification' => 'این صفحه از همان زمان به روز شده است. توصیه می شود از این پیش نویس صرف نظر کنید.',
'pages_draft_page_changed_since_creation' => 'این صفحه از زمان ایجاد این پیش نویس به روز شده است. توصیه می‌شود که این پیش‌نویس را کنار بگذارید یا مراقب باشید که تغییرات صفحه را بازنویسی نکنید.',
'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page',
'start_b' => ':userName has started editing this page',
'time_a' => 'since the page was last updated',
'time_b' => 'in the last :minCount minutes',
'message' => ':start :time. Take care not to overwrite each other\'s updates!',
'start_a' => ':count کاربران شروع به ویرایش این صفحه کرده اند',
'start_b' => ':userName ویرایش این صفحه را شروع کرده است',
'time_a' => 'از آخرین به روز رسانی صفحه',
'time_b' => 'در آخرین دقیقه :minCount',
'message' => ':start :time. مراقب باشید به روز رسانی های یکدیگر را بازنویسی نکنید!',
],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
'pages_specific' => 'Specific Page',
'pages_is_template' => 'Page Template',
'pages_draft_discarded' => 'پیش نویس حذف شد، ویرایشگر با محتوای صفحه فعلی به روز شده است',
'pages_specific' => 'صفحه خاص',
'pages_is_template' => 'الگوی صفحه',
// Editor Sidebar
'page_tags' => 'Page Tags',
'chapter_tags' => 'Chapter Tags',
'book_tags' => 'Book Tags',
'shelf_tags' => 'Shelf Tags',
'tag' => 'Tag',
'tags' => 'Tags',
'tag_name' => 'Tag Name',
'tag_value' => 'Tag Value (Optional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
'tags_add' => 'Add another tag',
'tags_remove' => 'Remove this tag',
'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' => 'Attachments',
'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',
'attachments_explain_instant_save' => 'Changes here are saved instantly.',
'attachments_items' => 'Attached Items',
'attachments_upload' => 'Upload File',
'attachments_link' => 'Attach Link',
'attachments_set_link' => 'Set Link',
'attachments_delete' => 'Are you sure you want to delete this attachment?',
'attachments_dropzone' => 'Drop files or click here to attach a file',
'attachments_no_files' => 'No files have been uploaded',
'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',
'attachments_link_name' => 'Link Name',
'attachment_link' => 'Attachment link',
'attachments_link_url' => 'Link to file',
'attachments_link_url_hint' => 'Url of site or file',
'attach' => 'Attach',
'attachments_insert_link' => 'Add Attachment Link to Page',
'attachments_edit_file' => 'Edit File',
'attachments_edit_file_name' => 'File Name',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
'attachments_order_updated' => 'Attachment order updated',
'attachments_updated_success' => 'Attachment details updated',
'attachments_deleted' => 'Attachment deleted',
'attachments_file_uploaded' => 'File successfully uploaded',
'attachments_file_updated' => 'File successfully updated',
'attachments_link_attached' => 'Link successfully attached to page',
'templates' => 'Templates',
'templates_set_as_template' => 'Page is a template',
'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
'templates_replace_content' => 'Replace page content',
'templates_append_content' => 'Append to page content',
'templates_prepend_content' => 'Prepend to page content',
'page_tags' => 'تگ های صفحه',
'chapter_tags' => 'برچسب های فصل',
'book_tags' => 'برچسب های کتاب',
'shelf_tags' => 'برچسب های قفسه',
'tag' => 'برچسب',
'tags' => 'برچسب ها',
'tag_name' => 'نام برچسب',
'tag_value' => 'مقدار برچسب (اختیاری)',
'tags_explain' => "برای دسته بندی بهتر مطالب خود چند تگ اضافه کنید.\n می توانید برای سازماندهی عمیق تر، یک مقدار به یک برچسب اختصاص دهید.",
'tags_add' => 'یک برچسب دیگر اضافه کنید',
'tags_remove' => 'این تگ را حذف کنید',
'tags_usages' => 'مجموع استفاده از برچسب',
'tags_assigned_pages' => 'به صفحات اختصاص داده شده است',
'tags_assigned_chapters' => 'اختصاص به فصل',
'tags_assigned_books' => 'به کتاب ها اختصاص داده شده است',
'tags_assigned_shelves' => 'به قفسه ها اختصاص داده شده است',
'tags_x_unique_values' => ':count مقادیر منحصر به فرد',
'tags_all_values' => 'همه ارزش ها',
'tags_view_tags' => 'مشاهده برچسب ها',
'tags_view_existing_tags' => 'مشاهده تگ های موجود',
'tags_list_empty_hint' => 'برچسب ها را می توان از طریق نوار کناری ویرایشگر صفحه یا هنگام ویرایش جزئیات یک کتاب، فصل یا قفسه اختصاص داد.',
'attachments' => 'پیوست ها',
'attachments_explain' => 'چند فایل را آپلود کنید یا چند پیوند را برای نمایش در صفحه خود ضمیمه کنید. اینها در نوار کناری صفحه قابل مشاهده هستند.',
'attachments_explain_instant_save' => 'تغییرات در اینجا فورا ذخیره می شوند.',
'attachments_items' => 'موارد پیوست شده',
'attachments_upload' => 'آپلود فایل',
'attachments_link' => 'پیوند را ضمیمه کنید',
'attachments_set_link' => 'پیوند را تنظیم کنید',
'attachments_delete' => 'آیا مطمئن هستید که می خواهید این پیوست را حذف کنید؟',
'attachments_dropzone' => 'فایل ها را رها کنید یا برای پیوست کردن یک فایل اینجا را کلیک کنید',
'attachments_no_files' => 'هیچ فایلی آپلود نشده است',
'attachments_explain_link' => 'اگر ترجیح می دهید فایلی را آپلود نکنید، می توانید پیوندی را پیوست کنید. این می تواند پیوندی به صفحه دیگر یا پیوندی به فایلی در فضای ابری باشد.',
'attachments_link_name' => 'نام پیوند',
'attachment_link' => 'لینک پیوست',
'attachments_link_url' => 'پیوند به فایل',
'attachments_link_url_hint' => 'آدرس سایت یا فایل',
'attach' => 'ضمیمه کنید',
'attachments_insert_link' => 'پیوند پیوست را به صفحه اضافه کنید',
'attachments_edit_file' => 'ویرایش فایل',
'attachments_edit_file_name' => 'نام فایل',
'attachments_edit_drop_upload' => 'فایل ها را رها کنید یا برای آپلود و بازنویسی اینجا کلیک کنید',
'attachments_order_updated' => 'سفارش پیوست به روز شد',
'attachments_updated_success' => 'جزئیات پیوست به روز شد',
'attachments_deleted' => 'پیوست حذف شد',
'attachments_file_uploaded' => 'فایل با موفقیت آپلود شد',
'attachments_file_updated' => 'فایل با موفقیت به روز شد',
'attachments_link_attached' => 'پیوند با موفقیت به صفحه پیوست شد',
'templates' => 'قالب ها',
'templates_set_as_template' => 'صفحه یک الگو است',
'templates_explain_set_as_template' => 'می توانید این صفحه را به عنوان یک الگو تنظیم کنید تا از محتویات آن هنگام ایجاد صفحات دیگر استفاده شود. سایر کاربران در صورت داشتن مجوز مشاهده برای این صفحه می توانند از این الگو استفاده کنند.',
'templates_replace_content' => 'محتوای صفحه را جایگزین کنید',
'templates_append_content' => 'به محتوای صفحه اضافه کنید',
'templates_prepend_content' => 'به محتوای صفحه اضافه کنید',
// Profile View
'profile_user_for_x' => 'User for :time',
'profile_created_content' => 'Created Content',
'profile_not_created_pages' => ':userName has not created any pages',
'profile_not_created_chapters' => ':userName has not created any chapters',
'profile_not_created_books' => ':userName has not created any books',
'profile_not_created_shelves' => ':userName has not created any shelves',
'profile_user_for_x' => 'کاربر برای :time',
'profile_created_content' => 'محتوا ایجاد کرد',
'profile_not_created_pages' => ':userName هیچ صفحه ای ایجاد نکرده است',
'profile_not_created_chapters' => ':userName هیچ فصلی ایجاد نکرده است',
'profile_not_created_books' => ':userName هیچ کتابی ایجاد نکرده است',
'profile_not_created_shelves' => ':userName هیچ قفسه ای ایجاد نکرده است',
// Comments
'comment' => 'Comment',
'comments' => 'Comments',
'comment_add' => 'Add Comment',
'comment_placeholder' => 'Leave a comment here',
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
'comment_save' => 'Save Comment',
'comment_saving' => 'Saving comment...',
'comment_deleting' => 'Deleting comment...',
'comment_new' => 'New Comment',
'comment_created' => 'commented :createDiff',
'comment_updated' => 'Updated :updateDiff by :username',
'comment_deleted_success' => 'Comment deleted',
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
'comment' => 'اظهار نظر',
'comments' => 'نظرات',
'comment_add' => 'افزودن توضیح',
'comment_placeholder' => 'اینجا نظر بدهید',
'comment_count' => '{0} بدون نظر|{1} 1 نظر|[2,*] :count نظرات',
'comment_save' => 'ذخیره نظر',
'comment_saving' => 'در حال ذخیره نظر...',
'comment_deleting' => 'در حال حذف نظر...',
'comment_new' => 'نظر جدید',
'comment_created' => ':createDiff نظر داد',
'comment_updated' => 'به روز رسانی :updateDiff توسط :username',
'comment_deleted_success' => 'نظر حذف شد',
'comment_created_success' => 'نظر اضافه شد',
'comment_updated_success' => 'نظر به روز شد',
'comment_delete_confirm' => 'آیا مطمئن هستید که می خواهید این نظر را حذف کنید؟',
'comment_in_reply_to' => 'در پاسخ به :commentId',
// Revision
'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
'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.',
'revision_delete_confirm' => 'آیا مطمئن هستید که می خواهید این ویرایش را حذف کنید؟',
'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.',
'copy_consider' => 'لطفاً هنگام کپی کردن مطالب به موارد زیر توجه کنید.',
'copy_consider_permissions' => 'تنظیمات مجوز سفارشی کپی نخواهد شد.',
'copy_consider_owner' => 'شما مالک تمام محتوای کپی شده خواهید شد.',
'copy_consider_images' => 'فایل های تصویر صفحه تکراری نخواهند شد و تصاویر اصلی ارتباط خود را با صفحه ای که در ابتدا در آن آپلود شده اند حفظ می کنند.',
'copy_consider_attachments' => 'پیوست های صفحه کپی نمی شود.',
'copy_consider_access' => 'تغییر مکان، مالک یا مجوزها ممکن است منجر به دسترسی به این محتوا برای افرادی شود که قبلاً به آنها دسترسی نداشتند.',
];

View File

@@ -23,10 +23,10 @@ return [
'saml_no_email_address' => 'آدرس داده ای برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد',
'saml_invalid_response_id' => 'درخواست از سیستم احراز هویت خارجی توسط فرایندی که توسط این نرم افزار آغاز شده است شناخته نمی شود. بازگشت به سیستم پس از ورود به سیستم می تواند باعث این مسئله شود.',
'saml_fail_authed' => 'ورود به سیستم :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد',
'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'oidc_already_logged_in' => 'قبلا وارد شده اید',
'oidc_user_not_registered' => 'کاربر :name ثبت نشده و ثبت نام خودکار غیرفعال است',
'oidc_no_email_address' => 'آدرس ایمیلی برای این کاربر در داده های ارائه شده توسط سیستم احراز هویت خارجی یافت نشد',
'oidc_fail_authed' => 'ورود به سیستم با استفاده از :system انجام نشد، سیستم مجوز موفقیت آمیز ارائه نکرد',
'social_no_action_defined' => 'عملی تعریف نشده است',
'social_login_bad_response' => "خطای دریافت شده در هنگام ورود به سیستم:\n:error",
'social_account_in_use' => 'این حساب :socialAccount از قبل در حال استفاده است، سعی کنید از طریق گزینه :socialAccount وارد سیستم شوید.',
@@ -37,73 +37,73 @@ return [
'social_account_register_instructions' => 'اگر هنوز حساب کاربری ندارید ، می توانید با استفاده از گزینه :socialAccount حساب خود را ثبت کنید.',
'social_driver_not_found' => 'درایور شبکه اجتماعی یافت نشد',
'social_driver_not_configured' => 'تنظیمات شبکه اجتماعی :socialAccount به درستی پیکربندی نشده است.',
'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
'invite_token_expired' => 'این پیوند دعوت منقضی شده است. در عوض می توانید سعی کنید رمز عبور حساب خود را بازنشانی کنید.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Cannot get image from :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'uploaded' => 'The server does not allow uploads of this size. Please try a smaller file size.',
'image_upload_error' => 'An error occurred uploading the image',
'image_upload_type_error' => 'The image type being uploaded is invalid',
'file_upload_timeout' => 'The file upload has timed out.',
'path_not_writable' => 'مسیر فایل :filePath را نمی توان در آن آپلود کرد. مطمئن شوید که روی سرور قابل نوشتن است.',
'cannot_get_image_from_url' => 'نمی توان تصویر را از :url دریافت کرد',
'cannot_create_thumbs' => 'سرور نمی تواند تصاویر کوچک ایجاد کند. لطفاً بررسی کنید که پسوند GD PHP را نصب کرده اید.',
'server_upload_limit' => 'سرور اجازه آپلود در این اندازه را نمی دهد. لطفا اندازه فایل کوچکتر را امتحان کنید.',
'uploaded' => 'سرور اجازه آپلود در این اندازه را نمی دهد. لطفا اندازه فایل کوچکتر را امتحان کنید.',
'image_upload_error' => 'هنگام آپلود تصویر خطایی روی داد',
'image_upload_type_error' => 'نوع تصویر در حال آپلود نامعتبر است',
'file_upload_timeout' => 'زمان بارگذاری فایل به پایان رسیده است.',
// Attachments
'attachment_not_found' => 'Attachment not found',
'attachment_not_found' => 'پیوست یافت نشد',
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
'page_draft_autosave_fail' => 'پیش نویس ذخیره نشد. قبل از ذخیره این صفحه مطمئن شوید که به اینترنت متصل هستید',
'page_custom_home_deletion' => 'وقتی صفحه ای به عنوان صفحه اصلی تنظیم شده است، نمی توان آن را حذف کرد',
// Entities
'entity_not_found' => 'Entity not found',
'bookshelf_not_found' => 'Bookshelf not found',
'book_not_found' => 'Book not found',
'page_not_found' => 'Page not found',
'chapter_not_found' => 'Chapter not found',
'selected_book_not_found' => 'The selected book was not found',
'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
'guests_cannot_save_drafts' => 'Guests cannot save drafts',
'entity_not_found' => 'موجودیت یافت نشد',
'bookshelf_not_found' => 'قفسه کتاب پیدا نشد',
'book_not_found' => 'کتاب پیدا نشد',
'page_not_found' => 'صفحه یافت نشد',
'chapter_not_found' => 'فصل پیدا نشد',
'selected_book_not_found' => 'کتاب انتخابی یافت نشد',
'selected_book_chapter_not_found' => 'کتاب یا فصل انتخابی یافت نشد',
'guests_cannot_save_drafts' => 'مهمانان نمی توانند پیش نویس ها را ذخیره کنند',
// Users
'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
'users_cannot_delete_guest' => 'You cannot delete the guest user',
'users_cannot_delete_only_admin' => 'شما نمی توانید تنها ادمین را حذف کنید',
'users_cannot_delete_guest' => 'شما نمی توانید کاربر مهمان را حذف کنید',
// Roles
'role_cannot_be_edited' => 'This role cannot be edited',
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',
'role_cannot_be_edited' => 'این نقش قابل ویرایش نیست',
'role_system_cannot_be_deleted' => 'این نقش یک نقش سیستمی است و قابل حذف نیست',
'role_registration_default_cannot_delete' => 'این نقش در حالی که به عنوان نقش پیش فرض ثبت نام تنظیم شده است قابل حذف نیست',
'role_cannot_remove_only_admin' => 'این کاربر تنها کاربری است که به نقش مدیر اختصاص داده شده است. قبل از تلاش برای حذف آن در اینجا، نقش مدیر را به کاربر دیگری اختصاص دهید.',
// Comments
'comment_list' => 'An error occurred while fetching the comments.',
'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',
'comment_add' => 'An error occurred while adding / updating the comment.',
'comment_delete' => 'An error occurred while deleting the comment.',
'empty_comment' => 'Cannot add an empty comment.',
'comment_list' => 'هنگام واکشی نظرات خطایی روی داد.',
'cannot_add_comment_to_draft' => 'شما نمی توانید نظراتی را به یک پیش نویس اضافه کنید.',
'comment_add' => 'هنگام افزودن/به‌روزرسانی نظر خطایی روی داد.',
'comment_delete' => 'هنگام حذف نظر خطایی روی داد.',
'empty_comment' => 'نمی توان یک نظر خالی اضافه کرد.',
// Error pages
'404_page_not_found' => 'Page Not Found',
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'return_home' => 'Return to home',
'error_occurred' => 'An Error Occurred',
'app_down' => ':appName is down right now',
'back_soon' => 'It will be back up soon.',
'404_page_not_found' => 'صفحه یافت نشد',
'sorry_page_not_found' => 'با عرض پوزش، صفحه مورد نظر شما یافت نشد.',
'sorry_page_not_found_permission_warning' => 'اگر انتظار داشتید این صفحه وجود داشته باشد، ممکن است اجازه مشاهده آن را نداشته باشید.',
'image_not_found' => 'تصویر پیدا نشد',
'image_not_found_subtitle' => 'با عرض پوزش، فایل تصویری که به دنبال آن بودید یافت نشد.',
'image_not_found_details' => 'اگر انتظار داشتید این تصویر وجود داشته باشد، ممکن است حذف شده باشد.',
'return_home' => 'بازگشت به خانه',
'error_occurred' => 'خطایی رخ داد',
'app_down' => ':appName در حال حاضر قطع است',
'back_soon' => 'به زودی پشتیبان خواهد شد.',
// API errors
'api_no_authorization_found' => 'No authorization token found on the request',
'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',
'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',
'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',
'api_user_token_expired' => 'The authorization token used has expired',
'api_no_authorization_found' => 'هیچ نشانه مجوزی در درخواست یافت نشد',
'api_bad_authorization_format' => 'یک نشانه مجوز در این درخواست یافت شد اما قالب نادرست به نظر می‌رسید',
'api_user_token_not_found' => 'هیچ نشانه API منطبقی برای کد مجوز ارائه شده یافت نشد',
'api_incorrect_token_secret' => 'راز ارائه شده برای کد API استفاده شده نادرست است',
'api_user_no_api_permission' => 'مالک نشانه API استفاده شده اجازه برقراری تماس های API را ندارد',
'api_user_token_expired' => 'رمز مجوز استفاده شده منقضی شده است',
// Settings & Maintenance
'maintenance_test_email_failure' => 'Error thrown when sending a test email:',
'maintenance_test_email_failure' => 'خطا در هنگام ارسال ایمیل آزمایشی:',
];

View File

@@ -7,258 +7,258 @@
return [
// Common Messages
'settings' => 'Settings',
'settings_save' => 'Save Settings',
'settings_save_success' => 'Settings saved',
'settings' => 'تنظیمات',
'settings_save' => 'تنظیمات را ذخیره کن',
'settings_save_success' => 'تنظیمات ذخیره شد',
// App Settings
'app_customization' => 'Customization',
'app_features_security' => 'Features & Security',
'app_name' => 'Application Name',
'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',
'app_name_header' => 'Show name in header',
'app_public_access' => 'Public Access',
'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',
'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the "Guest" user.',
'app_public_access_toggle' => 'Allow public access',
'app_public_viewing' => 'Allow public viewing?',
'app_secure_images' => 'Higher Security Image Uploads',
'app_secure_images_toggle' => 'Enable higher security image uploads',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
'app_editor' => 'Page Editor',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
'app_custom_html' => 'Custom HTML Head Content',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
'app_logo' => 'Application Logo',
'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
'app_primary_color' => 'Application Primary Color',
'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.',
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_select' => 'Select a page',
'app_footer_links' => 'Footer Links',
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
'app_footer_links_label' => 'Link Label',
'app_footer_links_url' => 'Link URL',
'app_footer_links_add' => 'Add Footer Link',
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
'app_customization' => 'سفارشی سازی',
'app_features_security' => 'ویژگی ها و امنیت',
'app_name' => 'نام نرم افزار',
'app_name_desc' => 'این نام در هدر و در هر ایمیل ارسال شده توسط سیستم نشان داده شده است.',
'app_name_header' => 'نمایش نام در هدر',
'app_public_access' => 'دسترسی عمومی',
'app_public_access_desc' => 'فعال کردن این گزینه به بازدیدکنندگانی که وارد سیستم نشده‌اند اجازه می‌دهد تا به محتوای موجود در نمونه BookStack شما دسترسی داشته باشند.',
'app_public_access_desc_guest' => 'دسترسی بازدیدکنندگان عمومی را می توان از طریق کاربر "مهمان" کنترل کرد.',
'app_public_access_toggle' => 'اجازه دسترسی عمومی',
'app_public_viewing' => 'مشاهده عمومی مجاز است؟',
'app_secure_images' => 'آپلود تصویر با امنیت بالاتر',
'app_secure_images_toggle' => 'آپلود تصویر با امنیت بالاتر',
'app_secure_images_desc' => 'به دلایل عملکرد، همه تصاویر عمومی هستند. این گزینه یک رشته تصادفی و غیرقابل حدس زدن را در مقابل آدرس های تصویر اضافه می کند. برای جلوگیری از دسترسی آسان، اطمینان حاصل کنید که فهرست های دایرکتوری فعال نیستند.',
'app_editor' => 'ویرایشگر صفحه',
'app_editor_desc' => 'انتخاب کنید کدام ویرایشگر توسط همه کاربران برای ویرایش صفحات استفاده شود.',
'app_custom_html' => 'محتوای اصلی HTML سفارشی',
'app_custom_html_desc' => 'هر محتوای اضافه شده در اینجا در پایین بخش <head> هر صفحه درج می شود. این برای تغییر سبک ها یا اضافه کردن کد تجزیه و تحلیل مفید است.',
'app_custom_html_disabled_notice' => 'محتوای سر HTML سفارشی در این صفحه تنظیمات غیرفعال است تا اطمینان حاصل شود که هر گونه تغییر شکسته می تواند برگردانده شود.',
'app_logo' => 'لوگوی برنامه',
'app_logo_desc' => 'این تصویر باید 43 پیکسل ارتفاع داشته باشد. <br>تصاویر بزرگ کوچک می شوند.',
'app_primary_color' => 'رنگ اصلی برنامه',
'app_primary_color_desc' => 'رنگ اصلی برنامه را از جمله بنر، دکمه ها و پیوندها تنظیم می کند.',
'app_homepage' => 'صفحه اصلی برنامه',
'app_homepage_desc' => 'به جای نمای پیش‌فرض، یک نمای را برای نمایش در صفحه اصلی انتخاب کنید. مجوزهای صفحه برای صفحات انتخابی نادیده گرفته می شود.',
'app_homepage_select' => 'یک صفحه را انتخاب کنید',
'app_footer_links' => 'پیوندهای پاورقی',
'app_footer_links_desc' => 'پیوندهایی را برای نمایش در پاورقی سایت اضافه کنید. اینها در پایین اکثر صفحات نمایش داده می شوند، از جمله صفحاتی که نیازی به ورود به سیستم ندارند. می توانید از برچسب "trans::<key>" برای استفاده از ترجمه های تعریف شده توسط سیستم استفاده کنید. به عنوان مثال: با استفاده از "trans::common.privacy_policy" متن ترجمه شده "خط مشی رازداری" و "trans::common.terms_of_service" متن ترجمه شده "شرایط خدمات" را ارائه می دهد.',
'app_footer_links_label' => 'برچسب پیوند',
'app_footer_links_url' => 'لینک URL',
'app_footer_links_add' => 'پیوند پاورقی را اضافه کنید',
'app_disable_comments' => 'غیرفعال کردن نظرات',
'app_disable_comments_toggle' => 'نظرات را غیرفعال کنید',
'app_disable_comments_desc' => 'نظرات را در تمام صفحات برنامه غیرفعال می کند. <br> نظرات موجود نشان داده نمی شوند.',
// Color settings
'content_colors' => 'Content Colors',
'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
'bookshelf_color' => 'Shelf Color',
'book_color' => 'Book Color',
'chapter_color' => 'Chapter Color',
'page_color' => 'Page Color',
'page_draft_color' => 'Page Draft Color',
'content_colors' => 'رنگ های محتوا',
'content_colors_desc' => 'رنگ ها را برای همه عناصر در سلسله مراتب سازمان صفحه تنظیم می کند. برای خوانایی، انتخاب رنگ هایی با روشنایی مشابه با رنگ های پیش فرض توصیه می شود.',
'bookshelf_color' => 'رنگ قفسه',
'book_color' => 'رنگ کتاب',
'chapter_color' => 'رنگ فصل',
'page_color' => 'رنگ صفحه',
'page_draft_color' => 'رنگ پیش نویس صفحه',
// Registration Settings
'reg_settings' => 'Registration',
'reg_enable' => 'Enable Registration',
'reg_enable_toggle' => 'Enable registration',
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
'reg_default_role' => 'Default user role after registration',
'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
'reg_email_confirmation' => 'Email Confirmation',
'reg_email_confirmation_toggle' => 'Require email confirmation',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
'reg_confirm_restrict_domain' => 'Domain Restriction',
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
'reg_settings' => 'ثبت نام',
'reg_enable' => 'فعال کردن ثبت نام',
'reg_enable_toggle' => 'فعال کردن ثبت نام',
'reg_enable_desc' => 'هنگامی که ثبت نام فعال باشد، کاربر می تواند خود را به عنوان کاربر برنامه ثبت نام کند. پس از ثبت نام به آنها یک نقش کاربر پیش فرض داده می شود.',
'reg_default_role' => 'نقش کاربر پیش فرض پس از ثبت نام',
'reg_enable_external_warning' => 'هنگامی که احراز هویت خارجی LDAP یا SAML فعال است، گزینه بالا نادیده گرفته می شود. در صورتی که احراز هویت، در برابر سیستم خارجی در حال استفاده، موفقیت آمیز باشد، حساب های کاربری برای اعضای غیر موجود به طور خودکار ایجاد می شود.',
'reg_email_confirmation' => 'تایید ایمیل',
'reg_email_confirmation_toggle' => 'نیاز به تایید ایمیل',
'reg_confirm_email_desc' => 'در صورت استفاده از محدودیت دامنه، تایید ایمیل مورد نیاز است و این گزینه نادیده گرفته می شود.',
'reg_confirm_restrict_domain' => 'محدودیت دامنه',
'reg_confirm_restrict_domain_desc' => 'فهرستی از دامنه‌های ایمیل جدا شده با کاما را وارد کنید که می‌خواهید ثبت نام را محدود کنید. قبل از اینکه به کاربران اجازه تعامل با برنامه داده شود، ایمیلی برای تأیید آدرس آنها ارسال می شود. <br> توجه داشته باشید که کاربران پس از ثبت نام موفق می توانند آدرس ایمیل خود را تغییر دهند.',
'reg_confirm_restrict_domain_placeholder' => 'بدون محدودیت',
// Maintenance settings
'maint' => 'Maintenance',
'maint_image_cleanup' => 'Cleanup Images',
'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',
'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
'maint_image_cleanup_run' => 'Run Cleanup',
'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
'maint_send_test_email' => 'Send a Test Email',
'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
'maint_send_test_email_run' => 'Send test email',
'maint_send_test_email_success' => 'Email sent to :address',
'maint_send_test_email_mail_subject' => 'Test Email',
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
'maint_recycle_bin_open' => 'Open Recycle Bin',
'maint' => 'نگهداری',
'maint_image_cleanup' => 'پاکسازی تصاویر',
'maint_image_cleanup_desc' => 'محتوای صفحه و بازبینی را اسکن می‌کند تا بررسی کند که کدام تصاویر و نقاشی‌ها در حال حاضر استفاده می‌شوند و کدام تصاویر اضافی هستند. قبل از اجرای این کار، مطمئن شوید که یک پایگاه داده کامل و یک نسخه پشتیبان از تصویر ایجاد کرده اید.',
'maint_delete_images_only_in_revisions' => 'همچنین تصاویری را که فقط در ویرایش های صفحه قدیمی وجود دارند حذف کنید',
'maint_image_cleanup_run' => 'پاکسازی را اجرا کنید',
'maint_image_cleanup_warning' => ':count تصاویر بالقوه استفاده نشده پیدا شد. آیا مطمئن هستید که می خواهید این تصاویر را حذف کنید؟',
'maint_image_cleanup_success' => ':count تصویر بالقوه استفاده نشده پیدا و حذف شد!',
'maint_image_cleanup_nothing_found' => 'هیچ تصویر استفاده نشده ای یافت نشد، چیزی حذف نشد!',
'maint_send_test_email' => 'یک ایمیل آزمایشی ارسال کنید',
'maint_send_test_email_desc' => 'این یک ایمیل آزمایشی به آدرس ایمیل شما مشخص شده در نمایه شما ارسال می کند.',
'maint_send_test_email_run' => 'ارسال ایمیل آزمایشی',
'maint_send_test_email_success' => 'ایمیل به آدرس :address ارسال شد',
'maint_send_test_email_mail_subject' => 'تست ایمیل',
'maint_send_test_email_mail_greeting' => 'به نظر می رسد تحویل ایمیل کار می کند!',
'maint_send_test_email_mail_text' => 'تبریک می گویم! با دریافت این اعلان ایمیل، به نظر می رسد تنظیمات ایمیل شما به درستی پیکربندی شده است.',
'maint_recycle_bin_desc' => 'قفسه‌ها، کتاب‌ها، فصل‌ها و صفحات حذف‌شده به سطل بازیافت فرستاده می‌شوند تا بتوان آن‌ها را بازیابی کرد یا برای همیشه حذف کرد. بسته به پیکربندی سیستم، اقلام قدیمی در سطل بازیافت ممکن است پس از مدتی به طور خودکار حذف شوند.',
'maint_recycle_bin_open' => 'سطل بازیافت را باز کنید',
// Recycle Bin
'recycle_bin' => 'Recycle Bin',
'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'recycle_bin_deleted_item' => 'Deleted Item',
'recycle_bin_deleted_parent' => 'Parent',
'recycle_bin_deleted_by' => 'Deleted By',
'recycle_bin_deleted_at' => 'Deletion Time',
'recycle_bin_permanently_delete' => 'Permanently Delete',
'recycle_bin_restore' => 'Restore',
'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
'recycle_bin_empty' => 'Empty Recycle Bin',
'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',
'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?',
'recycle_bin_destroy_list' => 'Items to be Destroyed',
'recycle_bin_restore_list' => 'Items to be Restored',
'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',
'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',
'recycle_bin_restore_parent' => 'Restore Parent',
'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
'recycle_bin' => 'سطل زباله',
'recycle_bin_desc' => 'در اینجا می توانید مواردی را که حذف شده اند بازیابی کنید یا حذف دائمی آنها را از سیستم انتخاب کنید. این لیست برخلاف لیست‌های فعالیت مشابه در سیستمی که فیلترهای مجوز اعمال می‌شوند، فیلتر نشده است.',
'recycle_bin_deleted_item' => 'مورد حذف شده',
'recycle_bin_deleted_parent' => 'والد',
'recycle_bin_deleted_by' => 'حذف شده توسط',
'recycle_bin_deleted_at' => 'زمان حذف',
'recycle_bin_permanently_delete' => 'برای همیشه حذف کنید',
'recycle_bin_restore' => 'بازگرداندن',
'recycle_bin_contents_empty' => 'سطل بازیافت در حال حاضر خالی است',
'recycle_bin_empty' => 'سطل آشغال خالی',
'recycle_bin_empty_confirm' => 'این کار همه اقلام موجود در سطل بازیافت از جمله محتوای موجود در هر مورد را برای همیشه از بین می برد. آیا مطمئن هستید که می خواهید سطل بازیافت را خالی کنید؟',
'recycle_bin_destroy_confirm' => 'این اقدام این مورد را به همراه هر عنصر فرعی فهرست شده در زیر برای همیشه از سیستم حذف می کند و شما نمی توانید این محتوا را بازیابی کنید. آیا مطمئن هستید که می خواهید این مورد را برای همیشه حذف کنید؟',
'recycle_bin_destroy_list' => 'مواردی که باید نابود شوند',
'recycle_bin_restore_list' => 'مواردی که باید بازیابی شوند',
'recycle_bin_restore_confirm' => 'این اقدام، مورد حذف شده، از جمله هر عنصر فرزند، را به مکان اصلی خود باز می گرداند. اگر مکان اصلی از آن زمان حذف شده باشد، و اکنون در سطل بازیافت است، مورد اصلی نیز باید بازیابی شود.',
'recycle_bin_restore_deleted_parent' => 'والد این مورد نیز حذف شده است. تا زمانی که آن والد نیز بازیابی نشود، این موارد حذف خواهند شد.',
'recycle_bin_restore_parent' => 'بازیابی والد',
'recycle_bin_destroy_notification' => ':count تعداد از کل اقلام از سطل بازیافت حذف شده.',
'recycle_bin_restore_notification' => ':count تعداد از کل اقلام از سطل بازیافت بازیابی شده.',
// Audit Log
'audit' => 'Audit Log',
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'audit_event_filter' => 'Event Filter',
'audit_event_filter_no_filter' => 'No Filter',
'audit_deleted_item' => 'Deleted Item',
'audit_deleted_item_name' => 'Name: :name',
'audit_table_user' => 'User',
'audit_table_event' => 'Event',
'audit_table_related' => 'Related Item or Detail',
'audit_table_ip' => 'IP Address',
'audit_table_date' => 'Activity Date',
'audit_date_from' => 'Date Range From',
'audit_date_to' => 'Date Range To',
'audit' => 'گزارش حسابرسی',
'audit_desc' => 'این گزارش حسابرسی لیستی از فعالیت های ردیابی شده در سیستم را نمایش می دهد. این لیست برخلاف لیست‌های فعالیت مشابه در سیستمی که فیلترهای مجوز اعمال می‌شوند، فیلتر نشده است.',
'audit_event_filter' => 'فیلتر رویداد',
'audit_event_filter_no_filter' => 'بدون فیلتر',
'audit_deleted_item' => 'مورد حذف شده',
'audit_deleted_item_name' => 'نام :name',
'audit_table_user' => 'نام كاربري',
'audit_table_event' => 'رویداد',
'audit_table_related' => 'مورد یا جزئیات مرتبط',
'audit_table_ip' => 'آدرس IP',
'audit_table_date' => 'تاریخ‌های فعالیت',
'audit_date_from' => 'محدوده تاریخ از',
'audit_date_to' => 'محدوده تاریخ تا',
// Role Settings
'roles' => 'Roles',
'role_user_roles' => 'User Roles',
'role_create' => 'Create New Role',
'role_create_success' => 'Role successfully created',
'role_delete' => 'Delete Role',
'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
'role_delete_no_migration' => "Don't migrate users",
'role_delete_sure' => 'Are you sure you want to delete this role?',
'role_delete_success' => 'Role successfully deleted',
'role_edit' => 'Edit Role',
'role_details' => 'Role Details',
'role_name' => 'Role Name',
'role_desc' => 'Short Description of Role',
'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
'role_external_auth_id' => 'External Authentication IDs',
'role_system' => 'System Permissions',
'role_manage_users' => 'Manage users',
'role_manage_roles' => 'Manage roles & role permissions',
'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
'role_manage_page_templates' => 'Manage page templates',
'role_access_api' => 'Access system API',
'role_manage_settings' => 'Manage app settings',
'role_export_content' => 'Export content',
'role_asset' => 'Asset Permissions',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
'role_all' => 'All',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Save Role',
'role_update_success' => 'Role successfully updated',
'role_users' => 'Users in this role',
'role_users_none' => 'No users are currently assigned to this role',
'roles' => 'نقش ها',
'role_user_roles' => 'نقش های کاربر',
'role_create' => 'نقش جدید ایجاد کنید',
'role_create_success' => 'نقش با موفقیت ایجاد شد',
'role_delete' => 'حذف نقش',
'role_delete_confirm' => 'با این کار نقش با نام \':roleName\' حذف می شود.',
'role_delete_users_assigned' => 'این نقش دارای :userCount کاربرانی است که به آن اختصاص داده شده است. اگر می خواهید کاربران را از این نقش مهاجرت کنید، نقش جدیدی را در زیر انتخاب کنید.',
'role_delete_no_migration' => "کاربران را منتقل نکنید",
'role_delete_sure' => 'آیا مطمئنید که می خواهید این نقش را حذف کنید؟',
'role_delete_success' => 'نقش با موفقیت حذف شد',
'role_edit' => 'ویرایش نقش',
'role_details' => 'جزئیات نقش',
'role_name' => 'اسم نقش',
'role_desc' => 'شرح کوتاه نقش',
'role_mfa_enforced' => 'به احراز هویت چند عاملی نیاز دارد',
'role_external_auth_id' => 'شناسه های تأیید هویت خارجی',
'role_system' => 'مجوزهای سیستم',
'role_manage_users' => 'مدیریت کاربران',
'role_manage_roles' => 'نقش ها و مجوزهای نقش را مدیریت کنید',
'role_manage_entity_permissions' => 'تمام مجوزهای کتاب، فصل و صفحه را مدیریت کنید',
'role_manage_own_entity_permissions' => 'مجوزها را در کتاب، فصل و صفحات خود مدیریت کنید',
'role_manage_page_templates' => 'مدیریت قالب های صفحه',
'role_access_api' => 'دسترسی به API سیستم',
'role_manage_settings' => 'تنظیمات برنامه را مدیریت کنید',
'role_export_content' => 'صادرات محتوا',
'role_asset' => 'مجوزهای دارایی',
'roles_system_warning' => 'توجه داشته باشید که دسترسی به هر یک از سه مجوز فوق می‌تواند به کاربر اجازه دهد تا امتیازات خود یا امتیازات دیگران را در سیستم تغییر دهد. فقط نقش هایی را با این مجوزها به کاربران مورد اعتماد اختصاص دهید.',
'role_asset_desc' => 'این مجوزها دسترسی پیش‌فرض به دارایی‌های درون سیستم را کنترل می‌کنند. مجوزهای مربوط به کتاب‌ها، فصل‌ها و صفحات این مجوزها را لغو می‌کنند.',
'role_asset_admins' => 'به ادمین‌ها به‌طور خودکار به همه محتوا دسترسی داده می‌شود، اما این گزینه‌ها ممکن است گزینه‌های UI را نشان داده یا پنهان کنند.',
'role_all' => 'همه',
'role_own' => 'صاحب',
'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود',
'role_save' => 'ذخیره نقش',
'role_update_success' => 'نقش با موفقیت به روز شد',
'role_users' => 'کاربران در این نقش',
'role_users_none' => 'در حال حاضر هیچ کاربری به این نقش اختصاص داده نشده است',
// Users
'users' => 'Users',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
'users_latest_activity' => 'Latest Activity',
'users_details' => 'User Details',
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
'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_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',
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
'users_password_warning' => 'Only fill the below if you would like to change your password.',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
'users_migrate_ownership' => 'Migrate Ownership',
'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
'users_none_selected' => 'No user selected',
'users_delete_success' => 'User successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
'users_avatar' => 'User Avatar',
'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',
'users_preferred_language' => 'Preferred Language',
'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
'users_social_accounts' => 'Social Accounts',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
'users_social_connect' => 'Connect Account',
'users_social_disconnect' => 'Disconnect Account',
'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
'users_api_tokens' => 'API Tokens',
'users_api_tokens_none' => 'No API tokens have been created for this user',
'users_api_tokens_create' => 'Create Token',
'users_api_tokens_expires' => 'Expires',
'users_api_tokens_docs' => 'API Documentation',
'users_mfa' => 'Multi-Factor Authentication',
'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
'users_mfa_x_methods' => ':count method configured|:count methods configured',
'users_mfa_configure' => 'Configure Methods',
'users' => 'کاربران',
'user_profile' => 'پرونده کاربر',
'users_add_new' => 'افزودن کاربر جدید',
'users_search' => 'جستجوی کاربران',
'users_latest_activity' => 'آخرین فعالیت',
'users_details' => 'جزئیات کاربر',
'users_details_desc' => 'یک نام نمایشی و یک آدرس ایمیل برای این کاربر تنظیم کنید. آدرس ایمیل برای ورود به برنامه استفاده خواهد شد.',
'users_details_desc_no_email' => 'یک نام نمایشی برای این کاربر تنظیم کنید تا دیگران بتوانند آنها را تشخیص دهند.',
'users_role' => 'نقش های کاربر',
'users_role_desc' => 'انتخاب کنید که این کاربر به کدام نقش ها اختصاص داده شود. اگر یک کاربر به چندین نقش اختصاص داده شود، مجوزهای آن نقش‌ها روی هم قرار می‌گیرند و تمام توانایی‌های نقش‌های اختصاص داده شده را دریافت خواهند کرد.',
'users_password' => 'رمز عبور كاربر',
'users_password_desc' => 'رمز عبوری را که برای ورود به برنامه استفاده می شود تنظیم کنید. این باید حداقل 8 کاراکتر باشد.',
'users_send_invite_text' => 'می توانید انتخاب کنید که برای این کاربر یک ایمیل دعوت نامه ارسال شود که به آنها امکان می دهد رمز عبور خود را تعیین کنند در غیر این صورت می توانید رمز عبور خود را تعیین کنید.',
'users_send_invite_option' => 'ارسال ایمیل دعوت کاربر',
'users_external_auth_id' => 'شناسه احراز هویت خارجی',
'users_external_auth_id_desc' => 'این شناسه ای است که برای مطابقت با این کاربر هنگام برقراری ارتباط با سیستم احراز هویت خارجی شما استفاده می شود.',
'users_password_warning' => 'فقط در صورتی که مایل به تغییر رمز عبور خود هستید، موارد زیر را پر کنید.',
'users_system_public' => 'این کاربر نماینده هر کاربر مهمانی است که از نمونه شما بازدید می کند. نمی توان از آن برای ورود استفاده کرد اما به طور خودکار اختصاص داده می شود.',
'users_delete' => 'حذف کاربر',
'users_delete_named' => 'حذف :userName',
'users_delete_warning' => 'با این کار این کاربر با نام \':userName\' به طور کامل از سیستم حذف می شود.',
'users_delete_confirm' => 'آیا مطمئن هستید که می خواهید این کاربر را حذف کنید؟',
'users_migrate_ownership' => 'انتقال مالکیت',
'users_migrate_ownership_desc' => 'اگر می‌خواهید کاربر دیگری مالک همه مواردی باشد که در حال حاضر متعلق به این کاربر است، کاربری را در اینجا انتخاب کنید.',
'users_none_selected' => 'هیچ کاربری انتخاب نشد',
'users_delete_success' => 'کاربر با موفقیت حذف شد',
'users_edit' => 'ویرایش کاربر',
'users_edit_profile' => 'ویرایش پروفایل',
'users_edit_success' => 'کاربر با موفقیت به روز شد',
'users_avatar' => 'آواتار کاربر',
'users_avatar_desc' => 'تصویری را برای نشان دادن این کاربر انتخاب کنید. این باید تقریباً 256 پیکسل مربع باشد.',
'users_preferred_language' => 'زبان ترجیحی',
'users_preferred_language_desc' => 'این گزینه زبان مورد استفاده برای رابط کاربری برنامه را تغییر می دهد. این روی محتوای ایجاد شده توسط کاربر تأثیری نخواهد داشت.',
'users_social_accounts' => 'حساب های اجتماعی',
'users_social_accounts_info' => 'در اینجا می‌توانید حساب‌های دیگر خود را برای ورود سریع‌تر و آسان‌تر متصل کنید. قطع ارتباط حساب در اینجا، دسترسی مجاز قبلی را لغو نمی کند. دسترسی را از تنظیمات نمایه خود در حساب اجتماعی متصل لغو کنید.',
'users_social_connect' => 'اتصال حساب کاربری',
'users_social_disconnect' => 'قطع حساب',
'users_social_connected' => 'حساب :socialAccount با موفقیت به نمایه شما پیوست شد.',
'users_social_disconnected' => 'حساب :socialAccount با موفقیت از نمایه شما قطع شد.',
'users_api_tokens' => 'توکن‌های API',
'users_api_tokens_none' => 'هیچ نشانه API برای این کاربر ایجاد نشده است',
'users_api_tokens_create' => 'ایجاد توکن',
'users_api_tokens_expires' => 'منقضی شده ها',
'users_api_tokens_docs' => 'مستندات API',
'users_mfa' => 'احراز هویت چند عاملی',
'users_mfa_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.',
'users_mfa_x_methods' => ':count روش پیکربندی شده است|:count روش های پیکربندی شده',
'users_mfa_configure' => 'روش پیکربندی',
// API Tokens
'user_api_token_create' => 'Create API Token',
'user_api_token_name' => 'Name',
'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
'user_api_token_expiry' => 'Expiry Date',
'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
'user_api_token_create_success' => 'API token successfully created',
'user_api_token_update_success' => 'API token successfully updated',
'user_api_token' => 'API Token',
'user_api_token_id' => 'Token ID',
'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
'user_api_token_secret' => 'Token Secret',
'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
'user_api_token_created' => 'Token created :timeAgo',
'user_api_token_updated' => 'Token updated :timeAgo',
'user_api_token_delete' => 'Delete Token',
'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
'user_api_token_delete_success' => 'API token successfully deleted',
'user_api_token_create' => 'ایجاد توکن API',
'user_api_token_name' => 'نام',
'user_api_token_name_desc' => 'توکن خود را به عنوان یادآوری هدف مورد نظر در آینده، نامی خوانا بدهید.',
'user_api_token_expiry' => 'تاریخ انقضا',
'user_api_token_expiry_desc' => 'تاریخی را تعیین کنید که در آن این توکن منقضی شود. پس از این تاریخ، درخواست‌هایی که با استفاده از این رمز انجام می‌شوند دیگر کار نمی‌کنند. خالی گذاشتن این فیلد باعث انقضای 100 سال آینده می شود.',
'user_api_token_create_secret_message' => 'بلافاصله پس از ایجاد این توکن یک "شناسه رمز" و "رمز رمز" تولید و نمایش داده می شود. راز فقط یک بار نشان داده می‌شود، بنابراین قبل از ادامه، حتماً مقدار را در جایی امن و مطمئن کپی کنید.',
'user_api_token_create_success' => 'توکن API با موفقیت ایجاد شد',
'user_api_token_update_success' => 'توکن API با موفقیت به روز شد',
'user_api_token' => 'توکن API',
'user_api_token_id' => 'شناسه توکن',
'user_api_token_id_desc' => 'این یک شناسه غیرقابل ویرایش است که برای این نشانه ایجاد شده است که باید در درخواست‌های API ارائه شود.',
'user_api_token_secret' => 'رمز توکن',
'user_api_token_secret_desc' => 'این یک راز ایجاد شده توسط سیستم برای این نشانه است که باید در درخواست های API ارائه شود. این فقط یک بار نمایش داده می شود، بنابراین این مقدار را در جایی امن و مطمئن کپی کنید.',
'user_api_token_created' => 'توکن ایجاد شد :timeAgo',
'user_api_token_updated' => 'توکن به روز شد :timeAgo',
'user_api_token_delete' => 'توکن را حذف کنید',
'user_api_token_delete_warning' => 'با این کار این نشانه API با نام \':tokenName\' به طور کامل از سیستم حذف می شود.',
'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:',
'webhooks' => 'وب هوک ها',
'webhooks_create' => 'ایجاد وب هوک جدید',
'webhooks_none_created' => 'هنوز هیچ وب هوکی ایجاد نشده است.',
'webhooks_edit' => 'ویرایش وب هوک',
'webhooks_save' => 'ذخیره وب هوک',
'webhooks_details' => 'جزئیات وب هوک',
'webhooks_details_desc' => 'یک نام کاربر پسند و یک نقطه پایانی POST به عنوان مکانی برای ارسال داده های وب هوک ارائه دهید.',
'webhooks_events' => 'رویدادهای وب هوک',
'webhooks_events_desc' => 'تمام رویدادهایی را که باید باعث فراخوانی این وب هوک شوند، انتخاب کنید.',
'webhooks_events_warning' => 'به خاطر داشته باشید که این رویدادها برای همه رویدادهای انتخابی فعال خواهند شد، حتی اگر مجوزهای سفارشی اعمال شوند. مطمئن شوید که استفاده از این وب هوک محتوای محرمانه را فاش نمی کند.',
'webhooks_events_all' => 'تمام رویدادهای سیستم',
'webhooks_name' => 'نام وب هوک',
'webhooks_timeout' => 'مهلت درخواست وب هوک (ثانیه)',
'webhooks_endpoint' => 'نقطه پایانی وب هوک',
'webhooks_active' => 'وب هوک فعال',
'webhook_events_table_header' => 'رویدادها',
'webhooks_delete' => 'حذف وب هوک',
'webhooks_delete_warning' => 'با این کار این وب هوک با نام \':webhookName\' به طور کامل از سیستم حذف می شود.',
'webhooks_delete_confirm' => 'آیا مطمئن هستید که می خواهید این وب هوک را حذف کنید؟',
'webhooks_format_example' => 'نمونه قالب وب هوک',
'webhooks_format_example_desc' => 'داده‌های وب هوک به‌عنوان یک درخواست POST به نقطه پایانی پیکربندی‌شده به‌عنوان JSON با فرمت زیر ارسال می‌شوند. ویژگی های "related_item" و "url" اختیاری هستند و به نوع رویداد راه اندازی شده بستگی دارد.',
'webhooks_status' => 'وضعیت وب هوک',
'webhooks_last_called' => 'آخرین تماس:',
'webhooks_last_errored' => 'آخرین خطا:',
'webhooks_last_error_message' => 'آخرین پیغام خطا:',
//! If editing translations files directly please ignore this in all

View File

@@ -15,7 +15,7 @@ return [
'alpha_dash' => ':attribute باید فقط حروف الفبا، اعداد، خط تیره و زیرخط باشد.',
'alpha_num' => ':attribute باید فقط حروف الفبا و اعداد باشد.',
'array' => ':attribute باید آرایه باشد.',
'backup_codes' => 'The provided code is not valid or has already been used.',
'backup_codes' => 'کد ارائه شده معتبر نیست یا قبلا استفاده شده است.',
'before' => ':attribute باید تاریخی قبل از :date باشد.',
'between' => [
'numeric' => ':attribute باید بین :min و :max باشد.',
@@ -99,7 +99,7 @@ return [
],
'string' => 'فیلد :attribute باید متن باشد.',
'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.',
'totp' => 'The provided code is not valid or has expired.',
'totp' => 'کد ارائه شده معتبر نیست یا منقضی شده است.',
'unique' => ':attribute قبلا انتخاب شده است.',
'url' => ':attribute معتبر نمی‌باشد.',
'uploaded' => 'بارگذاری فایل :attribute موفقیت آمیز نبود.',

View File

@@ -74,7 +74,7 @@ return [
'status' => 'Statut',
'status_active' => 'Actif',
'status_inactive' => 'Inactif',
'never' => 'Never',
'never' => 'Jamais',
// Header
'header_menu_expand' => 'Développer le menu',

View File

@@ -240,24 +240,24 @@ return [
'webhooks_edit' => 'Éditer le Webhook',
'webhooks_save' => 'Enregistrer le Webhook',
'webhooks_details' => 'Détails du Webhook',
'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
'webhooks_details_desc' => 'Renseignez un nom ainsi que votre endpoint POST sur lequel les données du webhook doivent être envoyées.',
'webhooks_events' => 'Événements du Webhook',
'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_desc' => 'Sélectionnez tous les évènements qui doivent déclencher un appel sur ce webhook.',
'webhooks_events_warning' => 'Gardez à l\'esprit que ces événements seront déclenchés pour chaque événement sélectionné, même si des permissions personnalisées sont appliquées. Vérifiez bien que l\'utilisation de ce webhook n\'exposera pas de contenu confidentiel.',
'webhooks_events_all' => 'Tous les événements système',
'webhooks_name' => 'Nom du Webhook',
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
'webhooks_timeout' => 'Délai d\'expiration de requête du Webhook (en secondes)',
'webhooks_endpoint' => 'Point de terminaison du Webhook',
'webhooks_active' => 'Webhook actif',
'webhook_events_table_header' => 'Événements',
'webhooks_delete' => 'Supprimer le 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_delete_warning' => 'Ceci supprimera complètement du système le webhook ayant le nom \':webhookName\'.',
'webhooks_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce webhook ?',
'webhooks_format_example' => 'Exemple de Format de Webhook',
'webhooks_format_example_desc' => 'Les données du webhook sont envoyées dans une requête POST vers l\'endpoint au format JSON respectant le format ci-dessous. Les propriétés "related_item" et "url" sont optionnelles et dépendront du type d\'événement déclenché.',
'webhooks_status' => 'Statut du webhook',
'webhooks_last_called' => 'Dernier appel :',
'webhooks_last_errored' => 'Dernier en erreur :',
'webhooks_last_error_message' => 'Dernier message d\'erreur : ',

View File

@@ -21,7 +21,7 @@ return [
'email' => 'メールアドレス',
'password' => 'パスワード',
'password_confirm' => 'パスワード (確認)',
'password_hint' => 'Must be at least 8 characters',
'password_hint' => '8文字以上で設定する必要があります',
'forgot_password' => 'パスワードをお忘れですか?',
'remember_me' => 'ログイン情報を保存する',
'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
@@ -54,7 +54,7 @@ return [
'email_confirm_text' => '以下のボタンを押し、メールアドレスを確認してください:',
'email_confirm_action' => 'メールアドレスを確認',
'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。',
'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
'email_confirm_success' => 'メールアドレスが確認されました!このメールアドレスでログインできるようになりました。',
'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。',
'email_not_confirmed' => 'Eメールアドレスが確認できていません',
@@ -71,7 +71,7 @@ return [
'user_invite_page_welcome' => ':appNameへようこそ',
'user_invite_page_text' => 'アカウントの設定を完了してアクセスするには、今後の訪問時に:appNameにログインするためのパスワードを設定する必要があります。',
'user_invite_page_confirm_button' => 'パスワードを確定',
'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
'user_invite_success_login' => 'パスワードが設定されました。設定したパスワードで:appNameにログインできるようになりました',
// Multi-factor Authentication
'mfa_setup' => '多要素認証を設定',

View File

@@ -71,24 +71,24 @@ return [
'list_view' => 'リスト形式',
'default' => 'デフォルト',
'breadcrumb' => 'パンくずリスト',
'status' => 'Status',
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'status' => '状態',
'status_active' => '有効',
'status_inactive' => '無効',
'never' => '該当なし',
// Header
'header_menu_expand' => 'Expand Header Menu',
'profile_menu' => 'Profile Menu',
'header_menu_expand' => 'ヘッダーメニューを展開',
'profile_menu' => 'プロフィールメニュー',
'view_profile' => 'プロフィール表示',
'edit_profile' => 'プロフィール編集',
'dark_mode' => 'ダークモード',
'light_mode' => 'ライトモード',
// Layout tabs
'tab_info' => 'Info',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_content' => 'Content',
'tab_content_label' => 'Tab: Show Primary Content',
'tab_info' => '情報',
'tab_info_label' => 'タブ: サブコンテンツを表示',
'tab_content' => '内容',
'tab_content_label' => 'タブ: メインコンテンツを表示',
// Email Content
'email_action_help' => '":actionText" をクリックできない場合、以下のURLをコピーしブラウザで開いてください:',

View File

@@ -22,7 +22,7 @@ return [
'meta_created_name' => '作成: :timeLength (:user)',
'meta_updated' => '更新: :timeLength',
'meta_updated_name' => '更新: :timeLength (:user)',
'meta_owned_name' => 'Owned by :user',
'meta_owned_name' => '所有者: :user',
'entity_select' => 'エンティティ選択',
'images' => '画像',
'my_recent_drafts' => '最近の下書き',
@@ -36,14 +36,14 @@ return [
'export_html' => 'Webページ',
'export_pdf' => 'PDF',
'export_text' => 'テキストファイル',
'export_md' => 'Markdown File',
'export_md' => 'Markdown',
// Permissions and restrictions
'permissions' => '権限',
'permissions_intro' => 'この設定は各ユーザの役割よりも優先して適用されます。',
'permissions_enable' => 'カスタム権限設定を有効にする',
'permissions_save' => '権限を保存',
'permissions_owner' => 'Owner',
'permissions_owner' => '所有者',
// Search
'search_results' => '検索結果',
@@ -143,8 +143,8 @@ return [
'books_sort_chapters_last' => 'チャプターを後に',
'books_sort_show_other' => '他のブックを表示',
'books_sort_save' => '並び順を保存',
'books_copy' => 'Copy Book',
'books_copy_success' => 'Book successfully copied',
'books_copy' => 'ブックをコピー',
'books_copy_success' => 'ブックが正常にコピーされました',
// Chapters
'chapter' => 'チャプター',
@@ -163,8 +163,8 @@ return [
'chapters_move' => 'チャプターを移動',
'chapters_move_named' => 'チャプター「:chapterName」を移動',
'chapter_move_success' => 'チャプターを「:bookName」に移動しました',
'chapters_copy' => 'Copy Chapter',
'chapters_copy_success' => 'Chapter successfully copied',
'chapters_copy' => 'チャプターをコピー',
'chapters_copy_success' => 'チャプターが正常にコピーされました',
'chapters_permissions' => 'チャプター権限',
'chapters_empty' => 'まだチャプター内にページはありません。',
'chapters_permissions_active' => 'チャプターの権限は有効です',
@@ -215,16 +215,16 @@ return [
'pages_copy_success' => 'ページが正常にコピーされました',
'pages_permissions' => 'ページの権限設定',
'pages_permissions_success' => 'ページの権限を更新しました',
'pages_revision' => 'Revision',
'pages_revision' => '編集履歴',
'pages_revisions' => '編集履歴',
'pages_revisions_named' => ':pageName のリビジョン',
'pages_revision_named' => ':pageName のリビジョン',
'pages_revision_restored_from' => 'Restored from #:id; :summary',
'pages_revision_restored_from' => '#:id :summary から復元',
'pages_revisions_created_by' => '作成者',
'pages_revisions_date' => '日付',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'Revision #:id',
'pages_revisions_numbered_changes' => 'Revision #:id Changes',
'pages_revisions_numbered' => 'リビジョン #:id',
'pages_revisions_numbered_changes' => 'リビジョン #:id の変更',
'pages_revisions_changelog' => '説明',
'pages_revisions_changes' => '変更点',
'pages_revisions_current' => '現在のバージョン',
@@ -333,15 +333,15 @@ return [
// Revision
'revision_delete_confirm' => 'このリビジョンを削除しますか?',
'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
'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.',
'copy_consider' => 'コンテンツをコピーする場合は以下の点にご注意ください。',
'copy_consider_permissions' => 'カスタム権限設定はコピーされません。',
'copy_consider_owner' => 'あなたはコピーされた全てのコンテンツの所有者になります。',
'copy_consider_images' => 'ページの画像ファイルは複製されず、元の画像は最初にアップロードされたページとの関係を保持します。',
'copy_consider_attachments' => 'ページの添付ファイルはコピーされません。',
'copy_consider_access' => '場所、所有者または権限を変更すると、以前アクセスできなかったユーザーがこのコンテンツにアクセスできるようになる可能性があります。',
];

View File

@@ -99,7 +99,7 @@ return [
'api_no_authorization_found' => 'リクエストに認証トークンが見つかりません',
'api_bad_authorization_format' => 'リクエストに認証トークンが見つかりましたが、形式が正しくないようです',
'api_user_token_not_found' => '提供された認証トークンに一致するAPIトークンが見つかりませんでした',
'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
'api_incorrect_token_secret' => '利用されたAPIトークンに対して提供されたシークレットが正しくありません',
'api_user_no_api_permission' => '使用されているAPIトークンの所有者には、API呼び出しを行う権限がありません',
'api_user_token_expired' => '認証トークンが期限切れです。',

View File

@@ -234,31 +234,31 @@ return [
'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:',
'webhooks' => 'Webhook',
'webhooks_create' => 'Webhookを作成',
'webhooks_none_created' => 'Webhookはまだ作成されていません。',
'webhooks_edit' => 'Webhookを編集',
'webhooks_save' => 'Webhookを保存',
'webhooks_details' => 'Webhookの詳細',
'webhooks_details_desc' => 'ユーザーフレンドリーな名前とWebhookデータの送信先にするPOSTエンドポイントを指定します。',
'webhooks_events' => 'Webhookのイベント',
'webhooks_events_desc' => 'このWebhookの呼び出しをトリガーするすべてのイベントを選択します。',
'webhooks_events_warning' => 'これらのイベントはカスタム権限が適用されている場合でも、選択したすべてのイベントに対してトリガーされることに注意してください。このWebhookの利用により機密コンテンツが公開されないことを確認してください。',
'webhooks_events_all' => '全てのシステムイベント',
'webhooks_name' => 'Webhook',
'webhooks_timeout' => 'Webhookリクエストタイムアウト (秒)',
'webhooks_endpoint' => 'Webhookエンドポイント',
'webhooks_active' => '有効なWebhook',
'webhook_events_table_header' => 'イベント',
'webhooks_delete' => 'Webhookを削除',
'webhooks_delete_warning' => 'これにより、このWebhook「:webhookName」がシステムから完全に削除されます。',
'webhooks_delete_confirm' => 'このWebhookを削除してよろしいですか',
'webhooks_format_example' => 'Webhookのフォーマット例',
'webhooks_format_example_desc' => 'Webhookのデータは、設定されたエンドポイントにPOSTリクエストにより以下のフォーマットのJSONで送信されます。related_item と url プロパティはオプションであり、トリガーされるイベントの種類によって異なります。',
'webhooks_status' => 'Webhookの状態',
'webhooks_last_called' => '最後の実行:',
'webhooks_last_errored' => '最後のエラー:',
'webhooks_last_error_message' => '最後のエラーのメッセージ:',
//! If editing translations files directly please ignore this in all

View File

@@ -7,57 +7,57 @@ return [
// Pages
'page_create' => 'criou a página',
'page_create_notification' => 'Page successfully created',
'page_create_notification' => 'Página criada com sucesso',
'page_update' => 'atualizou a página',
'page_update_notification' => 'Page successfully updated',
'page_update_notification' => 'Página atualizada com sucesso',
'page_delete' => 'excluiu a página',
'page_delete_notification' => 'Page successfully deleted',
'page_delete_notification' => 'Página excluída com sucesso',
'page_restore' => 'restaurou a página',
'page_restore_notification' => 'Page successfully restored',
'page_restore_notification' => 'Página restaurada com sucesso',
'page_move' => 'moveu a página',
// Chapters
'chapter_create' => 'criou o capítulo',
'chapter_create_notification' => 'Chapter successfully created',
'chapter_create_notification' => 'Capítulo criado com sucesso',
'chapter_update' => 'atualizou o capítulo',
'chapter_update_notification' => 'Chapter successfully updated',
'chapter_update_notification' => 'Capítulo atualizado com sucesso',
'chapter_delete' => 'excluiu o capítulo',
'chapter_delete_notification' => 'Chapter successfully deleted',
'chapter_delete_notification' => 'Capítulo excluída com sucesso',
'chapter_move' => 'moveu o capítulo',
// Books
'book_create' => 'criou o livro',
'book_create_notification' => 'Book successfully created',
'book_create_notification' => 'Livro criado com sucesso',
'book_update' => 'atualizou o livro',
'book_update_notification' => 'Book successfully updated',
'book_update_notification' => 'Livro atualizado com sucesso',
'book_delete' => 'excluiu o livro',
'book_delete_notification' => 'Book successfully deleted',
'book_delete_notification' => 'Livro excluído com sucesso',
'book_sort' => 'ordenou o livro',
'book_sort_notification' => 'Book successfully re-sorted',
'book_sort_notification' => 'Livro reordenado com sucesso',
// Bookshelves
'bookshelf_create' => 'created bookshelf',
'bookshelf_create_notification' => 'Bookshelf successfully created',
'bookshelf_create' => 'prateleira criada',
'bookshelf_create_notification' => 'Prateleira criada com sucesso',
'bookshelf_update' => 'atualizou a prateleira',
'bookshelf_update_notification' => 'Bookshelf successfully updated',
'bookshelf_update_notification' => 'Prateleira atualizada com sucesso',
'bookshelf_delete' => 'excluiu a prateleira',
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
'bookshelf_delete_notification' => 'Prateleira excluída com sucesso',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
'favourite_add_notification' => '":name" foi adicionada aos seus favoritos',
'favourite_remove_notification' => '":name" foi removida dos seus favoritos',
// MFA
'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
'mfa_setup_method_notification' => 'Método de multi-fatores configurado com sucesso',
'mfa_remove_method_notification' => 'Método de multi-fatores removido com sucesso',
// 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',
'webhook_create' => 'webhook criado',
'webhook_create_notification' => 'Webhook criado com sucesso',
'webhook_update' => 'webhook atualizado',
'webhook_update_notification' => 'Webhook atualizado com sucesso',
'webhook_delete' => 'webhook excluído',
'webhook_delete_notification' => 'Webhook excluido com sucesso',
// Other
'commented_on' => 'comentou em',

View File

@@ -39,14 +39,14 @@ return [
'reset' => 'Redefinir',
'remove' => 'Remover',
'add' => 'Adicionar',
'configure' => 'Configure',
'configure' => 'Configurar',
'fullscreen' => 'Tela cheia',
'favourite' => 'Favoritos',
'unfavourite' => 'Remover dos Favoritos',
'next' => 'Seguinte',
'previous' => 'Anterior',
'filter_active' => 'Active Filter:',
'filter_clear' => 'Clear Filter',
'filter_active' => 'Filtro Ativo:',
'filter_clear' => 'Limpar Filtro',
// Sort Options
'sort_options' => 'Opções de Ordenação',
@@ -72,12 +72,12 @@ return [
'default' => 'Padrão',
'breadcrumb' => 'Caminho',
'status' => 'Status',
'status_active' => 'Active',
'status_inactive' => 'Inactive',
'never' => 'Never',
'status_active' => 'Ativo',
'status_inactive' => 'Inativo',
'never' => 'Nunca',
// Header
'header_menu_expand' => 'Expand Header Menu',
'header_menu_expand' => 'Expandir Cabeçalho do Menu',
'profile_menu' => 'Menu de Perfil',
'view_profile' => 'Visualizar Perfil',
'edit_profile' => 'Editar Perfil',
@@ -86,9 +86,9 @@ return [
// Layout tabs
'tab_info' => 'Informações',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_info_label' => 'Aba: Mostrar Informação Secundária',
'tab_content' => 'Conteúdo',
'tab_content_label' => 'Tab: Show Primary Content',
'tab_content_label' => 'Aba: Mostrar Conteúdo Primário',
// Email Content
'email_action_help' => 'Se você estiver tendo problemas ao clicar o botão ":actionText", copie e cole a URL abaixo no seu navegador:',

View File

@@ -74,7 +74,7 @@ return [
'status' => 'Состояние',
'status_active' => 'Активен',
'status_inactive' => 'Неактивен',
'never' => 'Never',
'never' => 'Никогда',
// Header
'header_menu_expand' => 'Развернуть меню заголовка',

View File

@@ -246,7 +246,7 @@ return [
'webhooks_events_warning' => 'Имейте в виду, что эти события будут срабатывать для всех выбранных событий, даже если применяются пользовательские разрешения. Убедитесь, что использование этого вебхука не будет раскрывать конфиденциальные данные.',
'webhooks_events_all' => 'Все системные события',
'webhooks_name' => 'Имя вебхука',
'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
'webhooks_timeout' => 'Таймаут запроса Webhook (секунды)',
'webhooks_endpoint' => 'Конечная точка вебхука',
'webhooks_active' => 'Вебхук активен',
'webhook_events_table_header' => 'События',
@@ -255,10 +255,10 @@ return [
'webhooks_delete_confirm' => 'Вы уверены, что хотите удалить этот вебхук?',
'webhooks_format_example' => 'Пример вебхука',
'webhooks_format_example_desc' => 'Данные вебхука отправляются как POST запрос к настроенной конечной точке в виде JSON в соответствии с форматом ниже. Свойства "related_item" и "url" необязательны и зависят от типа вызванного события.',
'webhooks_status' => 'Webhook Status',
'webhooks_last_called' => 'Last Called:',
'webhooks_last_errored' => 'Last Errored:',
'webhooks_last_error_message' => 'Last Error Message:',
'webhooks_status' => 'Состояние Webhook',
'webhooks_last_called' => 'Последний вызов:',
'webhooks_last_errored' => 'Последняя ошибка:',
'webhooks_last_error_message' => 'Последнее сообщение об ошибке:',
//! If editing translations files directly please ignore this in all

View File

@@ -7,41 +7,41 @@ return [
// Pages
'page_create' => 'vytvoril(a) stránku',
'page_create_notification' => 'Page successfully created',
'page_create_notification' => 'Stránka úspešne vytvorená',
'page_update' => 'aktualizoval(a) stránku',
'page_update_notification' => 'Page successfully updated',
'page_update_notification' => 'Stránka úspešne aktualizovaná',
'page_delete' => 'odstránil(a) stránku',
'page_delete_notification' => 'Page successfully deleted',
'page_delete_notification' => 'Stránka úspešne odstránená',
'page_restore' => 'obnovil(a) stránku',
'page_restore_notification' => 'Page successfully restored',
'page_restore_notification' => 'Stránka úspešne obnovená',
'page_move' => 'presunul(a) stránku',
// Chapters
'chapter_create' => 'vytvoril(a) kapitolu',
'chapter_create_notification' => 'Chapter successfully created',
'chapter_create_notification' => 'Kapitola úspešne vytvorená',
'chapter_update' => 'aktualizoval(a) kapitolu',
'chapter_update_notification' => 'Chapter successfully updated',
'chapter_update_notification' => 'Kapitola úspešne aktualizovaná',
'chapter_delete' => 'odstránil(a) kapitolu',
'chapter_delete_notification' => 'Chapter successfully deleted',
'chapter_delete_notification' => 'Kapitola úspešne odstránená',
'chapter_move' => 'presunul(a) kapitolu',
// Books
'book_create' => 'vytvoril(a) knihu',
'book_create_notification' => 'Book successfully created',
'book_create_notification' => 'Kniha úspešne vytvorená',
'book_update' => 'aktualizoval(a) knihu',
'book_update_notification' => 'Book successfully updated',
'book_update_notification' => 'Kniha úspešne aktualizovaná',
'book_delete' => 'odstránil(a) knihu',
'book_delete_notification' => 'Book successfully deleted',
'book_delete_notification' => 'Kniha úspešne odstránená',
'book_sort' => 'zoradil(a) knihu',
'book_sort_notification' => 'Book successfully re-sorted',
'book_sort_notification' => 'Kniha úspešne znovu zoradená',
// Bookshelves
'bookshelf_create' => 'created bookshelf',
'bookshelf_create_notification' => 'Bookshelf successfully created',
'bookshelf_create' => 'vytvoril(a) knižnicu',
'bookshelf_create_notification' => 'Knižnica úspešne vytvorená',
'bookshelf_update' => 'aktualizoval(a) knižnicu',
'bookshelf_update_notification' => 'Bookshelf successfully updated',
'bookshelf_update_notification' => 'Knižnica úspešne aktualizovaná',
'bookshelf_delete' => 'odstránil(a) knižnicu',
'bookshelf_delete_notification' => 'Bookshelf successfully deleted',
'bookshelf_delete_notification' => 'Knižnica úspešne odstránená',
// Favourites
'favourite_add_notification' => '":name" bol pridaný medzi obľúbené',
@@ -52,12 +52,12 @@ return [
'mfa_remove_method_notification' => 'Viacúrovňový spôsob overenia úspešne odstránený',
// 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',
'webhook_create' => 'vytvoril(a) si webhook',
'webhook_create_notification' => 'Webhook úspešne vytvorený',
'webhook_update' => 'aktualizoval(a) si webhook',
'webhook_update_notification' => 'Webhook úspešne aktualizovaný',
'webhook_delete' => 'odstránil(a) si webhook',
'webhook_delete_notification' => 'Webhook úspešne odstránený',
// Other
'commented_on' => 'komentoval(a)',

View File

@@ -74,7 +74,7 @@ return [
'status' => '状态',
'status_active' => '已激活',
'status_inactive' => '未激活',
'never' => '永不',
'never' => '从未',
// Header
'header_menu_expand' => '展开标头菜单',

View File

@@ -236,7 +236,7 @@ return [
// Webhooks
'webhooks' => 'Webhooks',
'webhooks_create' => '新建 Webhook',
'webhooks_none_created' => '不存在已创建的 webhooks',
'webhooks_none_created' => '尚未创建任何 webhook',
'webhooks_edit' => '编辑 Webhook',
'webhooks_save' => '保存 Webhook',
'webhooks_details' => 'Webhook 详情',

View File

@@ -1,607 +0,0 @@
#editor.bs-editor {
padding-top: 0;
}
//.bs-editor .menubar {
// border-bottom: 1px solid #DDD;
// padding: 2px;
//}
//
//.bs-editor .menuicon {
// cursor: pointer;
// padding: 4px;
// min-width: 2rem;
// border-radius: 3px;
// border: 1px solid transparent;
// &:hover {
// background-color: #EEE;
// border: 1px solid #DDD;
// }
//}
// The below originated from https://github.com/ProseMirror/prosemirror-menu
// and is therefore subject to the MIT license found here:
// https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
white-space: break-spaces;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror table td, .ProseMirror table th {
min-height: 1rem;
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection {
caret-color: transparent;
}
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px;
top: -2px;
bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
/* Protect against generic img rules */
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: 4px;
}
.ProseMirror-menu-submenu-label {
padding-inline-end: 12px;
padding-inline-start: 4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 10em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: .3;
}
.ProseMirror-menu-submenu-wrap:hover > .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active > .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
position: relative;
min-height: 1em;
color: #666;
padding: 1px 6px;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: .8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
border-top: 3px solid #FFF;
border-bottom: 3px solid #FFF;
box-sizing: content-box;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0;
margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
}
.ProseMirror-prompt {
background: white;
padding: 5px 10px 5px 15px;
border: 1px solid silver;
position: fixed;
border-radius: 3px;
z-index: 11;
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
}
.ProseMirror-prompt h5 {
margin: 0;
font-weight: normal;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
padding: 0 4px;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px;
top: 1px;
color: #666;
border: none;
background: transparent;
padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor, .editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}
.ProseMirror > p:first-child,
.ProseMirror > h1:first-child,
.ProseMirror > h2:first-child,
.ProseMirror > h3:first-child,
.ProseMirror > h4:first-child,
.ProseMirror > h5:first-child,
.ProseMirror > h6:first-child {
margin-top: 10px;
}
.ProseMirror {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}
.ProseMirror > p {
margin-bottom: 1em
}
.ProseMirror-menu-color-grid-container {
display: grid;
grid-template-columns: repeat(8, 1fr);
}
.ProseMirror-menu-color-grid-item {
width: 20px;
height: 20px;
border: 2px solid #FFF;
display: block;
}
.ProseMirror-menu-table-creator-grid {
display: grid;
gap: 2px;
}
.ProseMirror-menu-table-creator-grid-item {
width: 14px;
height: 14px;
border: 2px solid #BBB;
display: block;
cursor: pointer;
}
.ProseMirror-menu-table-creator-grid-item-active {
border: 2px solid #555;
background-color: #DDD;
}
.ProseMirror-menu-table-creator-grid-label {
padding: $-xs;
text-align: center;
}
.ProseMirror-menu-dialog-wrap {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.1);
z-index: 50;
display: grid;
}
.ProseMirror-menu-dialog-title {
padding: $-xs $-s;
border-bottom: 1px solid #DDD;
font-weight: bold;
position: relative;
margin-bottom: $-xs;
}
.ProseMirror-menu-dialog-footer {
padding: $-xs $-s;
border-top: 1px solid #DDD;
display: flex;
justify-content: end;
margin-top: $-xs;
}
.ProseMirror-menu-dialog-title-close {
color: #FFF;
position: absolute;
top: $-xs + 2px;
right: $-s;
border-radius: 9px;
height: 18px;
width: 18px;
text-align: center;
line-height: 0;
vertical-align: top;
display: flex;
justify-content: center;
align-items: center;
}
.ProseMirror-menu-dialog {
background-color: #FFF;
border: 1px solid #DDD;
border-radius: 3px;
box-shadow: $bs-large;
width: fit-content;
min-width: 300px;
min-height: 100px;
margin: auto;
}
.ProseMirror-menu-dialog-button {
border: 1px solid #DDD;
padding: $-xs $-s;
color: #666;
min-width: 80px;
cursor: pointer;
&:hover {
background-color: #EEE;
}
}
.ProseMirror-menu-dialog-button + .ProseMirror-menu-dialog-button {
margin-left: $-xs;
}
.ProseMirror-menu-dialog-form-row {
display: grid;
grid-template-columns: 1fr 2fr;
align-items: center;
padding: $-xs 0;
label {
padding: 0 $-s;
font-size: .9rem;
}
input {
margin: 0 $-s;
}
}
.ProseMirror-menu-dialog-textarea-wrap {
padding: $-xs $-s;
label {
padding: 0 $-s;
font-size: .9rem;
}
textarea {
width: 100%;
font-size: 0.8rem;
}
}
.ProseMirror-imagewrap, .ProseMirror-iframewrap {
display: inline-block;
line-height: 0;
font-size: 0;
position: relative;
}
.ProseMirror-imagewrap.ProseMirror-selectednode {
outline: 0;
}
.ProseMirror img[data-show-handles] {
outline: 4px solid #000;
}
.ProseMirror .ProseMirror-iframewrap iframe {
pointer-events: none !important;
}
.ProseMirror-dragdummy {
position: absolute;
z-index: 2;
left: 0;
top: 0;
max-width: none !important;
max-height: none !important;
}
.ProseMirror-grabhandle {
width: 12px;
height: 12px;
border: 2px solid #000;
z-index: 4;
position: absolute;
background-color: #FFF;
}
.ProseMirror-grabhandle-left-top {
cursor: nw-resize;
}
.ProseMirror-grabhandle-right-top {
cursor: ne-resize;
}
.ProseMirror-grabhandle-right-bottom {
cursor: se-resize;
}
.ProseMirror-grabhandle-left-bottom {
cursor: sw-resize;
}
.ProseMirror .tableWrapper {
overflow-x: auto;
}
.ProseMirror table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
overflow: hidden;
}
.ProseMirror td, .ProseMirror th {
vertical-align: top;
box-sizing: border-box;
position: relative;
}
.ProseMirror .column-resize-handle {
position: absolute;
right: -2px; top: 0; bottom: 0;
width: 4px;
z-index: 20;
background-color: #adf;
pointer-events: none;
}
.ProseMirror.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
/* Give selected cells a blue overlay */
.ProseMirror .selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}

View File

@@ -105,9 +105,6 @@ body.mce-fullscreen, body.markdown-fullscreen {
margin-left: auto;
margin-right: auto;
}
.align-justify {
text-align: justify;
}
img {
max-width: 100%;
height:auto;

View File

@@ -20,7 +20,6 @@
@import "footer";
@import "lists";
@import "pages";
@import "editor";
// Jquery Sortable Styles
.dragged {

View File

@@ -47,8 +47,15 @@
display: block;
}
@if($engine === \BookStack\Entities\Tools\PdfGenerator::ENGINE_DOMPDF)
{{-- Fix for full width linked image sizes on DOMPDF --}}
.page-content a > img {
max-width: none;
max-width: 700px;
}
{{-- Undoes the above for table images to prevent visually worse scenario, Awaiting next DOMPDF release for patch --}}
.page-content td a > img {
max-width: 100%;
}
@endif
</style>
@endif

View File

@@ -1,83 +0,0 @@
@extends('layouts.simple')
@section('body')
<div class="container">
<div>
<input id="markdown-toggle" type="checkbox">
</div>
<div id="editor" class="bs-editor page-content" style="margin-bottom: 23px"></div>
<div id="content" style="display: none;">
<h2>This is an editable block</h2>
<p>
Lorem ipsum dolor sit amet, <strong>consectetur adipisicing</strong> elit. Asperiores? <br>
Some <span style="text-decoration: underline">Underlined content</span> Lorem ipsum dolor sit amet. <br>
Some <span style="text-decoration: line-through;">striked content</span> Lorem ipsum dolor sit amet. <br>
Some <span style="color: red;">Red Content</span> Lorem ipsum dolor sit amet. <br>
Some <a href="https://cats.com" target="_blank" title="link A">Linked Content</a> Lorem ipsum dolor sit amet. <br>
</p>
<details>
<summary>Dropdown here</summary>
<h1>Inner header</h1>
<p>Paragraph</p>
</details>
<table style="width: 100%;">
<thead>
<tr>
<th>Header A</th>
<th>Header B</th>
</tr>
</thead>
<tbody>
<tr>
<td style="width: 250px; height: 30px">Content 1</td>
<td style="width: 320px; height: 30px">Content 2</td>
<td style="width: 320px; height: 30px">Content 2</td>
</tr>
<tr>
<td colspan="2">Row 2, Spanning 2</td>
<td>Row 2 spanning 1</td>
</tr>
<tr>
<td rowspan="2">Row 3/4, Column 1</td>
<td>Row 3, Column 2</td>
<td>Row 3, Column 3</td>
</tr>
<tr>
<td>Row 4, Column 2</td>
<td>Row 4, Column 3</td>
</tr>
</tbody>
</table>
{{-- <iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>--}}
<p><img src="/user_avatar.png" alt="Logo"></p>
<ul>
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
<p>Lorem ipsum dolor sit amet.</p>
<hr>
<p class="align-right">Lorem ipsum dolor sit amet.</p>
<p class="callout info">
This is an info callout test!
</p>
</div>
</div>
@endsection
@section('scripts')
<script src="{{ versioned_asset('dist/editor.js') }}" nonce="{{ $cspNonce }}"></script>
@stop

View File

@@ -20,4 +20,13 @@
</div>
@endif
@if(($showUpdatedBy ?? false) && $entity->relationLoaded('updatedBy') && $entity->updatedBy)
<small title="{{ $entity->updated_at->toDayDateTimeString() }}">
{!! trans('entities.meta_updated_name', [
'timeLength' => $entity->updated_at->diffForHumans(),
'user' => e($entity->updatedBy->name)
]) !!}
</small>
@endif
@endcomponent

View File

@@ -4,7 +4,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>@yield('title')</title>
@include('common.export-styles', ['format' => $format])
@include('common.export-styles', ['format' => $format, 'engine' => $engine ?? ''])
@include('common.export-custom-head')
</head>
<body>

View File

@@ -8,7 +8,7 @@
<div class="image-manager-viewer">
<a href="{{ $image->url }}" target="_blank" rel="noopener" class="block">
<img src="{{ $image->thumbs['display'] }}"
<img src="{{ $image->thumbs['display'] ?? $image->url }}"
alt="{{ $image->name }}"
class="anim fadeIn"
title="{{ $image->name }}">

View File

@@ -38,8 +38,6 @@ use Illuminate\View\Middleware\ShareErrorsFromSession;
Route::get('/status', [StatusController::class, 'show']);
Route::get('/robots.txt', [HomeController::class, 'robots']);
Route::view('/editor-test', 'editor-test');
// Authenticated routes...
Route::middleware('auth')->group(function () {

View File

@@ -302,6 +302,7 @@ class ExportTest extends TestCase
$mockPdfGenerator->shouldReceive('fromHtml')
->with(\Mockery::capture($pdfHtml))
->andReturn('');
$mockPdfGenerator->shouldReceive('getActiveEngine')->andReturn(PdfGenerator::ENGINE_DOMPDF);
$this->asEditor()->get($page->getUrl('/export/pdf'));
$this->assertStringNotContainsString('iframe>', $pdfHtml);

View File

@@ -261,6 +261,57 @@ class PageTest extends TestCase
->assertElementContains('.entity-list .page:nth-child(1)', $content['page']->name);
}
public function test_recently_updated_pages_view_shows_updated_by_details()
{
$user = $this->getEditor();
/** @var Page $page */
$page = Page::query()->first();
$this->actingAs($user)->put($page->getUrl(), [
'name' => 'Updated title',
'html' => '<p>Updated content</p>',
]);
$resp = $this->asAdmin()->get('/pages/recently-updated');
$resp->assertElementContains('.entity-list .page:nth-child(1)', 'Updated 1 second ago by ' . $user->name);
}
public function test_recently_updated_pages_view_shows_parent_chain()
{
$user = $this->getEditor();
/** @var Page $page */
$page = Page::query()->whereNotNull('chapter_id')->first();
$this->actingAs($user)->put($page->getUrl(), [
'name' => 'Updated title',
'html' => '<p>Updated content</p>',
]);
$resp = $this->asAdmin()->get('/pages/recently-updated');
$resp->assertElementContains('.entity-list .page:nth-child(1)', $page->chapter->getShortName(42));
$resp->assertElementContains('.entity-list .page:nth-child(1)', $page->book->getShortName(42));
}
public function test_recently_updated_pages_view_does_not_show_parent_if_not_visible()
{
$user = $this->getEditor();
/** @var Page $page */
$page = Page::query()->whereNotNull('chapter_id')->first();
$this->actingAs($user)->put($page->getUrl(), [
'name' => 'Updated title',
'html' => '<p>Updated content</p>',
]);
$this->setEntityRestrictions($page->book);
$this->setEntityRestrictions($page, ['view'], [$user->roles->first()]);
$resp = $this->get('/pages/recently-updated');
$resp->assertDontSee($page->book->getShortName(42));
$resp->assertDontSee($page->chapter->getShortName(42));
$resp->assertElementContains('.entity-list .page:nth-child(1)', 'Updated title');
}
public function test_recently_updated_pages_on_home()
{
/** @var Page $page */

View File

@@ -79,6 +79,24 @@ class HomepageTest extends TestCase
$pageDeleteReq->assertSessionMissing('error');
}
public function test_custom_homepage_cannot_be_deleted_from_parent_deletion()
{
/** @var Page $page */
$page = Page::query()->first();
$this->setSettings([
'app-homepage' => $page->id,
'app-homepage-type' => 'page',
]);
$this->asEditor()->delete($page->book->getUrl());
$this->assertSessionError('Cannot delete a page while it is set as a homepage');
$this->assertDatabaseMissing('deletions', ['deletable_id' => $page->book->id]);
$page->refresh();
$this->assertNull($page->deleted_at);
$this->assertNull($page->book->deleted_at);
}
public function test_custom_homepage_renders_includes()
{
$this->asEditor();

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