]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'development' into release
authorDan Brown <redacted>
Wed, 3 May 2023 10:03:29 +0000 (11:03 +0100)
committerDan Brown <redacted>
Wed, 3 May 2023 10:03:29 +0000 (11:03 +0100)
325 files changed:
.env.example.complete
.github/translators.txt
.github/workflows/lint-js.yml [new file with mode: 0644]
.gitignore
app/Auth/Access/EmailConfirmationService.php
app/Auth/Access/Oidc/OidcIdToken.php
app/Auth/Access/Oidc/OidcService.php
app/Auth/Access/Saml2Service.php
app/Auth/Access/UserInviteService.php
app/Auth/Access/UserTokenService.php
app/Auth/Permissions/EntityPermission.php
app/Config/mail.php
app/Entities/EntityProvider.php
app/Entities/Tools/PageContent.php
app/Entities/Tools/PermissionsUpdater.php
app/Http/Controllers/Api/ContentPermissionApiController.php [new file with mode: 0644]
app/Http/Controllers/Api/ImageGalleryApiController.php [new file with mode: 0644]
app/Http/Controllers/Api/RoleApiController.php
app/Http/Controllers/Auth/ConfirmEmailController.php
app/Http/Controllers/Images/GalleryImageController.php
app/Search/SearchRunner.php
app/Theming/ThemeEvents.php
app/Uploads/Image.php
app/helpers.php
bookstack-system-cli [new file with mode: 0755]
composer.lock
dev/api/requests/content-permissions-update.json [new file with mode: 0644]
dev/api/requests/image-gallery-update.json [new file with mode: 0644]
dev/api/responses/chapters-list.json
dev/api/responses/content-permissions-read.json [new file with mode: 0644]
dev/api/responses/content-permissions-update.json [new file with mode: 0644]
dev/api/responses/image-gallery-create.json [new file with mode: 0644]
dev/api/responses/image-gallery-list.json [new file with mode: 0644]
dev/api/responses/image-gallery-read.json [new file with mode: 0644]
dev/api/responses/image-gallery-update.json [new file with mode: 0644]
dev/build/esbuild.js
dev/docs/development.md
dev/docs/javascript-code.md
dev/docs/javascript-public-events.md [new file with mode: 0644]
lang/ar/components.php
lang/ar/entities.php
lang/ar/errors.php
lang/bg/components.php
lang/bg/entities.php
lang/bg/errors.php
lang/bs/components.php
lang/bs/entities.php
lang/bs/errors.php
lang/ca/components.php
lang/ca/entities.php
lang/ca/errors.php
lang/cs/components.php
lang/cs/entities.php
lang/cs/errors.php
lang/cy/components.php
lang/cy/entities.php
lang/cy/errors.php
lang/da/components.php
lang/da/entities.php
lang/da/errors.php
lang/de/components.php
lang/de/entities.php
lang/de/errors.php
lang/de_informal/components.php
lang/de_informal/entities.php
lang/de_informal/errors.php
lang/el/components.php
lang/el/entities.php
lang/el/errors.php
lang/en/components.php
lang/en/entities.php
lang/en/errors.php
lang/es/components.php
lang/es/entities.php
lang/es/errors.php
lang/es_AR/components.php
lang/es_AR/entities.php
lang/es_AR/errors.php
lang/et/components.php
lang/et/entities.php
lang/et/errors.php
lang/eu/components.php
lang/eu/entities.php
lang/eu/errors.php
lang/fa/components.php
lang/fa/entities.php
lang/fa/errors.php
lang/fa/settings.php
lang/fr/components.php
lang/fr/entities.php
lang/fr/errors.php
lang/he/components.php
lang/he/entities.php
lang/he/errors.php
lang/hr/components.php
lang/hr/entities.php
lang/hr/errors.php
lang/hu/components.php
lang/hu/entities.php
lang/hu/errors.php
lang/id/activities.php
lang/id/auth.php
lang/id/components.php
lang/id/entities.php
lang/id/errors.php
lang/it/components.php
lang/it/entities.php
lang/it/errors.php
lang/ja/components.php
lang/ja/entities.php
lang/ja/errors.php
lang/ka/components.php
lang/ka/entities.php
lang/ka/errors.php
lang/ko/activities.php
lang/ko/auth.php
lang/ko/common.php
lang/ko/components.php
lang/ko/editor.php
lang/ko/entities.php
lang/ko/errors.php
lang/ko/preferences.php
lang/ko/settings.php
lang/lt/components.php
lang/lt/entities.php
lang/lt/errors.php
lang/lv/components.php
lang/lv/entities.php
lang/lv/errors.php
lang/nb/components.php
lang/nb/entities.php
lang/nb/errors.php
lang/nl/components.php
lang/nl/entities.php
lang/nl/errors.php
lang/pl/components.php
lang/pl/entities.php
lang/pl/errors.php
lang/pt/components.php
lang/pt/entities.php
lang/pt/errors.php
lang/pt_BR/components.php
lang/pt_BR/entities.php
lang/pt_BR/errors.php
lang/ro/components.php
lang/ro/entities.php
lang/ro/errors.php
lang/ru/components.php
lang/ru/entities.php
lang/ru/errors.php
lang/sk/components.php
lang/sk/editor.php
lang/sk/entities.php
lang/sk/errors.php
lang/sk/settings.php
lang/sl/components.php
lang/sl/entities.php
lang/sl/errors.php
lang/sv/components.php
lang/sv/entities.php
lang/sv/errors.php
lang/tr/components.php
lang/tr/entities.php
lang/tr/errors.php
lang/uk/components.php
lang/uk/entities.php
lang/uk/errors.php
lang/uz/components.php
lang/uz/entities.php
lang/uz/errors.php
lang/vi/components.php
lang/vi/entities.php
lang/vi/errors.php
lang/zh_CN/components.php
lang/zh_CN/entities.php
lang/zh_CN/errors.php
lang/zh_TW/activities.php
lang/zh_TW/components.php
lang/zh_TW/editor.php
lang/zh_TW/entities.php
lang/zh_TW/errors.php
lang/zh_TW/settings.php
package-lock.json
package.json
phpunit.xml
readme.md
resources/icons/file.svg
resources/icons/upload.svg [new file with mode: 0644]
resources/js/app.js
resources/js/code.mjs [deleted file]
resources/js/code/index.mjs [new file with mode: 0644]
resources/js/code/languages.js [new file with mode: 0644]
resources/js/code/legacy-modes.mjs [new file with mode: 0644]
resources/js/code/setups.js [new file with mode: 0644]
resources/js/code/simple-editor-interface.js [new file with mode: 0644]
resources/js/code/themes.js [new file with mode: 0644]
resources/js/code/views.js [new file with mode: 0644]
resources/js/components/add-remove-rows.js
resources/js/components/ajax-delete-row.js
resources/js/components/ajax-form.js
resources/js/components/attachments-list.js
resources/js/components/attachments.js
resources/js/components/auto-submit.js
resources/js/components/auto-suggest.js
resources/js/components/back-to-top.js
resources/js/components/book-sort.js
resources/js/components/chapter-contents.js
resources/js/components/code-editor.js
resources/js/components/code-highlighter.js
resources/js/components/code-textarea.js
resources/js/components/collapsible.js
resources/js/components/component.js
resources/js/components/confirm-dialog.js
resources/js/components/custom-checkbox.js
resources/js/components/details-highlighter.js
resources/js/components/dropdown-search.js
resources/js/components/dropdown.js
resources/js/components/dropzone.js
resources/js/components/editor-toolbox.js
resources/js/components/entity-permissions.js
resources/js/components/entity-search.js
resources/js/components/entity-selector-popup.js
resources/js/components/entity-selector.js
resources/js/components/event-emit-select.js
resources/js/components/expand-toggle.js
resources/js/components/global-search.js
resources/js/components/header-mobile-toggle.js
resources/js/components/image-manager.js
resources/js/components/image-picker.js
resources/js/components/index.js
resources/js/components/list-sort-control.js
resources/js/components/markdown-editor.js
resources/js/components/new-user-password.js
resources/js/components/notification.js
resources/js/components/optional-input.js
resources/js/components/page-comments.js
resources/js/components/page-display.js
resources/js/components/page-editor.js
resources/js/components/page-picker.js
resources/js/components/permissions-table.js
resources/js/components/pointer.js
resources/js/components/popup.js
resources/js/components/setting-app-color-scheme.js
resources/js/components/setting-color-picker.js
resources/js/components/setting-homepage-control.js
resources/js/components/shelf-sort.js
resources/js/components/shortcut-input.js
resources/js/components/shortcuts.js
resources/js/components/sortable-list.js
resources/js/components/submit-on-change.js
resources/js/components/tabs.js
resources/js/components/tag-manager.js
resources/js/components/template-manager.js
resources/js/components/toggle-switch.js
resources/js/components/tri-layout.js
resources/js/components/user-select.js
resources/js/components/webhook-events.js
resources/js/components/wysiwyg-editor.js
resources/js/markdown/actions.js
resources/js/markdown/codemirror.js
resources/js/markdown/common-events.js
resources/js/markdown/display.js
resources/js/markdown/editor.js
resources/js/markdown/markdown.js
resources/js/markdown/settings.js
resources/js/markdown/shortcuts.js
resources/js/services/animations.js
resources/js/services/clipboard.js
resources/js/services/components.js
resources/js/services/dates.js
resources/js/services/dom.js
resources/js/services/drawio.js
resources/js/services/events.js
resources/js/services/http.js
resources/js/services/keyboard-navigation.js
resources/js/services/text.js
resources/js/services/translations.js
resources/js/services/util.js
resources/js/services/vdom.js
resources/js/wysiwyg/common-events.js
resources/js/wysiwyg/config.js
resources/js/wysiwyg/drop-paste-handling.js
resources/js/wysiwyg/icons.js
resources/js/wysiwyg/plugin-codeeditor.js
resources/js/wysiwyg/plugin-drawio.js
resources/js/wysiwyg/plugins-about.js
resources/js/wysiwyg/plugins-customhr.js
resources/js/wysiwyg/plugins-details.js
resources/js/wysiwyg/plugins-imagemanager.js
resources/js/wysiwyg/plugins-stub.js
resources/js/wysiwyg/plugins-tasklist.js
resources/js/wysiwyg/scrolling.js
resources/js/wysiwyg/shortcuts.js
resources/js/wysiwyg/toolbars.js
resources/js/wysiwyg/util.js
resources/sass/_codemirror.scss
resources/sass/_components.scss
resources/sass/_forms.scss
resources/sass/_layout.scss
resources/sass/_pages.scss
resources/sass/_text.scss
resources/sass/styles.scss
resources/views/api-docs/parts/getting-started.blade.php
resources/views/attachments/manager-edit-form.blade.php
resources/views/attachments/manager-link-form.blade.php
resources/views/attachments/manager.blade.php
resources/views/form/dropzone.blade.php [deleted file]
resources/views/form/simple-dropzone.blade.php [new file with mode: 0644]
resources/views/pages/parts/code-editor.blade.php
resources/views/pages/parts/image-manager-list.blade.php
resources/views/pages/parts/image-manager.blade.php
resources/views/pages/parts/markdown-editor.blade.php
resources/views/pages/parts/pointer.blade.php
routes/api.php
storage/backups/.gitignore [new file with mode: 0644]
tests/Api/ContentPermissionsApiTest.php [new file with mode: 0644]
tests/Api/ImageGalleryApiTest.php [new file with mode: 0644]
tests/Api/TestsApi.php
tests/Auth/OidcTest.php
tests/Auth/Saml2Test.php
tests/Entity/EntitySearchTest.php
tests/Entity/PageContentTest.php
tests/ThemeTest.php
tests/Unit/ConfigTest.php
tests/Uploads/ImageTest.php

index f81bccae4702b9ddf2eb0f903713e1956c1976c1..7071846a36e9da31868d3b7a1ff24096463dd3df 100644 (file)
@@ -3,6 +3,10 @@
 # Each option is shown with it's default value.
 # Do not copy this whole file to use as your '.env' file.
 
+# The details here only serve as a quick reference.
+# Please refer to the BookStack documentation for full details:
+# https://www.bookstackapp.com/docs/
+
 # Application environment
 # Can be 'production', 'development', 'testing' or 'demo'
 APP_ENV=production
@@ -79,6 +83,7 @@ MAIL_PORT=1025
 MAIL_USERNAME=null
 MAIL_PASSWORD=null
 MAIL_ENCRYPTION=null
+MAIL_VERIFY_SSL=true
 
 # Command to use when email is sent via sendmail
 MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
@@ -322,6 +327,13 @@ FILE_UPLOAD_SIZE_LIMIT=50
 # Can be 'a4' or 'letter'.
 EXPORT_PAGE_SIZE=a4
 
+# Set path to wkhtmltopdf binary for PDF generation.
+# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf'
+# When false, BookStack will attempt to find a wkhtmltopdf in the application
+# root folder then fall back to the default dompdf renderer if no binary exists.
+# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
+WKHTMLTOPDF=false
+
 # Allow <script> tags in page content
 # Note, if set to 'true' the page editor may still escape scripts.
 ALLOW_CONTENT_SCRIPTS=false
@@ -372,4 +384,4 @@ LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
 # IP address '146.191.42.4' would result in '146.191.x.x' being logged.
 # For the IPv6 address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as:
 # '2001:db8:85a3:8d3:x:x:x:x'
-IP_ADDRESS_PRECISION=4
\ No newline at end of file
+IP_ADDRESS_PRECISION=4
index a9f8d73d0258eea46ad2e8dc8cdb183c1b19d7de..a448c4a0e448d6d626d26961161331165825ffd3 100644 (file)
@@ -320,3 +320,10 @@ kostasdizas :: Greek
 Ricardo Schroeder (brownstone666) :: Portuguese, Brazilian
 Eitan MG (EitanMG) :: Hebrew
 Robin Flikkema (RobinFlikkema) :: Dutch
+Michal Gurcik (mgurcik) :: Slovak
+Pooyan Arab (pooyanarab) :: Persian
+Ochi Darma Putra (troke12) :: Indonesian
+H.-H. Peng (Hsins) :: Chinese Traditional
+Mosi  Wang (mosiwang) :: Chinese Traditional
+骆言 (LawssssCat) :: Chinese Simplified
+Stickers Gaming Shøw (StickerSGSHOW) :: French
diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml
new file mode 100644 (file)
index 0000000..a8bd8ab
--- /dev/null
@@ -0,0 +1,16 @@
+name: lint-js
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    if: ${{ github.ref != 'refs/heads/l10n_development' }}
+    runs-on: ubuntu-22.04
+    steps:
+    - uses: actions/checkout@v1
+
+    - name: Install NPM deps
+      run: npm ci
+
+    - name: Run formatting check
+      run: npm run lint
index 615013ff78371cf236a479194a8c405446024881..ede4fe29601642d47a2c4b0614f20e8020dd934f 100644 (file)
@@ -1,5 +1,7 @@
 /vendor
 /node_modules
+/.vscode
+/composer
 Homestead.yaml
 .env
 .idea
@@ -21,8 +23,10 @@ yarn.lock
 nbproject
 .buildpath
 .project
+.nvmrc
 .settings/
 webpack-stats.json
 .phpunit.result.cache
 .DS_Store
-phpstan.neon
\ No newline at end of file
+phpstan.neon
+esbuild-meta.json
\ No newline at end of file
index 9c357d95f955f8dfde8227bd3c4525fd056d5d21..1873cad086ca7fa7f762425f18d9b4a0967a8dfe 100644 (file)
@@ -8,8 +8,8 @@ use BookStack\Notifications\ConfirmEmail;
 
 class EmailConfirmationService extends UserTokenService
 {
-    protected $tokenTable = 'email_confirmations';
-    protected $expiryTime = 24;
+    protected string $tokenTable = 'email_confirmations';
+    protected int $expiryTime = 24;
 
     /**
      * Create new confirmation for a user,
index c955c3b091573b042380dabf1230ee568d7d80e0..ca2c85d098205db5e39ef88fdebda9dcb1748db2 100644 (file)
@@ -4,35 +4,16 @@ namespace BookStack\Auth\Access\Oidc;
 
 class OidcIdToken
 {
-    /**
-     * @var array
-     */
-    protected $header;
-
-    /**
-     * @var array
-     */
-    protected $payload;
-
-    /**
-     * @var string
-     */
-    protected $signature;
+    protected array $header;
+    protected array $payload;
+    protected string $signature;
+    protected string $issuer;
+    protected array $tokenParts = [];
 
     /**
      * @var array[]|string[]
      */
-    protected $keys;
-
-    /**
-     * @var string
-     */
-    protected $issuer;
-
-    /**
-     * @var array
-     */
-    protected $tokenParts = [];
+    protected array $keys;
 
     public function __construct(string $token, string $issuer, array $keys)
     {
@@ -106,6 +87,14 @@ class OidcIdToken
         return $this->payload;
     }
 
+    /**
+     * Replace the existing claim data of this token with that provided.
+     */
+    public function replaceClaims(array $claims): void
+    {
+        $this->payload = $claims;
+    }
+
     /**
      * Validate the structure of the given token and ensure we have the required pieces.
      * As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
index 1ca5e19a236b9aff5e54b9a77ffba18f71a87936..3da8b76eb7006f2949b5bf30ee4b6a13e8cd9af5 100644 (file)
@@ -9,6 +9,8 @@ use BookStack\Auth\User;
 use BookStack\Exceptions\JsonDebugException;
 use BookStack\Exceptions\StoppedAuthenticationException;
 use BookStack\Exceptions\UserRegistrationException;
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
 use Illuminate\Support\Arr;
 use Illuminate\Support\Facades\Cache;
 use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@@ -21,24 +23,12 @@ use Psr\Http\Client\ClientInterface as HttpClient;
  */
 class OidcService
 {
-    protected RegistrationService $registrationService;
-    protected LoginService $loginService;
-    protected HttpClient $httpClient;
-    protected GroupSyncService $groupService;
-
-    /**
-     * OpenIdService constructor.
-     */
     public function __construct(
-        RegistrationService $registrationService,
-        LoginService $loginService,
-        HttpClient $httpClient,
-        GroupSyncService $groupService
+        protected RegistrationService $registrationService,
+        protected LoginService $loginService,
+        protected HttpClient $httpClient,
+        protected GroupSyncService $groupService
     ) {
-        $this->registrationService = $registrationService;
-        $this->loginService = $loginService;
-        $this->httpClient = $httpClient;
-        $this->groupService = $groupService;
     }
 
     /**
@@ -226,6 +216,16 @@ class OidcService
             $settings->keys,
         );
 
+        $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
+            'access_token' => $accessToken->getToken(),
+            'expires_in' => $accessToken->getExpires(),
+            'refresh_token' => $accessToken->getRefreshToken(),
+        ]);
+
+        if (!is_null($returnClaims)) {
+            $idToken->replaceClaims($returnClaims);
+        }
+
         if ($this->config()['dump_user_details']) {
             throw new JsonDebugException($idToken->getAllClaims());
         }
index a95e3b1d2e8d410f510680b0911bc7307ebd2269..24efd7f64aad905c996854c06e153defefa85371 100644 (file)
@@ -67,7 +67,7 @@ class Saml2Service
                 $returnRoute,
                 [],
                 $user->email,
-                null,
+                session()->get('saml2_session_index'),
                 true,
                 Constants::NAMEID_EMAIL_ADDRESS
             );
@@ -118,6 +118,7 @@ class Saml2Service
 
         $attrs = $toolkit->getAttributes();
         $id = $toolkit->getNameId();
+        session()->put('saml2_session_index', $toolkit->getSessionIndex());
 
         return $this->processLoginCallback($id, $attrs);
     }
index d884cd6369317ac5cd832e6dff63ea6636a6cbf9..191a03dd5f1d0502372cfd911a6f25a7f4945f28 100644 (file)
@@ -7,14 +7,12 @@ use BookStack\Notifications\UserInvite;
 
 class UserInviteService extends UserTokenService
 {
-    protected $tokenTable = 'user_invites';
-    protected $expiryTime = 336; // Two weeks
+    protected string $tokenTable = 'user_invites';
+    protected int $expiryTime = 336; // Two weeks
 
     /**
      * Send an invitation to a user to sign into BookStack
      * Removes existing invitation tokens.
-     *
-     * @param User $user
      */
     public function sendInvitation(User $user)
     {
index ffd828ab5095194b8df7ab638a73980215095c52..8dfe570f99208d26a05db2942a4b6af1d3edff76 100644 (file)
@@ -14,41 +14,29 @@ class UserTokenService
 {
     /**
      * Name of table where user tokens are stored.
-     *
-     * @var string
      */
-    protected $tokenTable = 'user_tokens';
+    protected string $tokenTable = 'user_tokens';
 
     /**
      * Token expiry time in hours.
-     *
-     * @var int
      */
-    protected $expiryTime = 24;
+    protected int $expiryTime = 24;
 
     /**
-     * Delete all email confirmations that belong to a user.
-     *
-     * @param User $user
-     *
-     * @return mixed
+     * Delete all tokens that belong to a user.
      */
-    public function deleteByUser(User $user)
+    public function deleteByUser(User $user): void
     {
-        return DB::table($this->tokenTable)
+        DB::table($this->tokenTable)
             ->where('user_id', '=', $user->id)
             ->delete();
     }
 
     /**
-     * Get the user id from a token, while check the token exists and has not expired.
-     *
-     * @param string $token
+     * Get the user id from a token, while checking the token exists and has not expired.
      *
      * @throws UserTokenNotFoundException
      * @throws UserTokenExpiredException
-     *
-     * @return int
      */
     public function checkTokenAndGetUserId(string $token): int
     {
@@ -67,8 +55,6 @@ class UserTokenService
 
     /**
      * Creates a unique token within the email confirmation database.
-     *
-     * @return string
      */
     protected function generateToken(): string
     {
@@ -82,10 +68,6 @@ class UserTokenService
 
     /**
      * Generate and store a token for the given user.
-     *
-     * @param User $user
-     *
-     * @return string
      */
     protected function createTokenForUser(User $user): string
     {
@@ -102,10 +84,6 @@ class UserTokenService
 
     /**
      * Check if the given token exists.
-     *
-     * @param string $token
-     *
-     * @return bool
      */
     protected function tokenExists(string $token): bool
     {
@@ -115,12 +93,8 @@ class UserTokenService
 
     /**
      * Get a token entry for the given token.
-     *
-     * @param string $token
-     *
-     * @return object|null
      */
-    protected function getEntryByToken(string $token)
+    protected function getEntryByToken(string $token): ?stdClass
     {
         return DB::table($this->tokenTable)
             ->where('token', '=', $token)
@@ -129,10 +103,6 @@ class UserTokenService
 
     /**
      * Check if the given token entry has expired.
-     *
-     * @param stdClass $tokenEntry
-     *
-     * @return bool
      */
     protected function entryExpired(stdClass $tokenEntry): bool
     {
index 32ebc440d1dccc0b9274fd935531498dd23cb857..603cf61ad64ca1e6252f44b22d6981de3310a1aa 100644 (file)
@@ -5,7 +5,6 @@ namespace BookStack\Auth\Permissions;
 use BookStack\Auth\Role;
 use BookStack\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
-use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
  * @property int $id
@@ -23,14 +22,14 @@ class EntityPermission extends Model
 
     protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
     public $timestamps = false;
-
-    /**
-     * Get this restriction's attached entity.
-     */
-    public function restrictable(): MorphTo
-    {
-        return $this->morphTo('restrictable');
-    }
+    protected $hidden = ['entity_id', 'entity_type', 'id'];
+    protected $casts = [
+        'view' => 'boolean',
+        'create' => 'boolean',
+        'read' => 'boolean',
+        'update' => 'boolean',
+        'delete' => 'boolean',
+    ];
 
     /**
      * Get the role assigned to this entity permission.
index b57c152d9d8062384ca9ed38d0d14fe9386a540c..87514aa409809c875cbe5523404214c440e71dba 100644 (file)
@@ -32,6 +32,7 @@ return [
             'encryption' => env('MAIL_ENCRYPTION', 'tls'),
             'username' => env('MAIL_USERNAME'),
             'password' => env('MAIL_PASSWORD'),
+            'verify_peer' => env('MAIL_VERIFY_SSL', true),
             'timeout' => null,
             'local_domain' => env('MAIL_EHLO_DOMAIN'),
         ],
index aaf392c7b2782b7f54199d4d7398ce8a3059c7d8..365daf7ebe66c9ec70e42159c0e3b8600e3e69b1 100644 (file)
@@ -18,30 +18,11 @@ use BookStack\Entities\Models\PageRevision;
  */
 class EntityProvider
 {
-    /**
-     * @var Bookshelf
-     */
-    public $bookshelf;
-
-    /**
-     * @var Book
-     */
-    public $book;
-
-    /**
-     * @var Chapter
-     */
-    public $chapter;
-
-    /**
-     * @var Page
-     */
-    public $page;
-
-    /**
-     * @var PageRevision
-     */
-    public $pageRevision;
+    public Bookshelf $bookshelf;
+    public Book $book;
+    public Chapter $chapter;
+    public Page $page;
+    public PageRevision $pageRevision;
 
     public function __construct()
     {
@@ -69,13 +50,18 @@ class EntityProvider
     }
 
     /**
-     * Get an entity instance by it's basic name.
+     * Get an entity instance by its basic name.
      */
     public function get(string $type): Entity
     {
         $type = strtolower($type);
+        $instance = $this->all()[$type] ?? null;
+
+        if (is_null($instance)) {
+            throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type");
+        }
 
-        return $this->all()[$type];
+        return $instance;
     }
 
     /**
index b4bc8b91b13cd680f3119822bd82b86ec9d67e6e..949816eff577cc2fc67e0a36c8507bc4d2efe8d1 100644 (file)
@@ -304,7 +304,9 @@ class PageContent
         if ($blankIncludes) {
             $content = $this->blankPageIncludes($content);
         } else {
-            $content = $this->parsePageIncludes($content);
+            for ($includeDepth = 0; $includeDepth < 3; $includeDepth++) {
+                $content = $this->parsePageIncludes($content);
+            }
         }
 
         return $content;
index eb4eb6b48581ae037fd95f911b46beea836bee83..36ed7ccde85dd986175bcffa7c8a7fec363e6156 100644 (file)
@@ -4,20 +4,20 @@ namespace BookStack\Entities\Tools;
 
 use BookStack\Actions\ActivityType;
 use BookStack\Auth\Permissions\EntityPermission;
+use BookStack\Auth\Role;
 use BookStack\Auth\User;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Entity;
 use BookStack\Facades\Activity;
 use Illuminate\Http\Request;
-use Illuminate\Support\Collection;
 
 class PermissionsUpdater
 {
     /**
      * Update an entities permissions from a permission form submit request.
      */
-    public function updateFromPermissionsForm(Entity $entity, Request $request)
+    public function updateFromPermissionsForm(Entity $entity, Request $request): void
     {
         $permissions = $request->get('permissions', null);
         $ownerId = $request->get('owned_by', null);
@@ -39,12 +39,44 @@ class PermissionsUpdater
         Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
     }
 
+    /**
+     * Update permissions from API request data.
+     */
+    public function updateFromApiRequestData(Entity $entity, array $data): void
+    {
+        if (isset($data['role_permissions'])) {
+            $entity->permissions()->where('role_id', '!=', 0)->delete();
+            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);
+            $entity->permissions()->createMany($rolePermissionData);
+        }
+
+        if (array_key_exists('fallback_permissions', $data)) {
+            $entity->permissions()->where('role_id', '=', 0)->delete();
+        }
+
+        if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
+            $data = $data['fallback_permissions'];
+            $data['role_id'] = 0;
+            $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
+            $entity->permissions()->createMany($rolePermissionData);
+        }
+
+        if (isset($data['owner_id'])) {
+            $this->updateOwnerFromId($entity, intval($data['owner_id']));
+        }
+
+        $entity->save();
+        $entity->rebuildPermissions();
+
+        Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
+    }
+
     /**
      * Update the owner of the given entity.
      * Checks the user exists in the system first.
      * Does not save the model, just updates it.
      */
-    protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
+    protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void
     {
         $newOwner = User::query()->find($newOwnerId);
         if (!is_null($newOwner)) {
@@ -67,7 +99,41 @@ class PermissionsUpdater
             $formatted[] = $entityPermissionData;
         }
 
-        return $formatted;
+        return $this->filterEntityPermissionDataUponRole($formatted, true);
+    }
+
+    protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array
+    {
+        $formatted = [];
+
+        foreach ($permissions as $requestPermissionData) {
+            $entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
+            foreach (EntityPermission::PERMISSIONS as $permission) {
+                $entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
+            }
+            $formatted[] = $entityPermissionData;
+        }
+
+        return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback);
+    }
+
+    protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array
+    {
+        $roleIds = [];
+        foreach ($entityPermissionData as $permissionEntry) {
+            $roleIds[] = intval($permissionEntry['role_id']);
+        }
+
+        $actualRoleIds = array_unique(array_values(array_filter($roleIds)));
+        $rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id');
+
+        return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) {
+            if (intval($data['role_id']) === 0) {
+                return $allowFallback;
+            }
+
+            return $rolesById->has($data['role_id']);
+        }));
     }
 
     /**
diff --git a/app/Http/Controllers/Api/ContentPermissionApiController.php b/app/Http/Controllers/Api/ContentPermissionApiController.php
new file mode 100644 (file)
index 0000000..47a0d37
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Tools\PermissionsUpdater;
+use Illuminate\Http\Request;
+
+class ContentPermissionApiController extends ApiController
+{
+    public function __construct(
+        protected PermissionsUpdater $permissionsUpdater,
+        protected EntityProvider $entities
+    ) {
+    }
+
+    protected $rules = [
+        'update' => [
+            'owner_id'  => ['int'],
+
+            'role_permissions' => ['array'],
+            'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'],
+            'role_permissions.*.view' => ['required', 'boolean'],
+            'role_permissions.*.create' => ['required', 'boolean'],
+            'role_permissions.*.update' => ['required', 'boolean'],
+            'role_permissions.*.delete' => ['required', 'boolean'],
+
+            'fallback_permissions' => ['nullable'],
+            'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'],
+            'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
+            'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
+            'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
+            'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
+        ]
+    ];
+
+    /**
+     * Read the configured content-level permissions for the item of the given type and ID.
+     * 'contentType' should be one of: page, book, chapter, bookshelf.
+     * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
+     * The permissions shown are those that override the default for just the specified item, they do not show the
+     * full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
+     * Fallback permission values may be `null` when inheriting is active.
+     */
+    public function read(string $contentType, string $contentId)
+    {
+        $entity = $this->entities->get($contentType)
+            ->newQuery()->scopes(['visible'])->findOrFail($contentId);
+
+        $this->checkOwnablePermission('restrictions-manage', $entity);
+
+        return response()->json($this->formattedPermissionDataForEntity($entity));
+    }
+
+    /**
+     * Update the configured content-level permission overrides for the item of the given type and ID.
+     * 'contentType' should be one of: page, book, chapter, bookshelf.
+     * 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
+     * Providing an empty `role_permissions` array will remove any existing configured role permissions,
+     * so you may want to fetch existing permissions beforehand if just adding/removing a single item.
+     * You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties
+     * from your request data if you don't wish to update details within those categories.
+     */
+    public function update(Request $request, string $contentType, string $contentId)
+    {
+        $entity = $this->entities->get($contentType)
+            ->newQuery()->scopes(['visible'])->findOrFail($contentId);
+
+        $this->checkOwnablePermission('restrictions-manage', $entity);
+
+        $data = $this->validate($request, $this->rules()['update']);
+        $this->permissionsUpdater->updateFromApiRequestData($entity, $data);
+
+        return response()->json($this->formattedPermissionDataForEntity($entity));
+    }
+
+    protected function formattedPermissionDataForEntity(Entity $entity): array
+    {
+        $rolePermissions = $entity->permissions()
+            ->where('role_id', '!=', 0)
+            ->with(['role:id,display_name'])
+            ->get();
+
+        $fallback = $entity->permissions()->where('role_id', '=', 0)->first();
+        $fallbackData = [
+            'inheriting' => is_null($fallback),
+            'view' => $fallback->view ?? null,
+            'create' => $fallback->create ?? null,
+            'update' => $fallback->update ?? null,
+            'delete' => $fallback->delete ?? null,
+        ];
+
+        return [
+            'owner' => $entity->ownedBy()->first(),
+            'role_permissions' => $rolePermissions,
+            'fallback_permissions' => $fallbackData,
+        ];
+    }
+}
diff --git a/app/Http/Controllers/Api/ImageGalleryApiController.php b/app/Http/Controllers/Api/ImageGalleryApiController.php
new file mode 100644 (file)
index 0000000..3dba3d4
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+
+namespace BookStack\Http\Controllers\Api;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Uploads\Image;
+use BookStack\Uploads\ImageRepo;
+use Illuminate\Http\Request;
+
+class ImageGalleryApiController extends ApiController
+{
+    protected array $fieldsToExpose = [
+        'id', 'name', 'url', 'path', 'type', 'uploaded_to', 'created_by', 'updated_by',  'created_at', 'updated_at',
+    ];
+
+    public function __construct(
+        protected ImageRepo $imageRepo
+    ) {
+    }
+
+    protected function rules(): array
+    {
+        return [
+            'create' => [
+                'type'  => ['required', 'string', 'in:gallery,drawio'],
+                'uploaded_to' => ['required', 'integer'],
+                'image' => ['required', 'file', ...$this->getImageValidationRules()],
+                'name'  => ['string', 'max:180'],
+            ],
+            'update' => [
+                'name'  => ['string', 'max:180'],
+            ]
+        ];
+    }
+
+    /**
+     * Get a listing of images in the system. Includes gallery (page content) images and drawings.
+     * Requires visibility of the page they're originally uploaded to.
+     */
+    public function list()
+    {
+        $images = Image::query()->scopes(['visible'])
+            ->select($this->fieldsToExpose)
+            ->whereIn('type', ['gallery', 'drawio']);
+
+        return $this->apiListingResponse($images, [
+            ...$this->fieldsToExpose
+        ]);
+    }
+
+    /**
+     * Create a new image in the system.
+     * Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request.
+     * The provided "uploaded_to" should be an existing page ID in the system.
+     * If the "name" parameter is omitted, the filename of the provided image file will be used instead.
+     * The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used
+     * when the file is a PNG file with diagrams.net image data embedded within.
+     */
+    public function create(Request $request)
+    {
+        $this->checkPermission('image-create-all');
+        $data = $this->validate($request, $this->rules()['create']);
+        Page::visible()->findOrFail($data['uploaded_to']);
+
+        $image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']);
+
+        if (isset($data['name'])) {
+            $image->refresh();
+            $image->update(['name' => $data['name']]);
+        }
+
+        return response()->json($this->formatForSingleResponse($image));
+    }
+
+    /**
+     * View the details of a single image.
+     * The "thumbs" response property contains links to scaled variants that BookStack may use in its UI.
+     * The "content" response property provides HTML and Markdown content, in the format that BookStack
+     * would typically use by default to add the image in page content, as a convenience.
+     * Actual image file data is not provided but can be fetched via the "url" response property.
+     */
+    public function read(string $id)
+    {
+        $image = Image::query()->scopes(['visible'])->findOrFail($id);
+
+        return response()->json($this->formatForSingleResponse($image));
+    }
+
+    /**
+     * Update the details of an existing image in the system.
+     * Only allows updating of the image name at this time.
+     */
+    public function update(Request $request, string $id)
+    {
+        $data = $this->validate($request, $this->rules()['update']);
+        $image = $this->imageRepo->getById($id);
+        $this->checkOwnablePermission('page-view', $image->getPage());
+        $this->checkOwnablePermission('image-update', $image);
+
+        $this->imageRepo->updateImageDetails($image, $data);
+
+        return response()->json($this->formatForSingleResponse($image));
+    }
+
+    /**
+     * Delete an image from the system.
+     * Will also delete thumbnails for the image.
+     * Does not check or handle image usage so this could leave pages with broken image references.
+     */
+    public function delete(string $id)
+    {
+        $image = $this->imageRepo->getById($id);
+        $this->checkOwnablePermission('page-view', $image->getPage());
+        $this->checkOwnablePermission('image-delete', $image);
+        $this->imageRepo->destroyImage($image);
+
+        return response('', 204);
+    }
+
+    /**
+     * Format the given image model for single-result display.
+     */
+    protected function formatForSingleResponse(Image $image): array
+    {
+        $this->imageRepo->loadThumbs($image);
+        $data = $image->getAttributes();
+        $data['created_by'] = $image->createdBy;
+        $data['updated_by'] = $image->updatedBy;
+        $data['content'] = [];
+
+        $escapedUrl = htmlentities($image->url);
+        $escapedName = htmlentities($image->name);
+        if ($image->type === 'drawio') {
+            $data['content']['html'] = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$escapedUrl}\"></div>";
+            $data['content']['markdown'] = $data['content']['html'];
+        } else {
+            $escapedDisplayThumb = htmlentities($image->thumbs['display']);
+            $data['content']['html'] = "<a href=\"{$escapedUrl}\" target=\"_blank\"><img src=\"{$escapedDisplayThumb}\" alt=\"{$escapedName}\"></a>";
+            $mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name));
+            $mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display']));
+            $data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})";
+        }
+
+        return $data;
+    }
+}
index 4f78455e0cae1de72a6567c18df11a0a21b467d2..6986c73f78d9befe3465b0fed3ee1b8b3a98b894 100644 (file)
@@ -88,10 +88,10 @@ class RoleApiController extends ApiController
      */
     public function read(string $id)
     {
-        $user = $this->permissionsRepo->getRoleById($id);
-        $this->singleFormatter($user);
+        $role = $this->permissionsRepo->getRoleById($id);
+        $this->singleFormatter($role);
 
-        return response()->json($user);
+        return response()->json($role);
     }
 
     /**
index b282d0601f31eaab351f971be08d0f0ad6540150..fdde8e70c1b9e851cf79fac5261b72b7fa25a723 100644 (file)
@@ -14,21 +14,11 @@ use Illuminate\Http\Request;
 
 class ConfirmEmailController extends Controller
 {
-    protected EmailConfirmationService $emailConfirmationService;
-    protected LoginService $loginService;
-    protected UserRepo $userRepo;
-
-    /**
-     * Create a new controller instance.
-     */
     public function __construct(
-        EmailConfirmationService $emailConfirmationService,
-        LoginService $loginService,
-        UserRepo $userRepo
+        protected EmailConfirmationService $emailConfirmationService,
+        protected LoginService $loginService,
+        protected UserRepo $userRepo
     ) {
-        $this->emailConfirmationService = $emailConfirmationService;
-        $this->loginService = $loginService;
-        $this->userRepo = $userRepo;
     }
 
     /**
index 5484411d36c4208da2055e37d9e5335689c8270a..c01eccfee30193a5fc95f85f5a2630e190846569 100644 (file)
@@ -10,14 +10,9 @@ use Illuminate\Validation\ValidationException;
 
 class GalleryImageController extends Controller
 {
-    protected $imageRepo;
-
-    /**
-     * GalleryImageController constructor.
-     */
-    public function __construct(ImageRepo $imageRepo)
-    {
-        $this->imageRepo = $imageRepo;
+    public function __construct(
+        protected ImageRepo $imageRepo
+    ) {
     }
 
     /**
@@ -47,9 +42,14 @@ class GalleryImageController extends Controller
     public function create(Request $request)
     {
         $this->checkPermission('image-create-all');
-        $this->validate($request, [
-            'file' => $this->getImageValidationRules(),
-        ]);
+
+        try {
+            $this->validate($request, [
+                'file' => $this->getImageValidationRules(),
+            ]);
+        } catch (ValidationException $exception) {
+            return $this->jsonError(implode("\n", $exception->errors()['file']));
+        }
 
         try {
             $imageUpload = $request->file('file');
index 013f7b380b82239e8ce475b63d8efa52d856db8f..a0fd1fe3dab8264f63ef7ec95088a4db665baa66 100644 (file)
@@ -173,6 +173,7 @@ class SearchRunner
         // Handle exact term matching
         foreach ($searchOpts->exacts as $inputTerm) {
             $entityQuery->where(function (EloquentBuilder $query) use ($inputTerm, $entityModelInstance) {
+                $inputTerm = str_replace('\\', '\\\\', $inputTerm);
                 $query->where('name', 'like', '%' . $inputTerm . '%')
                     ->orWhere($entityModelInstance->textField, 'like', '%' . $inputTerm . '%');
             });
@@ -218,6 +219,7 @@ class SearchRunner
         $subQuery->where('entity_type', '=', $entity->getMorphClass());
         $subQuery->where(function (Builder $query) use ($terms) {
             foreach ($terms as $inputTerm) {
+                $inputTerm = str_replace('\\', '\\\\', $inputTerm);
                 $query->orWhere('term', 'like', $inputTerm . '%');
             }
         });
@@ -354,6 +356,9 @@ class SearchRunner
                     $tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
                     $query->whereRaw("value {$tagOperator} {$tagValue}");
                 } else {
+                    if ($tagOperator === 'like') {
+                        $tagValue = str_replace('\\', '\\\\', $tagValue);
+                    }
                     $query->where('value', $tagOperator, $tagValue);
                 }
             } else {
index 91f4fcd675ab02e59af0366926f81cfcdd6f74f4..aacef80cfd29117c284398f9d2d695e20db52eef 100644 (file)
@@ -70,6 +70,19 @@ class ThemeEvents
      */
     const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
 
+    /**
+     * OIDC ID token pre-validate event.
+     * Runs just before BookStack validates the user ID token data upon login.
+     * Provides the existing found set of claims for the user as a key-value array,
+     * along with an array of the proceeding access token data provided by the identity platform.
+     * If the listener returns a non-null value, that will replace the existing ID token claim data.
+     *
+     * @param array $idTokenData
+     * @param array $accessTokenData
+     * @returns array|null
+     */
+    const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';
+
     /**
      * Page include parse event.
      * Runs when a page include tag is being parsed, typically when page content is being processed for viewing.
index c21a3b03fe5a27aee99faca7417b0f4a661b8b0c..0ab0b612a7c10a00b6f074d71e9e4e28f49ee2a9 100644 (file)
@@ -3,9 +3,11 @@
 namespace BookStack\Uploads;
 
 use BookStack\Auth\Permissions\JointPermission;
+use BookStack\Auth\Permissions\PermissionApplicator;
 use BookStack\Entities\Models\Page;
 use BookStack\Model;
 use BookStack\Traits\HasCreatorAndUpdater;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\HasMany;
 
@@ -33,12 +35,21 @@ class Image extends Model
             ->where('joint_permissions.entity_type', '=', 'page');
     }
 
+    /**
+     * Scope the query to just the images visible to the user based upon the
+     * user visibility of the uploaded_to page.
+     */
+    public function scopeVisible(Builder $query): Builder
+    {
+        return app()->make(PermissionApplicator::class)->restrictPageRelationQuery($query, 'images', 'uploaded_to');
+    }
+
     /**
      * Get a thumbnail for this image.
      *
      * @throws \Exception
      */
-    public function getThumb(int $width, int $height, bool $keepRatio = false): string
+    public function getThumb(?int $width, ?int $height, bool $keepRatio = false): string
     {
         return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
     }
index 191eddf4d0596e33c5f5ba7af0a783620a2bda5d..837b7fcfdd0bb9c5904726559a68b0d092a99738 100644 (file)
@@ -147,7 +147,7 @@ function icon(string $name, array $attrs = []): string
 }
 
 /**
- * Generate a url with multiple parameters for sorting purposes.
+ * Generate a URL with multiple parameters for sorting purposes.
  * Works out the logic to set the correct sorting direction
  * Discards empty parameters and allows overriding.
  */
@@ -172,7 +172,7 @@ function sortUrl(string $path, array $data, array $overrideData = []): string
     }
 
     if (count($queryStringSections) === 0) {
-        return $path;
+        return url($path);
     }
 
     return url($path . '?' . implode('&', $queryStringSections));
diff --git a/bookstack-system-cli b/bookstack-system-cli
new file mode 100755 (executable)
index 0000000..ef82d04
Binary files /dev/null and b/bookstack-system-cli differ
index 766f3d51b69fac077b9181a3b5f01ad4f20121fa..1f3d95683d3176eaa18b2a257fbf9795fdec0b82 100644 (file)
@@ -8,23 +8,27 @@
     "packages": [
         {
             "name": "aws/aws-crt-php",
-            "version": "v1.0.4",
+            "version": "v1.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/awslabs/aws-crt-php.git",
-                "reference": "f5c64ee7c5fce196e2519b3d9b7138649efe032d"
+                "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/f5c64ee7c5fce196e2519b3d9b7138649efe032d",
-                "reference": "f5c64ee7c5fce196e2519b3d9b7138649efe032d",
+                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5",
+                "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.5"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35|^5.6.3"
+                "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+                "yoast/phpunit-polyfills": "^1.0"
+            },
+            "suggest": {
+                "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
             },
             "type": "library",
             "autoload": {
@@ -43,7 +47,7 @@
                 }
             ],
             "description": "AWS Common Runtime for PHP",
-            "homepage": "http://aws.amazon.com/sdkforphp",
+            "homepage": "https://github.com/awslabs/aws-crt-php",
             "keywords": [
                 "amazon",
                 "aws",
             ],
             "support": {
                 "issues": "https://github.com/awslabs/aws-crt-php/issues",
-                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.0.4"
+                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1"
             },
-            "time": "2023-01-31T23:08:25+00:00"
+            "time": "2023-03-24T20:22:19+00:00"
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.262.0",
+            "version": "3.269.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "f45eefe4735d5a16ecc44cfd9a6c29421ae1e802"
+                "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f45eefe4735d5a16ecc44cfd9a6c29421ae1e802",
-                "reference": "f45eefe4735d5a16ecc44cfd9a6c29421ae1e802",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
+                "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
                 "shasum": ""
             },
             "require": {
@@ -77,7 +81,7 @@
                 "ext-simplexml": "*",
                 "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
                 "guzzlehttp/promises": "^1.4.0",
-                "guzzlehttp/psr7": "^1.8.5 || ^2.3",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
                 "mtdowling/jmespath.php": "^2.6",
                 "php": ">=5.5"
             },
                 "paragonie/random_compat": ">= 2",
                 "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
                 "psr/cache": "^1.0",
+                "psr/http-message": "^1.0",
                 "psr/simple-cache": "^1.0",
                 "sebastian/comparator": "^1.2.3 || ^4.0",
                 "yoast/phpunit-polyfills": "^1.0"
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.262.0"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.269.0"
             },
-            "time": "2023-03-23T18:21:18+00:00"
+            "time": "2023-04-26T18:21:04+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
         },
         {
             "name": "brick/math",
-            "version": "0.10.2",
+            "version": "0.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/brick/math.git",
-                "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f"
+                "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f",
-                "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f",
+                "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478",
+                "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478",
                 "shasum": ""
             },
             "require": {
-                "ext-json": "*",
-                "php": "^7.4 || ^8.0"
+                "php": "^8.0"
             },
             "require-dev": {
                 "php-coveralls/php-coveralls": "^2.2",
                 "phpunit/phpunit": "^9.0",
-                "vimeo/psalm": "4.25.0"
+                "vimeo/psalm": "5.0.0"
             },
             "type": "library",
             "autoload": {
             ],
             "support": {
                 "issues": "https://github.com/brick/math/issues",
-                "source": "https://github.com/brick/math/tree/0.10.2"
+                "source": "https://github.com/brick/math/tree/0.11.0"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-08-10T22:54:19+00:00"
+            "time": "2023-01-15T23:15:59+00:00"
         },
         {
             "name": "dasprid/enum",
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.6.1",
+            "version": "3.6.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e"
+                "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/57815c7bbcda3cd18871d253c1dd8cbe56f8526e",
-                "reference": "57815c7bbcda3cd18871d253c1dd8cbe56f8526e",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/b4bd1cfbd2b916951696d82e57d054394d84864c",
+                "reference": "b4bd1cfbd2b916951696d82e57d054394d84864c",
                 "shasum": ""
             },
             "require": {
                 "doctrine/coding-standard": "11.1.0",
                 "fig/log-test": "^1",
                 "jetbrains/phpstorm-stubs": "2022.3",
-                "phpstan/phpstan": "1.10.3",
+                "phpstan/phpstan": "1.10.9",
                 "phpstan/phpstan-strict-rules": "^1.5",
-                "phpunit/phpunit": "9.6.4",
+                "phpunit/phpunit": "9.6.6",
                 "psalm/plugin-phpunit": "0.18.4",
                 "squizlabs/php_codesniffer": "3.7.2",
                 "symfony/cache": "^5.4|^6.0",
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.6.1"
+                "source": "https://github.com/doctrine/dbal/tree/3.6.2"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-02T19:26:24+00:00"
+            "time": "2023-04-14T07:25:38+00:00"
         },
         {
             "name": "doctrine/deprecations",
         },
         {
             "name": "guzzlehttp/guzzle",
-            "version": "7.5.0",
+            "version": "7.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/guzzle.git",
-                "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba"
+                "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b50a2a1251152e43f6a37f0fa053e730a67d25ba",
-                "reference": "b50a2a1251152e43f6a37f0fa053e730a67d25ba",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b964ca597e86b752cd994f27293e9fa6b6a95ed9",
+                "reference": "b964ca597e86b752cd994f27293e9fa6b6a95ed9",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "guzzlehttp/promises": "^1.5",
-                "guzzlehttp/psr7": "^1.9 || ^2.4",
+                "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
                 "php": "^7.2.5 || ^8.0",
                 "psr/http-client": "^1.0",
                 "symfony/deprecation-contracts": "^2.2 || ^3.0"
             ],
             "support": {
                 "issues": "https://github.com/guzzle/guzzle/issues",
-                "source": "https://github.com/guzzle/guzzle/tree/7.5.0"
+                "source": "https://github.com/guzzle/guzzle/tree/7.5.1"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-08-28T15:39:27+00:00"
+            "time": "2023-04-17T16:30:08+00:00"
         },
         {
             "name": "guzzlehttp/promises",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "2.4.4",
+            "version": "2.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf"
+                "reference": "b635f279edd83fc275f822a1188157ffea568ff6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf",
-                "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6",
+                "reference": "b635f279edd83fc275f822a1188157ffea568ff6",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2.5 || ^8.0",
                 "psr/http-factory": "^1.0",
-                "psr/http-message": "^1.0",
+                "psr/http-message": "^1.1 || ^2.0",
                 "ralouphie/getallheaders": "^3.0"
             },
             "provide": {
                 "bamarni-bin": {
                     "bin-links": true,
                     "forward-command": false
-                },
-                "branch-alias": {
-                    "dev-master": "2.4-dev"
                 }
             },
             "autoload": {
             ],
             "support": {
                 "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.4.4"
+                "source": "https://github.com/guzzle/psr7/tree/2.5.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-09T13:19:02+00:00"
+            "time": "2023-04-17T16:11:26+00:00"
         },
         {
             "name": "guzzlehttp/uri-template",
         },
         {
             "name": "laravel/framework",
-            "version": "v9.52.4",
+            "version": "v9.52.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "9239128cfb4d22afefb64060dfecf53e82987267"
+                "reference": "675ea868fe36b18c8303e954aac540e6b1caa677"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/9239128cfb4d22afefb64060dfecf53e82987267",
-                "reference": "9239128cfb4d22afefb64060dfecf53e82987267",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/675ea868fe36b18c8303e954aac540e6b1caa677",
+                "reference": "675ea868fe36b18c8303e954aac540e6b1caa677",
                 "shasum": ""
             },
             "require": {
                 "league/flysystem-read-only": "^3.3",
                 "league/flysystem-sftp-v3": "^3.0",
                 "mockery/mockery": "^1.5.1",
-                "orchestra/testbench-core": "^7.16",
+                "orchestra/testbench-core": "^7.24",
                 "pda/pheanstalk": "^4.0",
                 "phpstan/phpdoc-parser": "^1.15",
                 "phpstan/phpstan": "^1.4.7",
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2023-02-22T14:38:06+00:00"
+            "time": "2023-04-25T13:44:05+00:00"
         },
         {
             "name": "laravel/serializable-closure",
         },
         {
             "name": "league/commonmark",
-            "version": "2.3.9",
+            "version": "2.4.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "c1e114f74e518daca2729ea8c4bf1167038fa4b5"
+                "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c1e114f74e518daca2729ea8c4bf1167038fa4b5",
-                "reference": "c1e114f74e518daca2729ea8c4bf1167038fa4b5",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
+                "reference": "d44a24690f16b8c1808bf13b1bd54ae4c63ea048",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "2.4-dev"
+                    "dev-main": "2.5-dev"
                 }
             },
             "autoload": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-02-15T14:07:24+00:00"
+            "time": "2023-03-24T15:16:10+00:00"
         },
         {
             "name": "league/config",
         },
         {
             "name": "league/flysystem",
-            "version": "3.12.3",
+            "version": "3.14.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem.git",
-                "reference": "81e87e74dd5213795c7846d65089712d2dda90ce"
+                "reference": "e2a279d7f47d9098e479e8b21f7fb8b8de230158"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/81e87e74dd5213795c7846d65089712d2dda90ce",
-                "reference": "81e87e74dd5213795c7846d65089712d2dda90ce",
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/e2a279d7f47d9098e479e8b21f7fb8b8de230158",
+                "reference": "e2a279d7f47d9098e479e8b21f7fb8b8de230158",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem/issues",
-                "source": "https://github.com/thephpleague/flysystem/tree/3.12.3"
+                "source": "https://github.com/thephpleague/flysystem/tree/3.14.0"
             },
             "funding": [
                 {
                 {
                     "url": "https://github.com/frankdejonge",
                     "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
-                    "type": "tidelift"
                 }
             ],
-            "time": "2023-02-18T15:32:41+00:00"
+            "time": "2023-04-11T18:11:47+00:00"
         },
         {
             "name": "league/flysystem-aws-s3-v3",
-            "version": "3.12.2",
+            "version": "3.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
-                "reference": "645e14e4a80bd2da8b01e57388e7296a695a80c2"
+                "reference": "8e04cbb403d4dfd5b73a2f8685f1df395bd177eb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/645e14e4a80bd2da8b01e57388e7296a695a80c2",
-                "reference": "645e14e4a80bd2da8b01e57388e7296a695a80c2",
+                "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/8e04cbb403d4dfd5b73a2f8685f1df395bd177eb",
+                "reference": "8e04cbb403d4dfd5b73a2f8685f1df395bd177eb",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
-                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.12.2"
+                "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.13.0"
             },
             "funding": [
                 {
                 {
                     "url": "https://github.com/frankdejonge",
                     "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
-                    "type": "tidelift"
                 }
             ],
-            "time": "2023-01-17T14:15:08+00:00"
+            "time": "2023-03-16T14:29:01+00:00"
         },
         {
             "name": "league/html-to-markdown",
         },
         {
             "name": "league/oauth2-client",
-            "version": "2.6.1",
+            "version": "2.7.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/oauth2-client.git",
-                "reference": "2334c249907190c132364f5dae0287ab8666aa19"
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/2334c249907190c132364f5dae0287ab8666aa19",
-                "reference": "2334c249907190c132364f5dae0287ab8666aa19",
+                "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8",
+                "reference": "160d6274b03562ebeb55ed18399281d8118b76c8",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/oauth2-client/issues",
-                "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.1"
+                "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0"
             },
-            "time": "2021-12-22T16:42:49+00:00"
+            "time": "2023-04-16T18:19:15+00:00"
         },
         {
             "name": "masterminds/html5",
-            "version": "2.7.6",
+            "version": "2.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Masterminds/html5-php.git",
-                "reference": "897eb517a343a2281f11bc5556d6548db7d93947"
+                "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947",
-                "reference": "897eb517a343a2281f11bc5556d6548db7d93947",
+                "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3",
+                "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3",
                 "shasum": ""
             },
             "require": {
-                "ext-ctype": "*",
                 "ext-dom": "*",
-                "ext-libxml": "*",
                 "php": ">=5.3.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7"
+                "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8"
             },
             "type": "library",
             "extra": {
             ],
             "support": {
                 "issues": "https://github.com/Masterminds/html5-php/issues",
-                "source": "https://github.com/Masterminds/html5-php/tree/2.7.6"
+                "source": "https://github.com/Masterminds/html5-php/tree/2.8.0"
             },
-            "time": "2022-08-18T16:18:26+00:00"
+            "time": "2023-04-26T07:27:39+00:00"
         },
         {
             "name": "monolog/monolog",
         },
         {
             "name": "psr/http-client",
-            "version": "1.0.1",
+            "version": "1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/http-client.git",
-                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
+                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
-                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+                "url": "https://api.github.com/repos/php-fig/http-client/zipball/0955afe48220520692d2d09f7ab7e0f93ffd6a31",
+                "reference": "0955afe48220520692d2d09f7ab7e0f93ffd6a31",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0 || ^8.0",
-                "psr/http-message": "^1.0"
+                "psr/http-message": "^1.0 || ^2.0"
             },
             "type": "library",
             "extra": {
             "authors": [
                 {
                     "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
+                    "homepage": "https://www.php-fig.org/"
                 }
             ],
             "description": "Common interface for HTTP clients",
                 "psr-18"
             ],
             "support": {
-                "source": "https://github.com/php-fig/http-client/tree/master"
+                "source": "https://github.com/php-fig/http-client/tree/1.0.2"
             },
-            "time": "2020-06-29T06:28:15+00:00"
+            "time": "2023-04-10T20:12:12+00:00"
         },
         {
             "name": "psr/http-factory",
-            "version": "1.0.1",
+            "version": "1.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/http-factory.git",
-                "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
+                "reference": "e616d01114759c4c489f93b099585439f795fe35"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
-                "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+                "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+                "reference": "e616d01114759c4c489f93b099585439f795fe35",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.0.0",
-                "psr/http-message": "^1.0"
+                "psr/http-message": "^1.0 || ^2.0"
             },
             "type": "library",
             "extra": {
             "authors": [
                 {
                     "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
+                    "homepage": "https://www.php-fig.org/"
                 }
             ],
             "description": "Common interfaces for PSR-7 HTTP message factories",
                 "response"
             ],
             "support": {
-                "source": "https://github.com/php-fig/http-factory/tree/master"
+                "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
             },
-            "time": "2019-04-30T12:38:16+00:00"
+            "time": "2023-04-10T20:10:41+00:00"
         },
         {
             "name": "psr/http-message",
-            "version": "1.0.1",
+            "version": "2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/php-fig/http-message.git",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
-                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+                "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.3.0"
+                "php": "^7.2 || ^8.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0.x-dev"
+                    "dev-master": "2.0.x-dev"
                 }
             },
             "autoload": {
             "authors": [
                 {
                     "name": "PHP-FIG",
-                    "homepage": "http://www.php-fig.org/"
+                    "homepage": "https://www.php-fig.org/"
                 }
             ],
             "description": "Common interface for HTTP messages",
                 "response"
             ],
             "support": {
-                "source": "https://github.com/php-fig/http-message/tree/master"
+                "source": "https://github.com/php-fig/http-message/tree/2.0"
             },
-            "time": "2016-08-06T14:39:51+00:00"
+            "time": "2023-04-04T09:54:51+00:00"
         },
         {
             "name": "psr/log",
         },
         {
             "name": "psy/psysh",
-            "version": "v0.11.13",
+            "version": "v0.11.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "722317c9f5627e588788e340f29b923e58f92f54"
+                "reference": "151b145906804eea8e5d71fea23bfb470c904bfb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/722317c9f5627e588788e340f29b923e58f92f54",
-                "reference": "722317c9f5627e588788e340f29b923e58f92f54",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/151b145906804eea8e5d71fea23bfb470c904bfb",
+                "reference": "151b145906804eea8e5d71fea23bfb470c904bfb",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.11.13"
+                "source": "https://github.com/bobthecow/psysh/tree/v0.11.16"
             },
-            "time": "2023-03-21T14:22:44+00:00"
+            "time": "2023-04-26T12:53:57+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
         },
         {
             "name": "ramsey/uuid",
-            "version": "4.7.3",
+            "version": "4.7.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/ramsey/uuid.git",
-                "reference": "433b2014e3979047db08a17a205f410ba3869cf2"
+                "reference": "60a4c63ab724854332900504274f6150ff26d286"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/ramsey/uuid/zipball/433b2014e3979047db08a17a205f410ba3869cf2",
-                "reference": "433b2014e3979047db08a17a205f410ba3869cf2",
+                "url": "https://api.github.com/repos/ramsey/uuid/zipball/60a4c63ab724854332900504274f6150ff26d286",
+                "reference": "60a4c63ab724854332900504274f6150ff26d286",
                 "shasum": ""
             },
             "require": {
-                "brick/math": "^0.8.8 || ^0.9 || ^0.10",
+                "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11",
                 "ext-json": "*",
                 "php": "^8.0",
                 "ramsey/collection": "^1.2 || ^2.0"
             ],
             "support": {
                 "issues": "https://github.com/ramsey/uuid/issues",
-                "source": "https://github.com/ramsey/uuid/tree/4.7.3"
+                "source": "https://github.com/ramsey/uuid/tree/4.7.4"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-01-12T18:13:24+00:00"
+            "time": "2023-04-15T23:01:58+00:00"
         },
         {
             "name": "robrichards/xmlseclibs",
         },
         {
             "name": "filp/whoops",
-            "version": "2.15.1",
+            "version": "2.15.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "e864ac957acd66e1565f25efda61e37791a5db0b"
+                "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/e864ac957acd66e1565f25efda61e37791a5db0b",
-                "reference": "e864ac957acd66e1565f25efda61e37791a5db0b",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
+                "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.15.1"
+                "source": "https://github.com/filp/whoops/tree/2.15.2"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-03-06T18:09:13+00:00"
+            "time": "2023-04-12T12:00:00+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
         },
         {
             "name": "nunomaduro/larastan",
-            "version": "2.5.1",
+            "version": "v2.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/larastan.git",
-                "reference": "072e2c9566ae000bf66c92384fc933b81885244b"
+                "reference": "ccac5b25949576807862cf32ba1fce1769c06c42"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/072e2c9566ae000bf66c92384fc933b81885244b",
-                "reference": "072e2c9566ae000bf66c92384fc933b81885244b",
+                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/ccac5b25949576807862cf32ba1fce1769c06c42",
+                "reference": "ccac5b25949576807862cf32ba1fce1769c06c42",
                 "shasum": ""
             },
             "require": {
                 "illuminate/support": "^9.47.0 || ^10.0.0",
                 "php": "^8.0.2",
                 "phpmyadmin/sql-parser": "^5.6.0",
-                "phpstan/phpstan": "~1.10.3"
+                "phpstan/phpstan": "~1.10.6"
             },
             "require-dev": {
                 "nikic/php-parser": "^4.15.2",
             ],
             "support": {
                 "issues": "https://github.com/nunomaduro/larastan/issues",
-                "source": "https://github.com/nunomaduro/larastan/tree/2.5.1"
+                "source": "https://github.com/nunomaduro/larastan/tree/v2.6.0"
             },
             "funding": [
                 {
                     "type": "patreon"
                 }
             ],
-            "time": "2023-03-04T23:46:40+00:00"
+            "time": "2023-04-20T12:40:01+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.8",
+            "version": "1.10.14",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9"
+                "reference": "d232901b09e67538e5c86a724be841bea5768a7c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0166aef76e066f0dd2adc2799bdadfa1635711e9",
-                "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d232901b09e67538e5c86a724be841bea5768a7c",
+                "reference": "d232901b09e67538e5c86a724be841bea5768a7c",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-24T10:28:16+00:00"
+            "time": "2023-04-19T13:47:27+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.6.5",
+            "version": "9.6.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5"
+                "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/86e761949019ae83f49240b2f2123fb5ab3b2fc5",
-                "reference": "86e761949019ae83f49240b2f2123fb5ab3b2fc5",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2",
+                "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.5"
+                "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-09T06:34:10+00:00"
+            "time": "2023-04-14T08:58:40+00:00"
         },
         {
             "name": "sebastian/cli-parser",
diff --git a/dev/api/requests/content-permissions-update.json b/dev/api/requests/content-permissions-update.json
new file mode 100644 (file)
index 0000000..124bb8b
--- /dev/null
@@ -0,0 +1,26 @@
+{
+  "owner_id": 1,
+  "role_permissions": [
+    {
+      "role_id": 2,
+      "view": true,
+      "create": true,
+      "update": true,
+      "delete": false
+    },
+    {
+      "role_id": 3,
+      "view": false,
+      "create": false,
+      "update": false,
+      "delete": false
+    }
+  ],
+  "fallback_permissions": {
+    "inheriting": false,
+    "view": true,
+    "create": true,
+    "update": false,
+    "delete": false
+  }
+}
\ No newline at end of file
diff --git a/dev/api/requests/image-gallery-update.json b/dev/api/requests/image-gallery-update.json
new file mode 100644 (file)
index 0000000..e332e3a
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "name": "My updated image name"
+}
\ No newline at end of file
index 9790286b0a2ccf52bf51743552878cd63b618121..dc6dde100f397e0062d82824852f4ff50fa7de7e 100644 (file)
@@ -7,7 +7,7 @@
       "slug": "content-creation",
       "description": "How to create documentation on whatever subject you need to write about.",
       "priority": 3,
-      "created_at": "2019-05-05:",
+      "created_at": "2019-05-05T21:49:56.000000Z",
       "updated_at": "2019-09-28T11:24:23.000000Z",
       "created_by": 1,
       "updated_by": 1,
diff --git a/dev/api/responses/content-permissions-read.json b/dev/api/responses/content-permissions-read.json
new file mode 100644 (file)
index 0000000..591fc5c
--- /dev/null
@@ -0,0 +1,38 @@
+{
+  "owner": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "role_permissions": [
+    {
+      "role_id": 2,
+      "view": true,
+      "create": false,
+      "update": true,
+      "delete": false,
+      "role": {
+        "id": 2,
+        "display_name": "Editor"
+      }
+    },
+    {
+      "role_id": 10,
+      "view": true,
+      "create": true,
+      "update": false,
+      "delete": false,
+      "role": {
+        "id": 10,
+        "display_name": "Wizards of the west"
+      }
+    }
+  ],
+  "fallback_permissions": {
+    "inheriting": false,
+    "view": true,
+    "create": false,
+    "update": false,
+    "delete": false
+  }
+}
\ No newline at end of file
diff --git a/dev/api/responses/content-permissions-update.json b/dev/api/responses/content-permissions-update.json
new file mode 100644 (file)
index 0000000..67fa40b
--- /dev/null
@@ -0,0 +1,38 @@
+{
+  "owner": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "role_permissions": [
+    {
+      "role_id": 2,
+      "view": true,
+      "create": true,
+      "update": true,
+      "delete": false,
+      "role": {
+        "id": 2,
+        "display_name": "Editor"
+      }
+    },
+    {
+      "role_id": 3,
+      "view": false,
+      "create": false,
+      "update": false,
+      "delete": false,
+      "role": {
+        "id": 3,
+        "display_name": "Viewer"
+      }
+    }
+  ],
+  "fallback_permissions": {
+    "inheriting": false,
+    "view": true,
+    "create": true,
+    "update": false,
+    "delete": false
+  }
+}
\ No newline at end of file
diff --git a/dev/api/responses/image-gallery-create.json b/dev/api/responses/image-gallery-create.json
new file mode 100644 (file)
index 0000000..e278244
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "name": "cute-cat-image.png",
+  "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "type": "gallery",
+  "uploaded_to": 1,
+  "created_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "updated_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "updated_at": "2023-03-15 08:17:37",
+  "created_at": "2023-03-15 08:17:37",
+  "id": 618,
+  "thumbs": {
+    "gallery": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png",
+    "display": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png"
+  },
+  "content": {
+    "html": "<a href=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png\" target=\"_blank\"><img src=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png\" alt=\"cute-cat-image.png\"><\/a>",
+    "markdown": "![cute-cat-image.png](https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)"
+  }
+}
\ No newline at end of file
diff --git a/dev/api/responses/image-gallery-list.json b/dev/api/responses/image-gallery-list.json
new file mode 100644 (file)
index 0000000..054d68a
--- /dev/null
@@ -0,0 +1,41 @@
+{
+  "data": [
+    {
+      "id": 1,
+      "name": "My cat scribbles",
+      "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/scribbles.jpg",
+      "path": "\/uploads\/images\/gallery\/2023-02\/scribbles.jpg",
+      "type": "gallery",
+      "uploaded_to": 1,
+      "created_by": 1,
+      "updated_by": 1,
+      "created_at": "2023-02-12T16:34:57.000000Z",
+      "updated_at": "2023-02-12T16:34:57.000000Z"
+    },
+    {
+      "id": 2,
+      "name": "Drawing-1.png",
+      "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/drawio\/2023-02\/drawing-1.png",
+      "path": "\/uploads\/images\/drawio\/2023-02\/drawing-1.png",
+      "type": "drawio",
+      "uploaded_to": 2,
+      "created_by": 2,
+      "updated_by": 2,
+      "created_at": "2023-02-12T16:39:19.000000Z",
+      "updated_at": "2023-02-12T16:39:19.000000Z"
+    },
+    {
+      "id": 8,
+      "name": "beans.jpg",
+      "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-02\/beans.jpg",
+      "path": "\/uploads\/images\/gallery\/2023-02\/beans.jpg",
+      "type": "gallery",
+      "uploaded_to": 6,
+      "created_by": 1,
+      "updated_by": 1,
+      "created_at": "2023-02-15T19:37:44.000000Z",
+      "updated_at": "2023-02-15T19:37:44.000000Z"
+    }
+  ],
+  "total": 3
+}
\ No newline at end of file
diff --git a/dev/api/responses/image-gallery-read.json b/dev/api/responses/image-gallery-read.json
new file mode 100644 (file)
index 0000000..c6c468d
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "id": 618,
+  "name": "cute-cat-image.png",
+  "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "created_at": "2023-03-15 08:17:37",
+  "updated_at": "2023-03-15 08:17:37",
+  "created_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "updated_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "type": "gallery",
+  "uploaded_to": 1,
+  "thumbs": {
+    "gallery": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png",
+    "display": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png"
+  },
+  "content": {
+    "html": "<a href=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png\" target=\"_blank\"><img src=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png\" alt=\"cute-cat-image.png\"><\/a>",
+    "markdown": "![cute-cat-image.png](https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)"
+  }
+}
\ No newline at end of file
diff --git a/dev/api/responses/image-gallery-update.json b/dev/api/responses/image-gallery-update.json
new file mode 100644 (file)
index 0000000..6e6168a
--- /dev/null
@@ -0,0 +1,28 @@
+{
+  "id": 618,
+  "name": "My updated image name",
+  "url": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "created_at": "2023-03-15 08:17:37",
+  "updated_at": "2023-03-15 08:24:50",
+  "created_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "updated_by": {
+    "id": 1,
+    "name": "Admin",
+    "slug": "admin"
+  },
+  "path": "\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png",
+  "type": "gallery",
+  "uploaded_to": 1,
+  "thumbs": {
+    "gallery": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/thumbs-150-150\/cute-cat-image.png",
+    "display": "https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png"
+  },
+  "content": {
+    "html": "<a href=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/cute-cat-image.png\" target=\"_blank\"><img src=\"https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png\" alt=\"My updated image name\"><\/a>",
+    "markdown": "![My updated image name](https:\/\/p.rizon.top:443\/https\/bookstack.example.com\/uploads\/images\/gallery\/2023-03\/scaled-1680-\/cute-cat-image.png)"
+  }
+}
\ No newline at end of file
index 46357038a23c082e4d472c072911afe14833f60e..c1e246955b2c2b65d577a7d620bc9b9611c4877a 100644 (file)
@@ -1,32 +1,35 @@
 #!/usr/bin/env node
 
 const esbuild = require('esbuild');
-const fs = require('fs');
 const path = require('path');
+const fs = require('fs');
 
 // Check if we're building for production
 // (Set via passing `production` as first argument)
 const isProd = process.argv[2] === 'production';
 
 // Gather our input files
-const jsInDir = path.join(__dirname, '../../resources/js');
-const jsInDirFiles = fs.readdirSync(jsInDir, 'utf8');
-const entryFiles = jsInDirFiles
-    .filter(f => f.endsWith('.js') || f.endsWith('.mjs'))
-    .map(f => path.join(jsInDir, f));
+const entryPoints = {
+    app: path.join(__dirname, '../../resources/js/app.js'),
+    code: path.join(__dirname, '../../resources/js/code/index.mjs'),
+    'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
+};
 
 // Locate our output directory
-const outDir = path.join(__dirname, '../../public/dist');
+const outdir = path.join(__dirname, '../../public/dist');
 
 // Build via esbuild
 esbuild.build({
     bundle: true,
-    entryPoints: entryFiles,
-    outdir: outDir,
+    metafile: true,
+    entryPoints,
+    outdir,
     sourcemap: true,
     target: 'es2020',
     mainFields: ['module', 'main'],
     format: 'esm',
     minify: isProd,
     logLevel: "info",
+}).then(result => {
+    fs.writeFileSync('esbuild-meta.json', JSON.stringify(result.metafile));
 }).catch(() => process.exit(1));
\ No newline at end of file
index b68f2664aa38762df7743f8628e4dc474bcf3c6e..a68ae50b47e8598d1fdeec41dbfc80b0d0acbcc8 100644 (file)
@@ -33,6 +33,10 @@ If the codebase needs to be tested with deprecations, this can be done via uncom
 
 ## Code Standards
 
+We use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.
+
+### PHP
+
 PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer).
 Static analysis is in place using [PHPStan](https://phpstan.org/) & [Larastan](https://github.com/nunomaduro/larastan).
 The below commands can be used to utilise these tools:
@@ -51,7 +55,19 @@ composer format
 composer check-static
 ```
 
-If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.
+### JavaScript
+
+JavaScript code standards use managed using [ESLint](https://eslint.org/).
+The ESLint rule configuration is managed within the `package.json` file.
+The below commands can be used to lint and format:
+
+```bash
+# Run code linting using ESLint
+npm run lint
+
+# Fix code where possible using ESLint
+npm run fix
+```
 
 ## Development using Docker
 
index a1092ce92569ffad21aec73471fbde7b8b621b93..9820839a45d1c094e72908dd7e9e2a6ef6c03a0e 100644 (file)
@@ -115,6 +115,7 @@ There are various global helper libraries in BookStack which can be accessed via
 
 ```js
 // HTTP service
+// Relative URLs will be resolved against the instance BASE_URL
 window.$http.get(url, params);
 window.$http.post(url, data);
 window.$http.put(url, data);
@@ -153,4 +154,10 @@ window.$components.get(name);
 // Get the first active component of the given name that's been
 // created on the given element.
 window.$components.firstOnElement(element, name);
-```
\ No newline at end of file
+```
+
+## Public Events
+
+There are a range of available events that are emitted as part of a public & supported API for accessing or extending JavaScript libraries & components used in the system.
+
+Details on these events can be found in the [JavaScript Public Events file](javascript-public-events.md).
\ No newline at end of file
diff --git a/dev/docs/javascript-public-events.md b/dev/docs/javascript-public-events.md
new file mode 100644 (file)
index 0000000..1e95dc8
--- /dev/null
@@ -0,0 +1,255 @@
+# JavaScript Public Events
+
+There are a range of available events emitted as part of a public & [supported](#support) API for accessing or extending JavaScript libraries and components used in the system.
+These are emitted via standard DOM events so can be consumed using standard DOM APIs like so:
+
+```javascript
+window.addEventListener('event-name', event => {
+   const eventData = event.detail; 
+});
+```
+
+Such events are typically emitted from a DOM element relevant to event, which then bubbles up.
+For most use-cases you can probably just listen on the `window` as shown above.
+
+## Support
+
+This event system, and the events emitted, are considered semi-supported.
+Breaking changes of the event API, event names, or event properties, are possible but will be documented in update notes.
+The detail provided within the events, and the libraries made accessible, are not considered supported nor stable, and changes to these won't be clearly documented changelogs.
+
+## Event Naming Scheme
+
+Events are typically named in the following format:
+
+```text
+<context>::<action/lifecycle>
+
+# Examples:
+editor-tinymce::setup
+library-cm6::configure-theme
+```
+
+If the event is generic in use but specific to a library, the `<context>` will start with `library-` followed by the library name. Otherwise `<context>` may reflect the UI context/component.
+
+The `<action/lifecycle>` reflects the lifecycle stage of the context, or a specific action to perform if the event is specific to a certain use-case.
+
+## Event Listing
+
+### `editor-markdown-cm6::pre-init`
+
+This event is called before the markdown input editor CodeMirror instance is created or loaded.
+
+#### Event Data
+
+- `editorViewConfig` - An [EditorViewConfig](https://codemirror.net/docs/ref/#view.EditorViewConfig) object that will eventially be passed when creating the CodeMirror EditorView instance.
+
+##### Example
+
+```javascript
+// Always load the editor with specific pre-defined content if empty
+window.addEventListener('editor-markdown-cm6::pre-init', event => {
+    const config = event.detail.editorViewConfig;
+    config.doc = config.doc || "Start this page with a nice story";
+});
+```
+
+### `editor-markdown::setup`
+
+This event is called when the markdown editor loads, post configuration but before the editor is ready to use.
+
+#### Event Data
+
+- `markdownIt` - A references to the [MarkdownIt](https://markdown-it.github.io/markdown-it/#MarkdownIt) instance used to render markdown to HTML (Just for the preview).
+- `displayEl` - The IFrame Element that wraps the HTML preview display.
+- `cmEditorView` - The CodeMirror [EditorView](https://codemirror.net/docs/ref/#view.EditorView) instance used for the markdown input editor.
+
+##### Example
+
+```javascript
+// Set all text in the display to be red by default.
+window.addEventListener('editor-markdown::setup', event => {
+    const display = event.detail.displayEl;
+    display.contentDocument.body.style.color = 'red';
+});
+```
+
+### `editor-drawio::configure`
+
+This event is called as the embedded diagrams.net drawing editor loads, to allow configuration of the diagrams.net interface.
+See [this diagrams.net page](https://www.diagrams.net/doc/faq/configure-diagram-editor) for details on the available options for the configure event.
+
+If using a custom diagrams.net instance, via the `DRAWIO` option, you will need to ensure  your DRAWIO option URL has the `configure=1` query parameter.
+
+#### Event Data
+
+- `config` - The configuration object that will be passed to diagrams.net.
+  - This will likely be empty by default, but modify this object in-place as needed with your desired options.
+
+##### Example
+
+```javascript
+// Set only the "general" and "android" libraries to show by default
+window.addEventListener('editor-drawio::configure', event => {
+    const config = event.detail.config;
+    config.enabledLibraries = ["general", "android"];
+});
+```
+
+### `editor-tinymce::pre-init`
+
+This event is called before the TinyMCE editor, used as the BookStack WYSIWYG page editor, is initialised.
+
+#### Event Data
+
+- `config` - Object containing the configuration that's going to be passed to [tinymce.init](https://www.tiny.cloud/docs/api/tinymce/root_tinymce/#init).
+
+##### Example
+
+```javascript
+// Removed "bold" from the editor toolbar
+window.addEventListener('editor-tinymce::pre-init', event => {
+    const tinyConfig = event.detail.config;
+    tinyConfig.toolbar = tinyConfig.toolbar.replace('bold ', '');
+});
+```
+
+### `editor-tinymce::setup`
+
+This event is called during the `setup` lifecycle stage of the TinyMCE editor used as the BookStack WYSIWYG editor. This is after configuration, but before the editor is fully loaded and ready to use. 
+
+##### Event Data
+
+- `editor` - The [tinymce.Editor](https://www.tiny.cloud/docs/api/tinymce/tinymce.editor/) instance used for the WYSIWYG editor.
+
+##### Example
+
+```javascript
+// Replaces the editor content with redacted message 3 seconds after load.
+window.addEventListener('editor-tinymce::setup', event => {
+    const editor = event.detail.editor;
+    setTimeout(() => {
+        editor.setContent('REDACTED!');
+    }, 3000);
+});
+```
+
+### `library-cm6::configure-theme`
+
+This event is called whenever a CodeMirror instance is loaded, as a method to configure the theme used by CodeMirror. This applies to all CodeMirror instances including in-page code blocks, editors using in BookStack settings, and the Page markdown editor.
+
+#### Event Data
+
+- `darkModeActive` - A boolean to indicate if the current view/page is being loaded with dark mode active.
+- `registerViewTheme(builder)` - A method that can be called to register a new view (CodeMirror UI) theme.
+  - `builder` - A function that will return  an object that will be passed into the CodeMirror [EditorView.theme()](https://codemirror.net/docs/ref/#view.EditorView^theme) function as a StyleSpec.
+- `registerHighlightStyle(builder)` - A method that can be called to register a new HighlightStyle (code highlighting) theme.
+  - `builder` - A function, that receives a reference to [Tag.tags](https://lezer.codemirror.net/docs/ref/#highlight.tags) and returns an array of [TagStyle](https://codemirror.net/docs/ref/#language.TagStyle) objects.
+
+##### Example
+
+The below shows registering a custom "Solarized dark" editor and syntax theme:
+
+<details>
+<summary>Show Example</summary>
+
+```javascript
+// Theme data taken from:
+// https://github.com/craftzdog/cm6-themes/blob/main/packages/solarized-dark/src/index.ts
+// (MIT License) - Copyright (C) 2022 by Takuya Matsuyama and others
+const base00 = '#002b36',
+    base01 = '#073642',
+    base02 = '#586e75',
+    base03 = '#657b83',
+    base04 = '#839496',
+    base05 = '#93a1a1',
+    base06 = '#eee8d5',
+    base07 = '#fdf6e3',
+    base_red = '#dc322f',
+    base_orange = '#cb4b16',
+    base_yellow = '#b58900',
+    base_green = '#859900',
+    base_cyan = '#2aa198',
+    base_blue = '#268bd2',
+    base_violet = '#6c71c4',
+    base_magenta = '#d33682'
+
+const invalid = '#d30102',
+    stone = base04,
+    darkBackground = '#00252f',
+    highlightBackground = '#173541',
+    background = base00,
+    tooltipBackground = base01,
+    selection = '#173541',
+    cursor = base04
+
+function viewThemeBuilder() {
+    return {
+      '&':{color:base05,backgroundColor:background},
+      '.cm-content':{caretColor:cursor},
+      '.cm-cursor, .cm-dropCursor':{borderLeftColor:cursor},
+      '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':{backgroundColor:selection},
+      '.cm-panels':{backgroundColor:darkBackground,color:base03},
+      '.cm-panels.cm-panels-top':{borderBottom:'2px solid black'},
+      '.cm-panels.cm-panels-bottom':{borderTop:'2px solid black'},
+      '.cm-searchMatch':{backgroundColor:'#72a1ff59',outline:'1px solid #457dff'},
+      '.cm-searchMatch.cm-searchMatch-selected':{backgroundColor:'#6199ff2f'},
+      '.cm-activeLine':{backgroundColor:highlightBackground},
+      '.cm-selectionMatch':{backgroundColor:'#aafe661a'},
+      '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket':{outline:`1px solid ${base06}`},
+      '.cm-gutters':{backgroundColor:darkBackground,color:stone,border:'none'},
+      '.cm-activeLineGutter':{backgroundColor:highlightBackground},
+      '.cm-foldPlaceholder':{backgroundColor:'transparent',border:'none',color:'#ddd'},
+      '.cm-tooltip':{border:'none',backgroundColor:tooltipBackground},
+      '.cm-tooltip .cm-tooltip-arrow:before':{borderTopColor:'transparent',borderBottomColor:'transparent'},
+      '.cm-tooltip .cm-tooltip-arrow:after':{borderTopColor:tooltipBackground,borderBottomColor:tooltipBackground},
+      '.cm-tooltip-autocomplete':{
+        '& > ul > li[aria-selected]':{backgroundColor:highlightBackground,color:base03}
+      }
+    };
+}
+
+function highlightStyleBuilder(t) {
+    return [{tag:t.keyword,color:base_green},
+      {tag:[t.name,t.deleted,t.character,t.propertyName,t.macroName],color:base_cyan},
+      {tag:[t.variableName],color:base05},
+      {tag:[t.function(t.variableName)],color:base_blue},
+      {tag:[t.labelName],color:base_magenta},
+      {tag:[t.color,t.constant(t.name),t.standard(t.name)],color:base_yellow},
+      {tag:[t.definition(t.name),t.separator],color:base_cyan},
+      {tag:[t.brace],color:base_magenta},
+      {tag:[t.annotation],color:invalid},
+      {tag:[t.number,t.changed,t.annotation,t.modifier,t.self,t.namespace],color:base_magenta},
+      {tag:[t.typeName,t.className],color:base_orange},
+      {tag:[t.operator,t.operatorKeyword],color:base_violet},
+      {tag:[t.tagName],color:base_blue},
+      {tag:[t.squareBracket],color:base_red},
+      {tag:[t.angleBracket],color:base02},
+      {tag:[t.attributeName],color:base05},
+      {tag:[t.regexp],color:invalid},
+      {tag:[t.quote],color:base_green},
+      {tag:[t.string],color:base_yellow},
+      {tag:t.link,color:base_cyan,textDecoration:'underline',textUnderlinePosition:'under'},
+      {tag:[t.url,t.escape,t.special(t.string)],color:base_yellow},
+      {tag:[t.meta],color:base_red},
+      {tag:[t.comment],color:base02,fontStyle:'italic'},
+      {tag:t.strong,fontWeight:'bold',color:base06},
+      {tag:t.emphasis,fontStyle:'italic',color:base_green},
+      {tag:t.strikethrough,textDecoration:'line-through'},
+      {tag:t.heading,fontWeight:'bold',color:base_yellow},
+      {tag:t.heading1,fontWeight:'bold',color:base07},
+      {tag:[t.heading2,t.heading3,t.heading4],fontWeight:'bold',color:base06},
+      {tag:[t.heading5,t.heading6],color:base06},
+      {tag:[t.atom,t.bool,t.special(t.variableName)],color:base_magenta},
+      {tag:[t.processingInstruction,t.inserted,t.contentSeparator],color:base_red},
+      {tag:[t.contentSeparator],color:base_yellow},
+      {tag:t.invalid,color:base02,borderBottom:`1px dotted ${base_red}`}];
+}
+
+window.addEventListener('library-cm6::configure-theme', event => {
+    const detail = event.detail;
+    detail.registerViewTheme(viewThemeBuilder);
+    detail.registerHighlightStyle(highlightStyleBuilder);
+});
+```
+</details>
\ No newline at end of file
index 803232b2d00e94706b151da31f4f1647dbebad17..0305ae95f54d919bd60a1babb4c82c8ef29ce703 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'تحديد صورة',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'الكل',
     'image_all_title' => 'عرض جميع الصور',
     'image_book_title' => 'عرض الصور المرفوعة لهذا الكتاب',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',
     'image_select_image' => 'تحديد الصورة',
     'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'تم حذف الصور',
     'image_preview' => 'معاينة الصور',
     'image_upload_success' => 'تم رفع الصورة بنجاح',
     'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح',
     'image_delete_success' => 'تم حذف الصورة بنجاح',
-    'image_upload_remove' => 'إزالة',
 
     // Code Editor
     'code_editor' => 'تعديل الشفرة',
index c9ca3fe357ae86b82ba6da867532e9cd963777fe..76c7662f492eae06d4bcac9a679f50b1787a0024 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'المرفقات',
     'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
     'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',
-    'attachments_items' => 'العناصر المرفقة',
     'attachments_upload' => 'رفع ملف',
     'attachments_link' => 'إرفاق رابط',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'تحديد الرابط',
     'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
-    'attachments_dropzone' => 'أسقط الملفات أو اضغط هنا لإرفاق ملف',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'لم تُرفع أي ملفات',
     'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
     'attachments_link_name' => 'اسم الرابط',
index 5726a5e0f7bbf984967790f3faa46497526b6acc..3db61431007687053d651374b82b674264435e56 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'لا يمكن للخادم إنشاء صور مصغرة. الرجاء التأكد من تثبيت إضافة GD PHP.',
     'server_upload_limit' => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',
     'uploaded'  => 'الخادم لا يسمح برفع ملفات بهذا الحجم. الرجاء محاولة الرفع بحجم أصغر.',
-    'file_upload_timeout' => 'انتهت عملية تحميل الملف.',
 
     // Drawing & Images
     'image_upload_error' => 'حدث خطأ خلال رفع الصورة',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'لم يتم العثور على المرفق',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'فشل حفظ المسودة. الرجاء التأكد من وجود اتصال بالإنترنت قبل حفظ الصفحة',
index ca7121437bb5e92ca28c2d9273107b5090793222..1bf9e68fa9286064bef43406a050b33d9ba7ec60 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Избор на изображение',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Всички',
     'image_all_title' => 'Преглед на всички изображения',
     'image_book_title' => 'Виж изображенията прикачени към тази книга',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Сигурни ли сте, че искате да изтриете това изображение?',
     'image_select_image' => 'Изберете изображение',
     'image_dropzone' => 'Поставете тук изображение или кликнете тук за да качите',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Изображението е изтрито',
     'image_preview' => 'Преглед на изображенията',
     'image_upload_success' => 'Изображението бе качено успешно',
     'image_update_success' => 'Данните за изобтажението са обновенни успешно',
     'image_delete_success' => 'Изображението е успешно изтрито',
-    'image_upload_remove' => 'Премахване',
 
     // Code Editor
     'code_editor' => 'Редактиране на кода',
index 33ed434f51bc616515b93f7d8982baca499e5949..67b3695c05b8b4355b9979f0633e3c8dafa9d5f9 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Прикачени файлове',
     'attachments_explain' => 'Прикачете файлове или линкове, които да са видими на вашата страница. Същите ще бъдат видими във вашето странично поле.',
     'attachments_explain_instant_save' => 'Промените тук се запазват веднага.',
-    'attachments_items' => 'Прикачен файл',
     'attachments_upload' => 'Прикачен файл',
     'attachments_link' => 'Прикачване на линк',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Поставяне на линк',
     'attachments_delete' => 'Сигурни ли сте, че искате да изтриете прикачения файл?',
-    'attachments_dropzone' => 'Поставете файлове или цъкнете тук за да прикачите файл',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Няма прикачени фалове',
     'attachments_explain_link' => 'Може да прикачите линк, ако не искате да качвате файл. Този линк може да бъде към друга страница или към файл в облакова пространство.',
     'attachments_link_name' => 'Има на линка',
index fe83dabe1ecdcd4370495598c64c38a16398e853..4c18a4ad0582e7aa1043ea77cb89259a6203d241 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Сървърът не може да създаде малки изображения. Моля, увери се, че разширението GD PHP е инсталирано.',
     'server_upload_limit' => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',
     'uploaded'  => 'Сървърът не позволява качвания с такъв размер. Моля, пробвайте файл с по-малък размер.',
-    'file_upload_timeout' => 'Качването на файла изтече.',
 
     // Drawing & Images
     'image_upload_error' => 'Възникна грешка при качването на изображението',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Прикачения файл не е намерен',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Неуспешно запазване на черновата. Увери се, че имаш свързаност с интернет преди да запазиш страницата',
index d40a95a9e547c3aa2829c69b2520cae5d2ff731e..191ac027df89285e05ad89a3b3b9b9dd5ab32f08 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Biraj sliku',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Sve',
     'image_all_title' => 'Pogledaj sve slike',
     'image_book_title' => 'Pogledaj slike prenesene u ovu knjigu',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Jeste li sigurni da želite obrisati ovu sliku?',
     'image_select_image' => 'Odaberi sliku',
     'image_dropzone' => 'Ostavi slike ili pritisnite ovdje da ih prenesete',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Slike su izbrisane',
     'image_preview' => 'Pregled Slike',
     'image_upload_success' => 'Slika uspješno učitana',
     'image_update_success' => 'Detalji slike uspješno ažurirani',
     'image_delete_success' => 'Slika uspješno izbrisana',
-    'image_upload_remove' => 'Ukloni',
 
     // Code Editor
     'code_editor' => 'Uredi Kod',
index e181f19a2c0e45f872eaa57073782fb414c89be6..07b891e025b4cafc07285e8620caff3ef4c74271 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Prilozi',
     'attachments_explain' => 'Učitajte fajlove ili priložite poveznice da bi ih prikazali na stranici. Oni su onda vidljivi u navigaciji sa strane.',
     'attachments_explain_instant_save' => 'Sve promjene se snimaju odmah.',
-    'attachments_items' => 'Priložene stavke',
     'attachments_upload' => 'Učitaj fajl',
     'attachments_link' => 'Zakači link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Postavi link',
     'attachments_delete' => 'Jeste li sigurni da želite obrisati ovaj prilog?',
-    'attachments_dropzone' => 'Spustite fajlove ili pritisnite ovdje da priložite fajl',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Niti jedan fajl nije prenesen',
     'attachments_explain_link' => 'Možete zakačiti link ako ne želite učitati fajl. To može biti link druge stranice ili link za fajl u oblaku.',
     'attachments_link_name' => 'Naziv linka',
index bb7617a77f317180b1c635b129b5954187a591d9..907a443095ac376e5fece893ab175d9e33106293 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Server ne može kreirati sličice. Provjerite da imate instaliranu GD PHP ekstenziju.',
     'server_upload_limit' => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',
     'uploaded'  => 'Server ne dopušta učitavanja ove veličine. Pokušajte sa manjom veličinom fajla.',
-    'file_upload_timeout' => 'Vrijeme učitavanja fajla je isteklo.',
 
     // Drawing & Images
     'image_upload_error' => 'Desila se greška prilikom učitavanja slike',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Prilog nije pronađen',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Snimanje skice nije uspjelo. Provjerite da ste povezani na internet prije snimanja ove stranice',
index d96582da7d5e9a80e2cb93e90a743d820912d0ba..fa6578e827ec8b70ba4582f136f092dc725d491c 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selecciona una imatge',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Totes',
     'image_all_title' => 'Mostra totes les imatges',
     'image_book_title' => 'Mostra les imatges pujades a aquest llibre',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Segur que voleu suprimir aquesta imatge?',
     'image_select_image' => 'Selecciona una imatge',
     'image_dropzone' => 'Arrossegueu imatges o feu clic aquí per a pujar-les',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Imatges suprimides',
     'image_preview' => 'Previsualització de la imatge',
     'image_upload_success' => 'Imatge pujada correctament',
     'image_update_success' => 'Detalls de la imatge actualitzats correctament',
     'image_delete_success' => 'Imatge suprimida correctament',
-    'image_upload_remove' => 'Suprimeix',
 
     // Code Editor
     'code_editor' => 'Edita el codi',
index d55260f47f5f006271e8ba8f3d461e15be8d0265..ec060242f1f460471c35743dd19c79c308fcad94 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Adjuncions',
     'attachments_explain' => 'Pugeu fitxers o adjunteu enllaços per a mostrar-los a la pàgina. Són visibles a la barra lateral de la pàgina.',
     'attachments_explain_instant_save' => 'Els canvis fets aquí es desen instantàniament.',
-    'attachments_items' => 'Elements adjunts',
     'attachments_upload' => 'Puja un fitxer',
     'attachments_link' => 'Adjunta un enllaç',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Defineix l\'enllaç',
     'attachments_delete' => 'Seguir que voleu suprimir aquesta adjunció?',
-    'attachments_dropzone' => 'Arrossegueu fitxers o feu clic aquí per a adjuntar un fitxer',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'No s\'ha pujat cap fitxer',
     'attachments_explain_link' => 'Podeu adjuntar un enllaç si preferiu no pujar un fitxer. Pot ser un enllaç a una altra pàgina o un enllaç a un fitxer al núvol.',
     'attachments_link_name' => 'Nom de l\'enllaç',
index ebcf63ef52dfd340ab99ee7e6d5e3d50ace67941..2f7112187e60c10e88e02a9def23157397113943 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'El servidor no pot crear miniatures. Reviseu que tingueu instal·lada l\'extensió GD del PHP.',
     'server_upload_limit' => 'El servidor no permet pujades d\'aquesta mida. Proveu-ho amb una mida de fitxer més petita.',
     'uploaded'  => 'El servidor no permet pujades d\'aquesta mida. Proveu-ho amb una mida de fitxer més petita.',
-    'file_upload_timeout' => 'La pujada del fitxer ha superat el temps màxim d\'espera.',
 
     // Drawing & Images
     'image_upload_error' => 'S\'ha produït un error en pujar la imatge',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'No s\'ha trobat l\'adjunció',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'No s\'ha pogut desar l\'esborrany. Assegureu-vos que tingueu connexió a Internet abans de desar la pàgina',
index 20df62e36066092af0f12f53691ccd55a1f2a8bb..4dffab0b4e5ea241b1a914ea64af15725fcc923b 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Výběr obrázku',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Vše',
     'image_all_title' => 'Zobrazit všechny obrázky',
     'image_book_title' => 'Zobrazit obrázky nahrané do této knihy',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Opravdu chcete odstranit tento obrázek?',
     'image_select_image' => 'Zvolte obrázek',
     'image_dropzone' => 'Přetáhněte obrázky nebo klikněte sem pro nahrání',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Obrázky odstraněny',
     'image_preview' => 'Náhled obrázku',
     'image_upload_success' => 'Obrázek byl nahrán',
     'image_update_success' => 'Podrobnosti o obrázku byly aktualizovány',
     'image_delete_success' => 'Obrázek byl odstraněn',
-    'image_upload_remove' => 'Odebrat',
 
     // Code Editor
     'code_editor' => 'Upravit kód',
index ac5694231d36c93085fb4ecf08af558fab6bf2ac..aca82c2928723634957312fe68f09ca84e11b394 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Přílohy',
     'attachments_explain' => 'Nahrajte soubory nebo připojte odkazy, které se zobrazí na stránce. Budou k nalezení v postranní liště.',
     'attachments_explain_instant_save' => 'Změny zde provedené se okamžitě ukládají.',
-    'attachments_items' => 'Připojené položky',
     'attachments_upload' => 'Nahrát soubor',
     'attachments_link' => 'Připojit odkaz',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Nastavit odkaz',
     'attachments_delete' => 'Jste si jisti, že chcete odstranit tuto přílohu?',
-    'attachments_dropzone' => 'Přetáhněte sem soubory myší nebo sem klikněte pro vybrání souboru',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Žádné soubory nebyly nahrány',
     'attachments_explain_link' => 'Můžete pouze připojit odkaz pokud nechcete nahrávat soubor přímo. Může to být odkaz na jinou stránku nebo na soubor v cloudu.',
     'attachments_link_name' => 'Název odkazu',
index b3ac5595dc39312a4c87d9875e3c8ab75a1c4199..4bc4ab450fa837705ac9aab1129971e5d14aea69 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Server nedokáže udělat náhledy. Zkontrolujte, že rozšíření GD pro PHP je nainstalováno.',
     'server_upload_limit' => 'Server nepovoluje nahrávat tak veliké soubory. Zkuste prosím menší soubor.',
     'uploaded'  => 'Server nepovoluje nahrávat tak veliké soubory. Zkuste prosím menší soubor.',
-    'file_upload_timeout' => 'Nahrávání souboru trvalo příliš dlouho a tak bylo ukončeno.',
 
     // Drawing & Images
     'image_upload_error' => 'Nastala chyba během nahrávání souboru',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Příloha nenalezena',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Nepovedlo se uložit koncept. Než stránku uložíte, ujistěte se, že jste připojeni k internetu.',
index 48a0a32faa38c4821a9d71dda9a5fb4f97d35232..cd5dca251bbc7ec59f779fb9d727b57dfbd2c0c2 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Image Select',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'All',
     'image_all_title' => 'View all images',
     'image_book_title' => 'View images uploaded to this book',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
     'image_select_image' => 'Select Image',
     'image_dropzone' => 'Drop images or click here to upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Images Deleted',
     'image_preview' => 'Image Preview',
     'image_upload_success' => 'Image uploaded successfully',
     'image_update_success' => 'Image details successfully updated',
     'image_delete_success' => 'Image successfully deleted',
-    'image_upload_remove' => 'Remove',
 
     // Code Editor
     'code_editor' => 'Edit Code',
index 9b02f31118a8c06d24043499112702fc5df18d98..9614f92fe1ff8b800ad087fff13c74ada1aabcbc 100644 (file)
@@ -311,12 +311,12 @@ return [
     '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_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     '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_dropzone' => 'Drop files here to upload',
     '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',
index 0f684998fca76481befafe7ab2793f2a7444200d..7d857e798ef5afd5a4f528166f8747ac68d7a2f9 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Ni all y gweinydd greu mân-luniau. Gwiriwch fod gennych yr estyniad GD PHP wedi\'i osod.',
     'server_upload_limit' => 'Nid yw\'r gweinydd yn caniatáu uwchlwythiadau o\'r maint hwn. Rhowch gynnig ar faint ffeil llai.',
     'uploaded'  => 'Nid yw\'r gweinydd yn caniatáu uwchlwythiadau o\'r maint hwn. Rhowch gynnig ar faint ffeil llai.',
-    'file_upload_timeout' => 'Mae\'r amser uwchlwytho ffeil wedi dod i ben.',
 
     // Drawing & Images
     'image_upload_error' => 'Bu gwall wrth uwchlwytho\'r ddelwedd',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Ni chanfuwyd yr atodiad',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Wedi methu cadw\'r drafft. Sicrhewch fod gennych gysylltiad rhyngrwyd cyn cadw\'r dudalen hon',
index 9ba511fc5124a2cc5a6bf7c05395796c6bc4c77c..bac7ec39ac96ae6f3de8c9ff54d5499e3c09dc40 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Billedselektion',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alt',
     'image_all_title' => 'Se alle billeder',
     'image_book_title' => 'Vis billeder uploadet til denne bog',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Er du sikker på at du vil slette dette billede?',
     'image_select_image' => 'Vælg billede',
     'image_dropzone' => 'Træk-og-slip billede eller klik her for at uploade',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Billede slettet',
     'image_preview' => 'Billedeksempel',
     'image_upload_success' => 'Foto uploadet',
     'image_update_success' => 'Billeddetaljer succesfuldt opdateret',
     'image_delete_success' => 'Billede slettet',
-    'image_upload_remove' => 'Fjern',
 
     // Code Editor
     'code_editor' => 'Rediger kode',
index c2d0ca92c70f68bbd011ebe1d949efd4d8986197..522fa7d7abde762bc0d744f483890e7b707435e0 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Vedhæftninger',
     'attachments_explain' => 'Upload nogle filer, eller vedhæft nogle links, der skal vises på siden. Disse er synlige i sidepanelet.',
     'attachments_explain_instant_save' => 'Ændringer her gemmes med det samme.',
-    'attachments_items' => 'Vedhæftede emner',
     'attachments_upload' => 'Upload fil',
     'attachments_link' => 'Vedhæft link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Sæt link',
     'attachments_delete' => 'Er du sikker på at du vil slette denne vedhæftning?',
-    'attachments_dropzone' => 'Slip filer eller klik her for at vedhæfte en fil',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Ingen filer er blevet overført',
     'attachments_explain_link' => 'Du kan vedhæfte et link, hvis du foretrækker ikke at uploade en fil. Dette kan være et link til en anden side eller et link til en fil i skyen.',
     'attachments_link_name' => 'Linknavn',
index 75ef6da20d3eceac95973ef22a092f1bc125570d..cc021ef59bb93a1e4912ce631dc6ef84d201b65b 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Serveren kan ikke oprette miniaturer. Kontroller, at GD PHP-udvidelsen er installeret.',
     'server_upload_limit' => 'Serveren tillader ikke uploads af denne størrelse. Prøv en mindre filstørrelse.',
     'uploaded'  => 'Serveren tillader ikke uploads af denne størrelse. Prøv en mindre filstørrelse.',
-    'file_upload_timeout' => 'Filuploaden udløb.',
 
     // Drawing & Images
     'image_upload_error' => 'Der opstod en fejl ved upload af billedet',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Vedhæftning ikke fundet',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Kunne ikke gemme kladde. Tjek at du har internetforbindelse før du gemmer siden',
index 7036888f47e361490c008a6f3c0825805f2cfc2f..c5ed9ab821fd16039de1e440d279d5186a2a6325 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Bild auswählen',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alle',
     'image_all_title' => 'Alle Bilder anzeigen',
     'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Möchten Sie dieses Bild wirklich löschen?',
     'image_select_image' => 'Bild auswählen',
     'image_dropzone' => 'Ziehen Sie Bilder hierher oder klicken Sie hier, um ein Bild auszuwählen',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Bilder gelöscht',
     'image_preview' => 'Bildvorschau',
     'image_upload_success' => 'Bild erfolgreich hochgeladen',
     'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
     'image_delete_success' => 'Bild erfolgreich gelöscht',
-    'image_upload_remove' => 'Entfernen',
 
     // Code Editor
     'code_editor' => 'Code editieren',
index 9415e9cbb2970fccdd7104b4033f98e742ae13f3..57c94b4d48e2e2183123c349497b5e03ccb7250c 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Anhänge',
     'attachments_explain' => 'Sie können auf Ihrer Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
     'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
-    'attachments_items' => 'Angefügte Elemente',
     'attachments_upload' => 'Datei hochladen',
     'attachments_link' => 'Link hinzufügen',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Link setzen',
     'attachments_delete' => 'Sind Sie sicher, dass Sie diesen Anhang löschen möchten?',
-    'attachments_dropzone' => 'Ziehen Sie Dateien hierher oder klicken Sie, um eine Datei auszuwählen',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
     'attachments_explain_link' => 'Wenn Sie keine Datei hochladen möchten, können Sie stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet weisen.',
     'attachments_link_name' => 'Link-Name',
index d164674ef615f28e2be9daf8b799d54a21f62849..92d930cd693ebf5d79b156e943801f1cc4c07e9e 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfen Sie, ob die GD PHP-Erweiterung installiert ist.',
     'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
     'uploaded'  => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuchen Sie es mit einer kleineren Datei.',
-    'file_upload_timeout' => 'Der Datei-Upload hat das Zeitlimit überschritten.',
 
     // Drawing & Images
     'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Anhang konnte nicht gefunden werden.',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
index 2a9782fcaf6b584ef3eaaee1c5982217476b6ffe..bcea984278a8cfb2fd5b5446903f6580a357124c 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Bild auswählen',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alle',
     'image_all_title' => 'Alle Bilder anzeigen',
     'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Bist Du sicher, dass Du diese Seite löschen möchtest?',
     'image_select_image' => 'Bild auswählen',
     'image_dropzone' => 'Ziehe Bilder hierher oder klicke hier, um ein Bild auszuwählen',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Bilder gelöscht',
     'image_preview' => 'Bildvorschau',
     'image_upload_success' => 'Bild erfolgreich hochgeladen',
     'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
     'image_delete_success' => 'Bild erfolgreich gelöscht',
-    'image_upload_remove' => 'Entfernen',
 
     // Code Editor
     'code_editor' => 'Code editieren',
index dcefac0d1abf519586f6d09d1c4a55f814777b53..8e1274f0ee8ed2d3c224f443d17cb5bcbcae7e21 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Anhänge',
     'attachments_explain' => 'Du kannst auf deiner Seite Dateien hochladen oder Links hinzufügen. Diese werden in der Seitenleiste angezeigt.',
     'attachments_explain_instant_save' => 'Änderungen werden direkt gespeichert.',
-    'attachments_items' => 'Angefügte Elemente',
     'attachments_upload' => 'Datei hochladen',
     'attachments_link' => 'Link hinzufügen',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Link setzen',
     'attachments_delete' => 'Bist du sicher, dass du diesen Anhang löschen möchtest?',
-    'attachments_dropzone' => 'Ziehe Dateien hierher oder klicke hier, um eine Datei auszuwählen',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
     'attachments_explain_link' => 'Wenn du keine Datei hochladen möchtest, kannst du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.',
     'attachments_link_name' => 'Link-Name',
index af38663ae3af5cc45f8e2d4426e72567e6c83a41..99ed10164852963694126053d6be48b2762cbbe8 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfe, ob die GD PHP-Erweiterung installiert ist.',
     'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.',
     'uploaded'  => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.',
-    'file_upload_timeout' => 'Der Upload der Datei ist abgelaufen.',
 
     // Drawing & Images
     'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Anhang konnte nicht gefunden werden.',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass du mit dem Internet verbunden bist, bevor du den Entwurf dieser Seite speicherst.',
index c59e93addc231d028d1a8d8c51396b26cbdec27a..5ec24a0495ae4202288998519937b3ded55c8e7b 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Επιλογή εικόνας',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Όλες',
     'image_all_title' => 'Δείτε όλες τις εικόνες που υπάρχουν στο Server',
     'image_book_title' => 'Προβολή εικόνων που έχουν μεταφορτωθεί σε αυτό το βιβλίο',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Είστε σίγουροι ότι θέλετε να διαγράψετε αυτήν την εικόνα;',
     'image_select_image' => 'Επιλέξτε Εικόνα',
     'image_dropzone' => 'Σύρτε ή κάντε κλικ εδώ για μεταφόρτωση εικόνων',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Οι εικόνες διαγράφηκαν',
     'image_preview' => 'Προεπισκόπηση εικόνας',
     'image_upload_success' => 'Η εικόνα μεταφορτώθηκε με επιτυχία',
     'image_update_success' => 'Τα στοιχεία της εικόνας ενημερώθηκαν με επιτυχία',
     'image_delete_success' => 'Η εικόνα διαγράφηκε επιτυχώς',
-    'image_upload_remove' => 'Αφαίρεση',
 
     // Code Editor
     'code_editor' => 'Επεξεργασία κώδικα',
index 6904bced44880d35d899e52eab646ec9c7421de0..ae626bef88a93bf92ba9243aa4f0c124da41138f 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Συνημμένα',
     'attachments_explain' => 'Ανεβάστε μερικά αρχεία ή επισυνάψτε μερικούς συνδέσμους για να εμφανίσετε στη σελίδα σας. Αυτά είναι ορατά στην πλαϊνή μπάρα σελίδας.',
     'attachments_explain_instant_save' => 'Οι αλλαγές εδώ αποθηκεύονται αμέσως.',
-    'attachments_items' => 'Συνημμένα Στοιχεία',
     'attachments_upload' => 'Μεταφόρτωση Αρχείου',
     'attachments_link' => 'Επισύναψη Δεσμού',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Ορισμός Συνδέσμου',
     'attachments_delete' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το συνημμένο;',
-    'attachments_dropzone' => 'Αποθέστε αρχεία ή κάντε κλικ εδώ για να επισυνάψετε ένα αρχείο',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Δεν έχουν μεταφορτωθεί αρχεία',
     'attachments_explain_link' => 'Μπορείτε να επισυνάψετε έναν σύνδεσμο αν προτιμάτε να μην ανεβάσετε ένα αρχείο. Αυτό μπορεί να είναι ένας σύνδεσμος σε άλλη σελίδα ή ένας σύνδεσμος σε ένα αρχείο στο σύννεφο.',
     'attachments_link_name' => 'Όνομα Συνδέσμου',
index ea8807b58ad0923aec3794a08447a34914bcd27a..d4edfcb61c4c164d70812fb1632fcce4ae25a26e 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Ο διακομιστής δεν μπορεί να δημιουργήσει μικρογραφίες. Παρακαλώ ελέγξτε ότι έχετε την επέκταση GD PHP εγκατεστημένη.',
     'server_upload_limit' => 'Ο διακομιστής δεν επιτρέπει τη μεταφόρτωση αυτού του μεγέθους. Παρακαλώ δοκιμάστε ένα μικρότερο μέγεθος αρχείου.',
     'uploaded'  => 'Ο διακομιστής δεν επιτρέπει τη μεταφόρτωση αυτού του μεγέθους. Παρακαλώ δοκιμάστε ένα μικρότερο μέγεθος αρχείου.',
-    'file_upload_timeout' => 'Το χρονικό όριο μεταφόρτωσης αρχείου έληξε.',
 
     // Drawing & Images
     'image_upload_error' => 'Παρουσιάστηκε σφάλμα κατά το ανέβασμα της εικόνας.',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Το συνημμένο δεν βρέθηκε',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Αποτυχία αποθήκευσης προσχέδιου. Βεβαιωθείτε ότι έχετε σύνδεση στο διαδίκτυο πριν την αποθήκευση αυτής της σελίδας',
index 48a0a32faa38c4821a9d71dda9a5fb4f97d35232..cd5dca251bbc7ec59f779fb9d727b57dfbd2c0c2 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Image Select',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'All',
     'image_all_title' => 'View all images',
     'image_book_title' => 'View images uploaded to this book',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
     'image_select_image' => 'Select Image',
     'image_dropzone' => 'Drop images or click here to upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Images Deleted',
     'image_preview' => 'Image Preview',
     'image_upload_success' => 'Image uploaded successfully',
     'image_update_success' => 'Image details successfully updated',
     'image_delete_success' => 'Image successfully deleted',
-    'image_upload_remove' => 'Remove',
 
     // Code Editor
     'code_editor' => 'Edit Code',
index 9b02f31118a8c06d24043499112702fc5df18d98..9614f92fe1ff8b800ad087fff13c74ada1aabcbc 100644 (file)
@@ -311,12 +311,12 @@ return [
     '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_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     '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_dropzone' => 'Drop files here to upload',
     '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',
index 703d0edbef0635a4963c2eb7e294f0d7c65bf0b1..6991f96e4359facd80e789406e28054c8432b2b0 100644 (file)
@@ -45,7 +45,6 @@ return [
     '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.',
-    'file_upload_timeout' => 'The file upload has timed out.',
 
     // Drawing & Images
     'image_upload_error' => 'An error occurred uploading the image',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Attachment not found',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
index fb4929ad4682313af337e76df503b64342dfdb61..753ddc95323471e6179704a70a8f68083490c234 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Seleccionar Imagen',
+    'image_upload' => 'Subir imagen',
+    'image_intro' => 'Aquí puede seleccionar y administrar las imágenes que se han subido previamente al sistema.',
+    'image_intro_upload' => 'Suba una nueva imagen arrastrando un archivo de imagen en esta ventana, o usando el botón "Subir imagen" de arriba.',
     'image_all' => 'Todas',
     'image_all_title' => 'Ver todas las imágenes',
     'image_book_title' => 'Ver las imágenes subidas a este libro',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => '¿Estás seguro de que quieres eliminar esta imagen?',
     'image_select_image' => 'Seleccionar Imagen',
     'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
+    'image_dropzone_drop' => 'Arrastre las imágenes aquí para subirlas',
     'images_deleted' => 'Imágenes borradas',
     'image_preview' => 'Previsualización de la imagen',
     'image_upload_success' => 'Imagen subida éxitosamente',
     'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
     'image_delete_success' => 'Imagen borrada exitosamente',
-    'image_upload_remove' => 'Borrar',
 
     // Code Editor
     'code_editor' => 'Editar Código',
index 8251bf34560613a51b0fb9f70e1a333198b0c833..0d9da9305d0df7c10ac5efa4a99acb293ee6f74c 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Adjuntos',
     'attachments_explain' => 'Subir ficheros o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
     'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
-    'attachments_items' => 'Elementos adjuntados',
     'attachments_upload' => 'Subir Archivo',
     'attachments_link' => 'Adjuntar Enlace',
+    'attachments_upload_drop' => 'También puedes arrastrar y soltar un archivo aquí para subirlo como un archivo adjunto.',
     'attachments_set_link' => 'Ajustar Enlace',
     'attachments_delete' => '¿Está seguro de que quiere eliminar este archivo adjunto?',
-    'attachments_dropzone' => 'Arrastre ficheros aquí o haga click aquí para adjuntar un fichero',
+    'attachments_dropzone' => 'Arrastre aquí archivos para subirlos',
     'attachments_no_files' => 'No se han subido ficheros',
     'attachments_explain_link' => 'Puede agregar un enlace si prefiere no subir un archivo. Puede ser un enlace a otra página o un enlace a un fichero en la nube.',
     'attachments_link_name' => 'Nombre del Enlace',
index 508b8c2952f7b44e30eebfa65fa8ca24fee57eeb..4080cca874006f40411504daef69d1c49cf4b671 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'El servidor no puede crear la miniatura de la imagen. Compruebe que tiene la extensión PHP GD instalada.',
     'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamaño. Intente subir un fichero de menor tamaño.',
     'uploaded'  => 'El servidor no permite la subida de ficheros de este tamaño. Intente subir un fichero de menor tamaño.',
-    'file_upload_timeout' => 'La carga del archivo ha caducado.',
 
     // Drawing & Images
     'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'No se encontró el adjunto',
+    'attachment_upload_error' => 'Ha ocurrido un error al subir el archivo adjunto',
 
     // Pages
     'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegúrese de que tiene conexión a Internet antes de guardar este borrador',
index f7be885898a4ac7b6d5374b2bd55f5afac2f7286..3577afed32e9ab72dfd21673d2b5778c6b9021a3 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Seleccionar Imagen',
+    'image_upload' => 'Subir imagen',
+    'image_intro' => 'Aquí puede seleccionar y administrar las imágenes que se han subido previamente al sistema.',
+    'image_intro_upload' => 'Suba una nueva imagen arrastrando un archivo de imagen en esta ventana, o usando el botón "Subir imagen" de arriba.',
     'image_all' => 'Todo',
     'image_all_title' => 'Ver todas las imágenes',
     'image_book_title' => 'Ver las imágenes subidas a este libro',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => '¿Está seguro que quiere eliminar esta imagen?',
     'image_select_image' => 'Seleccionar Imagen',
     'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
+    'image_dropzone_drop' => 'Arrastre las imágenes aquí para subirlas',
     'images_deleted' => 'Imágenes borradas',
     'image_preview' => 'Preview de la imagen',
     'image_upload_success' => 'Imagen subida éxitosamente',
     'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
     'image_delete_success' => 'Imagen borrada exitosamente',
-    'image_upload_remove' => 'Quitar',
 
     // Code Editor
     'code_editor' => 'Editar Código',
index a0a9c2d5486f7dca9737661d2c021b15a9f07e10..b44e7dec6d4103b95dcd158beb41c471c29f0741 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Adjuntos',
     'attachments_explain' => 'Subir archivos o agregar enlaces para mostrar en la página. Estos son visibles en la barra lateral de la página.',
     'attachments_explain_instant_save' => 'Los cambios se guardan de manera instantánea.',
-    'attachments_items' => 'Elementos adjuntados',
     'attachments_upload' => 'Archivo adjuntado',
     'attachments_link' => 'Adjuntar enlace',
+    'attachments_upload_drop' => 'También puedes arrastrar y soltar un archivo aquí para subirlo como un archivo adjunto.',
     'attachments_set_link' => 'Establecer enlace',
     'attachments_delete' => '¿Está seguro que desea eliminar el archivo adjunto?',
-    'attachments_dropzone' => 'Arrastre archivos aquí o presione aquí para adjuntar un archivo',
+    'attachments_dropzone' => 'Arrastre aquí archivos para subirlos',
     'attachments_no_files' => 'No se adjuntó ningún archivo',
     'attachments_explain_link' => 'Usted puede agregar un enlace o si lo prefiere puede agregar un archivo. Esto puede ser un enlace a otra página o un enlace a un archivo en la nube.',
     'attachments_link_name' => 'Nombre del enlace',
index 4607eb10206790afd30b195f23631fd2f11f0145..5eadf97845aa1c2ee2cbd271e336e371b19142d5 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
     'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
     'uploaded'  => 'El servidor no permite subir archivos de este tamaño. Por favor intente un tamaño menor.',
-    'file_upload_timeout' => 'La carga del archivo ha caducado.',
 
     // Drawing & Images
     'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'No se encuentra el objeto adjunto',
+    'attachment_upload_error' => 'Ha ocurrido un error al subir el archivo adjunto',
 
     // Pages
     'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',
index aeb9c3f3037d2a91d0c063c460def51365fcfaf3..7771c495ac3633337e137099a9e0a47667c9439a 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Pildifaili valik',
+    'image_upload' => 'Laadi pilt üles',
+    'image_intro' => 'Siin saad valida ja hallata pilte, mis on eelnevalt süsteemi üles laaditud.',
+    'image_intro_upload' => 'Laadi uus pilt üles pildifaili sellesse aknasse lohistades või ülal "Laadi pilt üles" nupu abil.',
     'image_all' => 'Kõik',
     'image_all_title' => 'Vaata kõiki pildifaile',
     'image_book_title' => 'Vaata sellesse raamatusse laaditud pildifaile',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Kas oled kindel, et soovid selle pildifaili kustutada?',
     'image_select_image' => 'Vali pildifail',
     'image_dropzone' => 'Üleslaadimiseks lohista pildid või klõpsa siin',
+    'image_dropzone_drop' => 'Üleslaadimiseks lohista pildid siia',
     'images_deleted' => 'Pildifailid kustutatud',
     'image_preview' => 'Pildi eelvaade',
     'image_upload_success' => 'Pildifail üles laaditud',
     'image_update_success' => 'Pildifaili andmed muudetud',
     'image_delete_success' => 'Pildifail kustutatud',
-    'image_upload_remove' => 'Eemalda',
 
     // Code Editor
     'code_editor' => 'Muuda koodi',
index 9f356f95fa2ff436fc48b5588120b442a016da29..580e13cdb0983645e43e4f76d07974a4264620fc 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Manused',
     'attachments_explain' => 'Laadi üles faile või lisa linke, mida lehel kuvada. Need on nähtavad külgmenüüs.',
     'attachments_explain_instant_save' => 'Muudatused salvestatakse koheselt.',
-    'attachments_items' => 'Lisatud objektid',
     'attachments_upload' => 'Laadi fail üles',
     'attachments_link' => 'Lisa link',
+    'attachments_upload_drop' => 'Saad ka faile siia lohistada, et neid manusena üles laadida.',
     'attachments_set_link' => 'Määra link',
     'attachments_delete' => 'Kas oled kindel, et soovid selle manuse kustutada?',
-    'attachments_dropzone' => 'Manuse lisamiseks lohista failid või klõpsa siin',
+    'attachments_dropzone' => 'Lohista failid üleslaadimiseks siia',
     'attachments_no_files' => 'Üleslaaditud faile ei ole',
     'attachments_explain_link' => 'Faili üleslaadimise asemel saad lingi lisada. See võib viidata teisele lehele või failile kuskil pilves.',
     'attachments_link_name' => 'Lingi nimi',
index 06f9970a110d41ce752f83d2e2ee8d15f26ddcc3..a6e17716aa94a33609ababefce315963ba13639a 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Server ei saa piltide eelvaateid tekitada. Veendu, et PHP GD laiendus on paigaldatud.',
     'server_upload_limit' => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',
     'uploaded'  => 'Server ei luba nii suurte failide üleslaadimist. Proovi väiksema failiga.',
-    'file_upload_timeout' => 'Faili üleslaadimine aegus.',
 
     // Drawing & Images
     'image_upload_error' => 'Pildi üleslaadimisel tekkis viga',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Manust ei leitud',
+    'attachment_upload_error' => 'Manuse faili üleslaadimisel tekkis viga',
 
     // Pages
     'page_draft_autosave_fail' => 'Mustandi salvestamine ebaõnnestus. Kontrolli oma internetiühendust',
index db20ffc7d29a64d98c816c579e15c1e19d9b54b0..e44a4143b8e559cb802171381f91b15d59d593c5 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Irudia aukeratu',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Guztiak',
     'image_all_title' => 'Irudi guztiak ikusi',
     'image_book_title' => 'Liburu honetan igotako irudiak ikusi',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Ziur irudi hau ezabatu nahi duzula?',
     'image_select_image' => 'Irudia aukeratu',
     'image_dropzone' => 'Irudiak bota edo klikatu hemen igotzeko',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Ezabatutako irudiak',
     'image_preview' => 'Irudiaren aurrebista',
     'image_upload_success' => 'Irudia zuzen eguneratu da',
     'image_update_success' => 'Irudiaren xehetasunak egioki eguneratu dira',
     'image_delete_success' => 'Irudia ondo ezabatu da',
-    'image_upload_remove' => 'Ezabatu',
 
     // Code Editor
     'code_editor' => 'Kodea editatu',
index 260c8751615ee4da7fc365783700f2e2c9835e01..cc733365fd671b87d7ac962c94141bbf2f65dd02 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Eranskinak',
     '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' => 'Atxikiak',
     'attachments_upload' => 'Kargatu artxiboak',
     'attachments_link' => 'Attach Link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     '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_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Ez da igo fitxategirik',
     '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' => 'Loturaren izena',
index 8673da63028588a878432782c811588edcfdf944..09dd6bdf35ff5431d4ba84b23af52277cd5a53c0 100644 (file)
@@ -45,7 +45,6 @@ return [
     '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.',
-    'file_upload_timeout' => 'The file upload has timed out.',
 
     // Drawing & Images
     'image_upload_error' => 'Errorea gertatu da irudia igotzerakoan',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Atxikia ez da aurkitu',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
index 126bd093db9854d58c2d6b2718b86b605d4e996a..6aab79bb6332bca0879b85dc3118ff43754acf39 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'انتخاب تصویر',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'همه',
     'image_all_title' => 'نمایش تمام تصاویر',
     'image_book_title' => 'تصاویر بارگذاری شده در این کتاب را مشاهده کنید',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'آیا مطمئن هستید که میخواهید این عکس را پاک کنید؟',
     'image_select_image' => 'انتخاب تصویر',
     'image_dropzone' => 'تصاویر را رها کنید یا برای بارگذاری اینجا را کلیک کنید',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'تصاویر حذف شده',
     'image_preview' => 'پیش نمایش تصویر',
     'image_upload_success' => 'تصویر با موفقیت بارگذاری شد',
     'image_update_success' => 'جزئیات تصویر با موفقیت به روز شد',
     'image_delete_success' => 'تصویر با موفقیت حذف شد',
-    'image_upload_remove' => 'حذف',
 
     // Code Editor
     'code_editor' => 'ویرایش کد',
index 186c2e418ed9843afc23cc80032bd511d97f4c9f..4d6fde9da5869e115264a0f7b169f47c17b5bdc9 100644 (file)
@@ -23,7 +23,7 @@ return [
     'meta_updated' => 'به روزرسانی شده :timeLength',
     'meta_updated_name' => 'به روزرسانی شده :timeLength توسط :user',
     'meta_owned_name' => 'متعلق به :user',
-    'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
+    'meta_reference_page_count' => 'در 1 صفحه به آن ارجاع داده شده|در :count صفحه به آن ارجاع داده شده',
     'entity_select' => 'انتخاب موجودیت',
     'entity_select_lack_permission' => 'شما مجوزهای لازم برای انتخاب این مورد را ندارید',
     'images' => 'عکس‌ها',
@@ -49,7 +49,7 @@ return [
     'permissions_owner' => 'مالک',
     'permissions_role_everyone_else' => 'سایر کاربران',
     'permissions_role_everyone_else_desc' => 'مجوزها را برای نقش‌هایی تنظیم کنید که به طور خاص لغو نشده‌اند.',
-    'permissions_role_override' => 'لغو مجوز برای نقش',
+    'permissions_role_override' => 'تنظیم مجدد مجوز برای نقش',
     'permissions_inherit_defaults' => 'ارث بردن از مجوزهای پیش‌فرض',
 
     // Search
@@ -236,8 +236,8 @@ return [
     'pages_md_insert_image' => 'درج تصویر',
     'pages_md_insert_link' => 'پیوند نهاد را درج کنید',
     'pages_md_insert_drawing' => 'درج طرح',
-    'pages_md_show_preview' => 'Show preview',
-    'pages_md_sync_scroll' => 'Sync preview scroll',
+    'pages_md_show_preview' => 'دیدن پیش نمایش',
+    'pages_md_sync_scroll' => 'هماهنگ سازی اسکرول پیش نمایش',
     'pages_not_in_chapter' => 'صفحه در یک فصل نیست',
     'pages_move' => 'انتقال صفحه',
     'pages_move_success' => 'صفحه به ":parentName" منتقل شد',
@@ -292,7 +292,7 @@ return [
     'shelf_tags' => 'برچسب های قفسه',
     'tag' => 'برچسب',
     'tags' =>  'برچسب ها',
-    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
+    'tags_index_desc' => 'تگ ها را میتوان به محتوای داخل سیستم اعمال کرد تا فرم هماهنگی از طبقه‌بندی ایجاد شود. تگ ها می توانند شامل یک کلید و یک مقدار باشند، که مقدار آن انتخابی یا قابل خذف است. بعد از ایجاد تگ، محتوا را می توان توسط کلید یا مقدار هر تگ جستجو نمود.',
     'tag_name' =>  'نام برچسب',
     'tag_value' => 'مقدار برچسب (اختیاری)',
     'tags_explain' => "برای دسته بندی بهتر مطالب خود چند برچسب اضافه کنید.\nمی توانید برای سازماندهی عمیق‌تر، یک مقدار به یک برچسب اختصاص دهید.",
@@ -311,12 +311,12 @@ return [
     'attachments' => 'پیوست ها',
     'attachments_explain' => 'چند فایل را آپلود کنید یا چند پیوند را برای نمایش در صفحه خود ضمیمه کنید. اینها در نوار کناری صفحه قابل مشاهده هستند.',
     'attachments_explain_instant_save' => 'تغییرات در اینجا فورا ذخیره می شوند.',
-    'attachments_items' => 'موارد پیوست شده',
     'attachments_upload' => 'آپلود فایل',
     'attachments_link' => 'پیوند را ضمیمه کنید',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'پیوند را تنظیم کنید',
     'attachments_delete' => 'آیا مطمئن هستید که می خواهید این پیوست را حذف کنید؟',
-    'attachments_dropzone' => 'فایل ها را رها کنید یا برای پیوست کردن یک فایل اینجا را کلیک کنید',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'هیچ فایلی آپلود نشده است',
     'attachments_explain_link' => 'اگر ترجیح می دهید فایلی را آپلود نکنید، می توانید پیوندی را پیوست کنید. این می تواند پیوندی به صفحه دیگر یا پیوندی به فایلی در فضای ابری باشد.',
     'attachments_link_name' => 'نام پیوند',
@@ -395,6 +395,6 @@ return [
 
     // References
     'references' => 'مراجع',
-    'references_none' => 'There are no tracked references to this item.',
+    'references_none' => 'هیچ رفرنسی برای این قلم یافت نشد.',
     'references_to_desc' => 'در زیر تمام صفحات شناخته شده در سیستم که به این مورد پیوند دارند، نشان داده شده است.',
 ];
index 8bb5d71cd560670e4bc502b79bee2f3b52afe874..bb31ff5cd337d4d740d07ac9f8936811e728948a 100644 (file)
@@ -45,15 +45,15 @@ return [
     'cannot_create_thumbs' => 'سرور نمی تواند تصاویر کوچک ایجاد کند. لطفاً بررسی کنید که پسوند GD PHP را نصب کرده اید.',
     'server_upload_limit' => 'سرور اجازه آپلود در این اندازه را نمی دهد. لطفا اندازه فایل کوچکتر را امتحان کنید.',
     'uploaded'  => 'سرور اجازه آپلود در این اندازه را نمی دهد. لطفا اندازه فایل کوچکتر را امتحان کنید.',
-    'file_upload_timeout' => 'زمان بارگذاری فایل به پایان رسیده است.',
 
     // Drawing & Images
     'image_upload_error' => 'هنگام آپلود تصویر خطایی روی داد',
     'image_upload_type_error' => 'نوع تصویر در حال آپلود نامعتبر است',
-    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',
+    'drawing_data_not_found' => 'داده های طرح قابل بارگذاری نیستند. ممکن است فایل طرح دیگر وجود نداشته باشد یا شما به آن دسترسی نداشته باشید.',
 
     // Attachments
     'attachment_not_found' => 'پیوست یافت نشد',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'پیش نویس ذخیره نشد. قبل از ذخیره این صفحه مطمئن شوید که به اینترنت متصل هستید',
index d3018f32b14e9b86f031cdd8363ae0102c814095..7c8bb76f7f9d8bb0cb0c3103cb3781b35663a4bb 100644 (file)
@@ -50,11 +50,11 @@ return [
 
     // Color settings
     'color_scheme' => 'ترکیب رنگی برنامه',
-    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',
-    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',
-    'app_color' => 'Primary Color',
-    'link_color' => 'Default Link Color',
-    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
+    'color_scheme_desc' => 'رنگهایی که در رابط کاربری نرم‌افزار استفاده می شوند را انتخاب کنید. رنگها را میتوان برای حالت روشن یا تیره به صورت جداگانه تنظیم کرد تا هم با تم مورد استفاده سازگار بوده و هم خوانا باشند.',
+    'ui_colors_desc' => 'رنگ اصلی نرم‌افزار و رنگ پیش فرض پیوندها را انتخاب کنید. رنگ اصلی بیشتر برای بنر، کلیدها و عناصر تزیینی رابط کاربری استفاده می شوند. رنگ پیش فرض پیوند برای پیوندهای متنی و اکشن ها بکار میرود، هم در محتوای متنی و هم در رابط کاربری.',
+    'app_color' => 'رنگ اصلی',
+    'link_color' => 'رنگ پیش فرض پیوند',
+    'content_colors_desc' => 'رنگهای عناصر سلسه مراتب صفحه را انتخاب کنید. پیشنهاد می شود رنگهایی انتخاب گردند که با رنگ پیش فرض دارای روشنی مشابه باشند، تا به خوانایی کمک شود.',
     'bookshelf_color' => 'رنگ قفسه',
     'book_color' => 'رنگ کتاب',
     'chapter_color' => 'رنگ فصل',
@@ -93,10 +93,10 @@ return [
     'maint_send_test_email_mail_text' => 'تبریک می گویم! با دریافت این اعلان ایمیل، به نظر می رسد تنظیمات ایمیل شما به درستی پیکربندی شده است.',
     'maint_recycle_bin_desc' => 'قفسه‌ها، کتاب‌ها، فصل‌ها و صفحات حذف‌شده به سطل بازیافت فرستاده می‌شوند تا بتوان آن‌ها را بازیابی کرد یا برای همیشه حذف کرد. بسته به پیکربندی سیستم، اقلام قدیمی در سطل بازیافت ممکن است پس از مدتی به طور خودکار حذف شوند.',
     'maint_recycle_bin_open' => 'سطل بازیافت را باز کنید',
-    'maint_regen_references' => 'Regenerate References',
-    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
-    'maint_regen_references_success' => 'Reference index has been regenerated!',
-    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+    'maint_regen_references' => 'تولید مجدد رفرنس ها',
+    'maint_regen_references_desc' => 'این عمل اندیس رفرنس های میان اقلامی موجود در دیتابیس را بازسازی خواهد کرد. چنین کاری معمولا به صورت خودکار انجام میشود، ولی این عمل برای اندیس گذاری محتویات قدیمی یا محتویایی که از طرق غیرمعمول اضافه شده اند مفید است.',
+    'maint_regen_references_success' => 'اندیس رفرنس ها بازسازی شدند!',
+    'maint_timeout_command_note' => 'توجه: این عملیات زمان بر خواهد بود، که ممکن در برخی محیطهای وب باعث از دسترس خارج شدن شوند. به عنوان گزینه جایگزین، این عمل را میتوان با استفاده از یک دستور ترمینال انجام داد.',
 
     // Recycle Bin
     'recycle_bin' => 'سطل زباله',
@@ -141,7 +141,7 @@ return [
     'roles_x_users_assigned' => ':count کاربر اختصاص داده شده|:count کاربر اختصاص داده شده',
     'roles_x_permissions_provided' => ':count مجوز|:count مجوز',
     'roles_assigned_users' => 'کاربران اختصاص داده شده',
-    'roles_permissions_provided' => 'Provided Permissions',
+    'roles_permissions_provided' => 'دسترسی های موجود',
     'role_create' => 'نقش جدید ایجاد کنید',
     'role_delete' => 'حذف نقش',
     'role_delete_confirm' => 'با این کار نقش با نام \':roleName\' حذف می شود.',
@@ -168,7 +168,7 @@ return [
     'roles_system_warning' => 'توجه داشته باشید که دسترسی به هر یک از سه مجوز فوق می‌تواند به کاربر اجازه دهد تا امتیازات خود یا امتیازات دیگران را در سیستم تغییر دهد. فقط نقش هایی را با این مجوزها به کاربران مورد اعتماد اختصاص دهید.',
     'role_asset_desc' => 'این مجوزها دسترسی پیش‌فرض به دارایی‌های درون سیستم را کنترل می‌کنند. مجوزهای مربوط به کتاب‌ها، فصل‌ها و صفحات این مجوزها را لغو می‌کنند.',
     'role_asset_admins' => 'به ادمین‌ها به‌طور خودکار به همه محتوا دسترسی داده می‌شود، اما این گزینه‌ها ممکن است گزینه‌های UI را نشان داده یا پنهان کنند.',
-    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
+    'role_asset_image_view_note' => 'این مربوط به مرئی بودن در بخش مدیر تصاویر است. دسترسی عملی به تصاویر آپلود شده بستگی به گزینه ذخیره‌سازی تصویر سیستم دارد.',
     'role_all' => 'همه',
     'role_own' => 'صاحب',
     'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود',
@@ -178,7 +178,7 @@ return [
 
     // Users
     'users' => 'کاربران',
-    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
+    'users_index_desc' => 'ساخت و مدیریت حساب های کاربری درون سیستم. حساب های کاربری برای ورود به سیستم و تخصیص محتوا و فعالیت ها به کار می روند. اجازه های دسترسی عموما بر مبنای نقش کاربر هستند، ولی مالکیت محتوای کاربر (در کنار سایر عوامل) روی اجازه و دسترسی تاثیر گذارند.',
     'user_profile' => 'پرونده کاربر',
     'users_add_new' => 'افزودن کاربر جدید',
     'users_search' => 'جستجوی کاربران',
@@ -248,7 +248,7 @@ return [
 
     // Webhooks
     'webhooks' => 'وب‌هوک‌ها',
-    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_index_desc' => 'وب هوک ها روشی برای ارسال داده به آدرس های اینترنتی خارج از سیستم، بر اساس اتفاقات خاصی درون سیستم هستند که امکان یکپارچه سازی مبتنی بر وقایع را با سایر سیستم ها، مثل سیستم پیام رسانی یا اطلاع رسانی، فراهم می کنند.',
     'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',
     'webhooks_create' => 'ایجاد وب هوک جدید',
     'webhooks_none_created' => 'هنوز هیچ وب هوکی ایجاد نشده است.',
index fed157a4793ff589ffc975026859b157c2062a96..310ec071be2c0ceb311ef5c4b7c4fa13eeb21711 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Sélectionner une image',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Toutes',
     'image_all_title' => 'Voir toutes les images',
     'image_book_title' => 'Voir les images ajoutées à ce livre',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Êtes-vous sûr de vouloir supprimer cette image ?',
     'image_select_image' => 'Sélectionner l\'image',
     'image_dropzone' => 'Glissez les images ici ou cliquez pour les ajouter',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Images supprimées',
     'image_preview' => 'Prévisualiser l\'image',
     'image_upload_success' => 'Image ajoutée avec succès',
     'image_update_success' => 'Détails de l\'image mis à jour',
     'image_delete_success' => 'Image supprimée avec succès',
-    'image_upload_remove' => 'Supprimer',
 
     // Code Editor
     'code_editor' => 'Éditer le code',
index 7ff76acf5dfc652cc3f9842445dfdab01a9a28b8..c750600640e5f14a72b5408b130893c9c82957b1 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Fichiers joints',
     'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale',
     'attachments_explain_instant_save' => 'Ces changements sont enregistrés immédiatement.',
-    'attachments_items' => 'Fichiers joints',
     'attachments_upload' => 'Uploader un fichier',
     'attachments_link' => 'Attacher un lien',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Définir un lien',
     'attachments_delete' => 'Êtes-vous sûr de vouloir supprimer la pièce jointe ?',
-    'attachments_dropzone' => 'Glissez des fichiers ou cliquez ici pour attacher des fichiers',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Aucun fichier ajouté',
     'attachments_explain_link' => 'Vous pouvez ajouter un lien si vous ne souhaitez pas uploader un fichier.',
     'attachments_link_name' => 'Nom du lien',
index eaee37be733d9554f205f91f626e4f53f59892df..976655ae3d5e9b1d51b0788017802a505a96666d 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\'extension PHP GD est installée.',
     'server_upload_limit' => 'La taille du fichier est trop grande.',
     'uploaded'  => 'Le serveur n\'autorise pas l\'envoi d\'un fichier de cette taille. Veuillez essayer avec une taille de fichier réduite.',
-    'file_upload_timeout' => 'Le téléchargement du fichier a expiré.',
 
     // Drawing & Images
     'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Fichier joint non trouvé',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être enregistré. Vérifiez votre connexion internet',
index 48f7aa2d25af1c67f6b6866780b736357de4c214..f0b0da3fd8df9e4ba9c3d26044a44e8089a1eafb 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'בחירת תמונה',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'הכל',
     'image_all_title' => 'הצג את כל התמונות',
     'image_book_title' => 'הצג תמונות שהועלו לספר זה',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'האם את/ה בטוח/ה שברצונך למחוק את התמונה הזו?',
     'image_select_image' => 'בחר תמונה',
     'image_dropzone' => 'גרור תמונות או לחץ כאן להעלאה',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'התמונות נמחקו',
     'image_preview' => 'תצוגה מקדימה',
     'image_upload_success' => 'התמונה עלתה בהצלחה',
     'image_update_success' => 'פרטי התמונה עודכנו בהצלחה',
     'image_delete_success' => 'התמונה נמחקה בהצלחה',
-    'image_upload_remove' => 'מחק',
 
     // Code Editor
     'code_editor' => 'ערוך קוד',
index 3c3b9b9c74c82569ced23b9c79513bb63ebc2a1e..cd18075139d202bcbf7cf7e5ca381251a752539f 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'קבצים מצורפים',
     'attachments_explain' => 'צרף קבצים או קישורים על מנת להציגם בדף שלך. צירופים אלו יהיו זמינים בתפריט הצדדי של הדף',
     'attachments_explain_instant_save' => 'שינויים נשמרים באופן מיידי',
-    'attachments_items' => 'פריטים שצורפו',
     'attachments_upload' => 'העלה קובץ',
     'attachments_link' => 'צרף קישור',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'הגדר קישור',
     'attachments_delete' => 'Are you sure you want to delete this attachment?',
-    'attachments_dropzone' => 'גרור לכאן קבצים או לחץ על מנת לצרף קבצים',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'לא הועלו קבצים',
     'attachments_explain_link' => 'ניתן לצרף קישור במקום העלאת קובץ, קישור זה יכול להוביל לדף אחר או לכל קובץ באינטרנט',
     'attachments_link_name' => 'שם הקישור',
index ee19b58d82c5d6800d90e9f66b464075eaddbc55..36a7df46f2aae1b13c5c992625cc5837f692f0b5 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
     'server_upload_limit' => 'השרת אינו מרשה העלאת קבצים במשקל זה. אנא נסה להעלות קובץ קטן יותר.',
     'uploaded'  => 'השרת אינו מרשה העלאת קבצים במשקל זה. אנא נסה להעלות קובץ קטן יותר.',
-    'file_upload_timeout' => 'The file upload has timed out.',
 
     // Drawing & Images
     'image_upload_error' => 'התרחשה שגיאה במהלך העלאת התמונה',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'קובץ מצורף לא נמצא',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'שגיאה בשמירת הטיוטה. אנא ודא כי חיבור האינטרנט תקין לפני שמירת דף זה.',
index 5caffd55f3c2710527cdce1eae9b9d7214815182..a1cb1139fdc6a7d207cda2d64512c744add2bf2a 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Odabir slike',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Sve',
     'image_all_title' => 'Vidi sve slike',
     'image_book_title' => 'Vidi slike dodane ovoj knjizi',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Jeste li sigurni da želite obrisati sliku?',
     'image_select_image' => 'Odaberi sliku',
     'image_dropzone' => 'Prebacite sliku ili kliknite ovdje za prijenos',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Obrisane slike',
     'image_preview' => 'Pregled slike',
     'image_upload_success' => 'Slika je uspješno dodana',
     'image_update_success' => 'Detalji slike su uspješno ažurirani',
     'image_delete_success' => 'Slika je obrisana',
-    'image_upload_remove' => 'Ukloni',
 
     // Code Editor
     'code_editor' => 'Uredi kod',
index 9b861e5f32c346bb70d999feae11e9392e5d19e7..fab7a6cc7af57ec7a7e5586ecf1aee3725d78dd3 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Prilozi',
     'attachments_explain' => 'Dodajte datoteke ili poveznice za prikaz na vašoj stranici. Vidljive su na rubnoj oznaci stranice.',
     'attachments_explain_instant_save' => 'Promjene se automatski spremaju.',
-    'attachments_items' => 'Dodane stavke',
     'attachments_upload' => 'Dodaj datoteku',
     'attachments_link' => 'Dodaj poveznicu',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Postavi poveznicu',
     'attachments_delete' => 'Jeste li sigurni da želite izbrisati ovu stavku?',
-    'attachments_dropzone' => 'Dodajte datoteke ili kliknite ovdje',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nijedna datoteka nije prenesena',
     'attachments_explain_link' => 'Možete dodati poveznicu ako ne želite prenijeti datoteku. Poveznica može voditi na drugu stranicu ili datoteku.',
     'attachments_link_name' => 'Ime poveznice',
index 87f85180e7b3bc80e12a302ff20b8942b4ebae26..706c92dfe83db42f2e31268b2f4d9333e200dd31 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Provjerite imate li instaliranu GD PHP ekstenziju.',
     'server_upload_limit' => 'Prevelika količina za server. Pokušajte prenijeti manju veličinu.',
     'uploaded'  => 'Prevelika količina za server. Pokušajte prenijeti manju veličinu.',
-    'file_upload_timeout' => 'Isteklo vrijeme za prijenos datoteke.',
 
     // Drawing & Images
     'image_upload_error' => 'Problem s prenosom slike',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Prilozi nisu pronađeni',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Problem sa spremanjem nacrta. Osigurajte stabilnu internetsku vezu.',
index c1c57d27e76183b5139501cdb9658437d2630f32..9b9eca6d5fb2e8132916abf2f16b074085e834c0 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Kép kiválasztása',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Összes',
     'image_all_title' => 'Összes kép megtekintése',
     'image_book_title' => 'A könyv feltöltött képek megtekintése',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Biztosan törölhető ez a kép?',
     'image_select_image' => 'Kép kiválasztása',
     'image_dropzone' => 'Képek feltöltése ejtéssel vagy kattintással',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Képek törölve',
     'image_preview' => 'Kép előnézete',
     'image_upload_success' => 'Kép sikeresen feltöltve',
     'image_update_success' => 'Kép részletei sikeresen frissítve',
     'image_delete_success' => 'Kép sikeresen törölve',
-    'image_upload_remove' => 'Eltávolítás',
 
     // Code Editor
     'code_editor' => 'Kód szerkesztése',
index 149d4a7d184cf65c3965e78978df66ed143a8755..8407e9e949c59827583f2a89d8594360787a41eb 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Csatolmányok',
     'attachments_explain' => 'Az oldalon megjelenő fájlok feltöltése vagy hivatkozások csatolása. Az oldal oldalsávjában fognak megjelenni.',
     'attachments_explain_instant_save' => 'Az itt történt módosítások azonnal el lesznek mentve.',
-    'attachments_items' => 'Csatolt elemek',
     'attachments_upload' => 'Fájlfeltöltés',
     'attachments_link' => 'Hivatkozás csatolása',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Hivatkozás beállítása',
     'attachments_delete' => 'Biztosan törölhető ez a melléklet?',
-    'attachments_dropzone' => 'Fájlok csatolása ejtéssel vagy kattintással',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nincsenek fájlok feltöltve',
     'attachments_explain_link' => 'Fájl feltöltése helyett hozzá lehet kapcsolni egy hivatkozást. Ez egy hivatkozás lesz egy másik oldalra vagy egy fájlra a felhőben.',
     'attachments_link_name' => 'Hivatkozás neve',
index 7e31f4d636213880b2f02d4ff6b77d84896ff789..bb697e3370a8809e14f0f793cf4fa1ac0e407701 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'A kiszolgáló nem tud létrehozni bélyegképeket. Ellenőrizni kell, hogy telepítve van-a a GD PHP kiterjesztés.',
     'server_upload_limit' => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',
     'uploaded'  => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.',
-    'file_upload_timeout' => 'A fáj feltöltése időtúllépést okozott.',
 
     // Drawing & Images
     'image_upload_error' => 'Hiba történt a kép feltöltése közben',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Csatolmány nem található',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Nem sikerült a vázlat mentése. Mentés előtt meg kell róla győződni, hogy van internetkapcsolat',
index 3af0e5afc5965be6126e85e1089af121bde2f0a6..9ead91fe6dbfae371df69c8e30af82ed00059271 100644 (file)
@@ -9,43 +9,43 @@ return [
     'page_create'                 => 'telah membuat halaman',
     'page_create_notification'    => 'Jenis Halaman berhasil dibuat',
     'page_update'                 => 'halaman telah diperbaharui',
-    'page_update_notification'    => 'Page successfully updated',
+    'page_update_notification'    => 'Halaman berhasil diperbarui',
     'page_delete'                 => 'halaman dihapus',
-    'page_delete_notification'    => 'Page successfully deleted',
+    'page_delete_notification'    => 'Halaman berhasil dihapus',
     'page_restore'                => 'halaman telah dipulihkan',
-    'page_restore_notification'   => 'Page successfully restored',
+    'page_restore_notification'   => 'Halaman berhasil dipulihkan',
     'page_move'                   => 'halaman dipindahkan',
 
     // Chapters
     'chapter_create'              => 'membuat bab',
-    'chapter_create_notification' => 'Chapter successfully created',
+    'chapter_create_notification' => 'Bab berhasil dibuat',
     'chapter_update'              => 'bab diperbaharui',
-    'chapter_update_notification' => 'Chapter successfully updated',
+    'chapter_update_notification' => 'Bab berhasil diperbarui',
     'chapter_delete'              => 'hapus bab',
-    'chapter_delete_notification' => 'Chapter successfully deleted',
+    'chapter_delete_notification' => 'Bab berhasil dihapus',
     'chapter_move'                => 'bab dipindahkan',
 
     // Books
     'book_create'                 => 'membuat buku',
-    'book_create_notification'    => 'Book successfully created',
-    'book_create_from_chapter'              => 'converted chapter to book',
-    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',
+    'book_create_notification'    => 'Buku berhasil dibuat',
+    'book_create_from_chapter'              => 'mengkonversi bab ke buku',
+    'book_create_from_chapter_notification' => 'Bab berhasil dikonversi menjadi buku',
     'book_update'                 => 'update buku',
-    'book_update_notification'    => 'Book successfully updated',
+    'book_update_notification'    => 'Buku berhasil diperbarui',
     'book_delete'                 => 'hapus buku',
-    'book_delete_notification'    => 'Book successfully deleted',
+    'book_delete_notification'    => 'Buku berhasil dihapus',
     'book_sort'                   => 'buku yang diurutkan',
-    'book_sort_notification'      => 'Book successfully re-sorted',
+    'book_sort_notification'      => 'Buku berhasil diurutkan',
 
     // Bookshelves
-    'bookshelf_create'            => 'created shelf',
-    'bookshelf_create_notification'    => 'Shelf successfully created',
-    'bookshelf_create_from_book'    => 'converted book to shelf',
-    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',
-    'bookshelf_update'                 => 'updated shelf',
-    'bookshelf_update_notification'    => 'Shelf successfully updated',
-    'bookshelf_delete'                 => 'deleted shelf',
-    'bookshelf_delete_notification'    => 'Shelf successfully deleted',
+    'bookshelf_create'            => 'membuat rak',
+    'bookshelf_create_notification'    => 'Rak berhasil dibuat',
+    'bookshelf_create_from_book'    => 'mengkonversi buku ke rak',
+    'bookshelf_create_from_book_notification'    => 'Buku berhasil dikonversi menjadi rak',
+    'bookshelf_update'                 => 'memperbarui rak',
+    'bookshelf_update_notification'    => 'Rak berhasil diperbarui',
+    'bookshelf_delete'                 => 'menghapus rak',
+    'bookshelf_delete_notification'    => 'Rak berhasil dihapus',
 
     // Favourites
     'favourite_add_notification' => '":name" telah ditambahkan ke favorit Anda',
@@ -56,21 +56,21 @@ return [
     'mfa_remove_method_notification' => 'Metode multi-faktor sukses dihapus',
 
     // 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' => 'membuat webhook',
+    'webhook_create_notification' => 'Webhook berhasil dibuat',
+    'webhook_update' => 'memperbarui webhook',
+    'webhook_update_notification' => 'Webhook berhasil diperbarui',
+    'webhook_delete' => 'menghapus webhook',
+    'webhook_delete_notification' => 'Webhook berhasil dihapus',
 
     // Users
-    'user_update_notification' => 'User successfully updated',
-    'user_delete_notification' => 'User successfully removed',
+    'user_update_notification' => 'Pengguna berhasil diperbarui',
+    'user_delete_notification' => 'Pengguna berhasil dihapus',
 
     // Roles
-    'role_create_notification' => 'Role successfully created',
-    'role_update_notification' => 'Role successfully updated',
-    'role_delete_notification' => 'Role successfully deleted',
+    'role_create_notification' => 'Peran berhasil dibuat',
+    'role_update_notification' => 'Peran berhasil diperbarui',
+    'role_delete_notification' => 'Peran berhasil dihapus',
 
     // Other
     'commented_on'                => 'berkomentar pada',
index bc871c4963c31ddb32a536281cf56355f129bf58..5d174f6acb4843cbe87a8c19ea365975a802ef69 100644 (file)
@@ -39,9 +39,9 @@ return [
     'register_success' => 'Terima kasih telah mendaftar! Anda sekarang terdaftar dan masuk.',
 
     // Login auto-initiation
-    'auto_init_starting' => 'Attempting Login',
-    'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.',
-    'auto_init_start_link' => 'Proceed with authentication',
+    'auto_init_starting' => 'Mencoba masuk',
+    'auto_init_starting_desc' => 'Kami sedang menghubungi sistem autentikasi Anda untuk memulai proses login. Jika tidak ada kemajuan setelah 5 detik, Anda dapat mencoba mengklik link di bawah ini.',
+    'auto_init_start_link' => 'Lanjutkan dengan otentikasi',
 
     // Password Reset
     'reset_password' => 'Atur ulang kata sandi',
@@ -59,10 +59,10 @@ return [
     'email_confirm_text' => 'Silakan konfirmasi alamat email Anda dengan mengklik tombol di bawah ini:',
     'email_confirm_action' => 'Konfirmasi email',
     'email_confirm_send_error' => 'Konfirmasi email diperlukan tetapi sistem tidak dapat mengirim email. Hubungi admin untuk memastikan email disiapkan dengan benar.',
-    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
+    'email_confirm_success' => 'Email Anda sudah terkonfirmasi! Anda seharusnya sudah bisa masuk menggunakan email ini.',
     'email_confirm_resent' => 'Email konfirmasi dikirim ulang, Harap periksa kotak masuk Anda.',
-    'email_confirm_thanks' => 'Thanks for confirming!',
-    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
+    'email_confirm_thanks' => 'Terima kasih untuk mengkonfirmasi!',
+    'email_confirm_thanks_desc' => 'Harap tunggu sebentar, konfirmasi Anda sedang ditangani. Jika Anda tidak dipindahkan setelah 3 detik, tekan link "Selanjutnya" dibawah ini untuk melanjutkan.',
 
     'email_not_confirmed' => 'Alamat Email Tidak Dikonfirmasi',
     'email_not_confirmed_text' => 'Alamat email Anda belum dikonfirmasi.',
@@ -78,12 +78,12 @@ return [
     'user_invite_page_welcome' => 'Selamat datang di :appName!',
     'user_invite_page_text' => 'Untuk menyelesaikan akun Anda dan mendapatkan akses, Anda perlu mengatur kata sandi yang akan digunakan untuk masuk ke :appName pada kunjungan berikutnya.',
     'user_invite_page_confirm_button' => 'Konfirmasi Kata sandi',
-    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
+    'user_invite_success_login' => 'Kata sandi diset, Anda seharusnya sudah bisa masuk menggunakan kata sandi yang sudah diset untuk mengakses :appName!',
 
     // Multi-factor Authentication
-    'mfa_setup' => 'Setup Multi-Factor Authentication',
-    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
-    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup' => 'Atur Multi-Factor Otentikasi',
+    'mfa_setup_desc' => 'Mengatur multi-factor otentikasi sebagai tambahan ekstra keamanan untuk akun Anda.',
+    'mfa_setup_configured' => 'Sudah dikonfigurasi',
     'mfa_setup_reconfigure' => 'Konfigurasi ulang',
     'mfa_setup_remove_confirmation' => 'Apakah Anda yakin ingin menghapus metode autentikasi multi-faktor ini?',
     'mfa_setup_action' => 'Setup',
index 309e93141ccc3fffe6d46ba035bc05ab96db9448..d2ff7cd4e3d79fc61c488e3800aba4f2144b8f69 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Pilih Gambar',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Semua',
     'image_all_title' => 'Lihat semua gambar',
     'image_book_title' => 'Lihat gambar yang diunggah ke buku ini',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Anda yakin ingin menghapus gambar ini?',
     'image_select_image' => 'Pilih gambar',
     'image_dropzone' => 'Lepaskan gambar atau klik di sini untuk mengunggah',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Gambar Dihapus',
     'image_preview' => 'Pratinjau Gambar',
     'image_upload_success' => 'Gambar berhasil diunggah',
     'image_update_success' => 'Detail gambar berhasil diperbarui',
     'image_delete_success' => 'Gambar berhasil dihapus',
-    'image_upload_remove' => 'Menghapus',
 
     // Code Editor
     'code_editor' => 'Edit Kode',
index 9353c3af97a33e94668a40ed7bdad8e4ac45784e..8b517198a4e9a9d142c308ff83cb6d8354b40140 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Lampiran',
     'attachments_explain' => 'Unggah beberapa berkas atau lampirkan beberapa tautan untuk ditampilkan di laman Anda. Ini terlihat di sidebar halaman.',
     'attachments_explain_instant_save' => 'Perubahan di sini disimpan secara instan.',
-    'attachments_items' => 'Item Terlampir',
     'attachments_upload' => 'Unggah Berkas',
     'attachments_link' => 'Lampirkan Tautan',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Setel Tautan',
     'attachments_delete' => 'Anda yakin ingin menghapus lampiran ini?',
-    'attachments_dropzone' => 'Jatuhkan file atau klik di sini untuk melampirkan file',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Tidak ada berkas yang telah diunggah',
     'attachments_explain_link' => 'Anda dapat melampirkan sebuah tautan jika Anda memilih untuk tidak mengunggah berkas. Ini bisa berupa sebuah tautan ke halaman lain atau tautan ke sebuah berkas di cloud.',
     'attachments_link_name' => 'Nama Tautan',
index 4e93855a4c60f85df91a87a8a492b579c4aee467..4bfd60a7e0a7c14c0180469652b0601af6797249 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Server tidak dapat membuat thumbnail. Harap periksa apakah Anda telah memasang ekstensi GD PHP.',
     'server_upload_limit' => 'Server tidak mengizinkan unggahan dengan ukuran ini. Harap coba ukuran berkas yang lebih kecil.',
     'uploaded'  => 'Server tidak mengizinkan unggahan dengan ukuran ini. Harap coba ukuran berkas yang lebih kecil.',
-    'file_upload_timeout' => 'Unggahan berkas telah habis waktu.',
 
     // Drawing & Images
     'image_upload_error' => 'Terjadi kesalahan saat mengunggah gambar',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Lampiran tidak ditemukan',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Gagal menyimpan draf. Pastikan Anda memiliki koneksi internet sebelum menyimpan halaman ini',
index 63637aa484fccf69450ce473aad73e8584767663..ad0b404263d54eda28a9569e54fd8d769e4366bf 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selezione Immagine',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Tutte',
     'image_all_title' => 'Visualizza tutte le immagini',
     'image_book_title' => 'Visualizza immagini caricate in questo libro',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Sei sicuro di voler eliminare questa immagine?',
     'image_select_image' => 'Seleziona Immagine',
     'image_dropzone' => 'Rilascia immagini o clicca qui per caricarle',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Immagini Eliminate',
     'image_preview' => 'Anteprima Immagine',
     'image_upload_success' => 'Immagine caricata correttamente',
     'image_update_success' => 'Dettagli immagine aggiornati correttamente',
     'image_delete_success' => 'Immagine eliminata correttamente',
-    'image_upload_remove' => 'Rimuovi',
 
     // Code Editor
     'code_editor' => 'Modifica Codice',
index 6273e8a0f9799fe82255fd270adb0f2cc9448f63..1d81f5ae0df2265fc17630db00307ed8affbfff1 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Allegati',
     'attachments_explain' => 'Carica alcuni file o allega link per visualizzarli nella pagina. Questi sono visibili nella sidebar della pagina.',
     'attachments_explain_instant_save' => 'I cambiamenti qui sono salvati istantaneamente.',
-    'attachments_items' => 'Oggetti Allegati',
     'attachments_upload' => 'Carica File',
     'attachments_link' => 'Allega Link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Imposta Link',
     'attachments_delete' => 'Sei sicuro di voler eliminare questo allegato?',
-    'attachments_dropzone' => 'Rilascia file o clicca qui per allegare un file',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nessun file è stato caricato',
     'attachments_explain_link' => 'Puoi allegare un link se preferisci non caricare un file. Questo può essere un link a un\'altra pagina o a un file nel cloud.',
     'attachments_link_name' => 'Nome Link',
index 815cd22b9e0d0bec42898e47b00754b51c0477e5..e2e7d49cd1a45b8493051cbb2dc50e5c02941b66 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Il server non può creare thumbnail. Controlla che l\'estensione GD sia installata.',
     'server_upload_limit' => 'Il server non permette un upload di questa grandezza. Prova con un file più piccolo.',
     'uploaded'  => 'Il server non consente upload di questa grandezza. Prova un file più piccolo.',
-    'file_upload_timeout' => 'Il caricamento del file è andato in timeout.',
 
     // Drawing & Images
     'image_upload_error' => 'C\'è stato un errore caricando l\'immagine',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Allegato non trovato',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Impossibile salvare la bozza. Controlla di essere connesso ad internet prima di salvare questa pagina',
index 54a1092d2ac4f247c2c247d261b6aa8a54a1745b..7fcc87d1d95dc29a40a1e59e320e2780279ded46 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => '画像を選択',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'すべて',
     'image_all_title' => '全ての画像を表示',
     'image_book_title' => 'このブックにアップロードされた画像を表示',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'この画像を削除してもよろしいですか?',
     'image_select_image' => '画像を選択',
     'image_dropzone' => '画像をドロップするか、クリックしてアップロード',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => '画像を削除しました',
     'image_preview' => '画像プレビュー',
     'image_upload_success' => '画像がアップロードされました',
     'image_update_success' => '画像が更新されました',
     'image_delete_success' => '画像が削除されました',
-    'image_upload_remove' => '削除',
 
     // Code Editor
     'code_editor' => 'コードを編集する',
index fc6be5b503ff990c708914fcc3b159016a4a73a2..ae952cd02be036807eaca21c8c880b5bee578d9d 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => '添付ファイル',
     'attachments_explain' => 'ファイルをアップロードまたはリンクを添付することができます。これらはサイドバーで確認できます。',
     'attachments_explain_instant_save' => 'この変更は即座に保存されます。',
-    'attachments_items' => 'アイテム',
     'attachments_upload' => 'アップロード',
     'attachments_link' => 'リンクを添付',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'リンクを設定',
     'attachments_delete' => 'この添付ファイルを削除してよろしいですか?',
-    'attachments_dropzone' => 'ファイルをドロップするか、クリックして選択',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'ファイルはアップロードされていません',
     'attachments_explain_link' => 'ファイルをアップロードしたくない場合、他のページやクラウド上のファイルへのリンクを添付できます。',
     'attachments_link_name' => 'リンク名',
index f1d3871f182a628529a72c0e002f894d80ccf223..945a02ab0ed4245244ca35bb0ce5ce9d3e71d1e1 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'このサーバはサムネイルを作成できません。GD PHP extensionがインストールされていることを確認してください。',
     'server_upload_limit' => 'このサイズの画像をアップロードすることは許可されていません。ファイルサイズを小さくし、再試行してください。',
     'uploaded'  => 'このサイズの画像をアップロードすることは許可されていません。ファイルサイズを小さくし、再試行してください。',
-    'file_upload_timeout' => 'ファイルのアップロードがタイムアウトしました。',
 
     // Drawing & Images
     'image_upload_error' => '画像アップロード時にエラーが発生しました。',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => '添付ファイルが見つかりません',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => '下書きの保存に失敗しました。インターネットへ接続してください。',
index 48a0a32faa38c4821a9d71dda9a5fb4f97d35232..cd5dca251bbc7ec59f779fb9d727b57dfbd2c0c2 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Image Select',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'All',
     'image_all_title' => 'View all images',
     'image_book_title' => 'View images uploaded to this book',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
     'image_select_image' => 'Select Image',
     'image_dropzone' => 'Drop images or click here to upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Images Deleted',
     'image_preview' => 'Image Preview',
     'image_upload_success' => 'Image uploaded successfully',
     'image_update_success' => 'Image details successfully updated',
     'image_delete_success' => 'Image successfully deleted',
-    'image_upload_remove' => 'Remove',
 
     // Code Editor
     'code_editor' => 'Edit Code',
index 9b02f31118a8c06d24043499112702fc5df18d98..9614f92fe1ff8b800ad087fff13c74ada1aabcbc 100644 (file)
@@ -311,12 +311,12 @@ return [
     '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_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     '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_dropzone' => 'Drop files here to upload',
     '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',
index 703d0edbef0635a4963c2eb7e294f0d7c65bf0b1..6991f96e4359facd80e789406e28054c8432b2b0 100644 (file)
@@ -45,7 +45,6 @@ return [
     '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.',
-    'file_upload_timeout' => 'The file upload has timed out.',
 
     // Drawing & Images
     'image_upload_error' => 'An error occurred uploading the image',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Attachment not found',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
index f095ce5487b072c3d7d623df8f21f12d50ae9d8c..d5dedfa15c46d013a5e3438c0949530cd402b4b8 100644 (file)
@@ -7,13 +7,13 @@ return [
 
     // Pages
     'page_create'                 => '문서 만들기',
-    'page_create_notification'    => '문서 생성함',
+    'page_create_notification'    => '페이지를 생성했습니다',
     'page_update'                 => '문서 수정',
-    'page_update_notification'    => '문서 수정함',
+    'page_update_notification'    => '페이지를 수정했습니다',
     'page_delete'                 => '삭제 된 페이지',
-    'page_delete_notification'    => '문서 삭제함',
+    'page_delete_notification'    => '페이지를 삭제했습니다',
     'page_restore'                => '문서 복원',
-    'page_restore_notification'   => '문서 복원함',
+    'page_restore_notification'   => '페이지가 복원되었습니다',
     'page_move'                   => '문서 이동',
 
     // Chapters
@@ -68,9 +68,9 @@ return [
     'user_delete_notification' => '사용자가 삭제되었습니다',
 
     // Roles
-    'role_create_notification' => 'Role successfully created',
-    'role_update_notification' => 'Role successfully updated',
-    'role_delete_notification' => 'Role successfully deleted',
+    'role_create_notification' => '역할이 생성되었습니다',
+    'role_update_notification' => '역할이 수정되었습니다',
+    'role_delete_notification' => '역할이 삭제되었습니다',
 
     // Other
     'commented_on'                => '댓글 쓰기',
index dc32c1355a209afa0e067df4532312c8826710ef..60a54dabd64b50861431a766bced2185e882a033 100644 (file)
@@ -7,24 +7,24 @@
 return [
 
     'failed' => '자격 증명이 기록과 일치하지 않습니다.',
-    'throttle' => 'ë¡\9cê·¸ì\9d¸ ì\8b\9cë\8f\84ê°\80 ë\84\88무 ë§\8eì\8aµë\8b\88ë\8b¤. :secondsì´\88 í\9b\84ì\97\90 ë\8b¤ì\8b\9c ì\8b\9cë\8f\84í\95\98세요.',
+    'throttle' => 'ë¡\9cê·¸ì\9d¸ ì\8b\9cë\8f\84ê°\80 ë\84\88무 ë§\8eì\8aµë\8b\88ë\8b¤. :secondsì´\88 í\9b\84ì\97\90 ë\8b¤ì\8b\9c ì\8b\9cë\8f\84í\95´ì£¼세요.',
 
     // Login & Register
     'sign_up' => '가입',
     'log_in' => '로그인',
-    'log_in_with' => ':socialDriver로 로그인',
-    'sign_up_with' => ':socialDriver로 가입',
+    'log_in_with' => ':socialDriver 소셜 계정으로 로그인',
+    'sign_up_with' => ':socialDriver 소셜 계정으로 가입',
     'logout' => '로그아웃',
 
     'name' => '이름',
     'username' => '사용자 이름',
-    'email' => '메일 주소',
-    'password' => '패스워드',
-    'password_confirm' => '패스워드 확인',
-    'password_hint' => '여덟 글자를 넘어야 합니다.',
-    'forgot_password' => '패스워드를 잊었나요?',
+    'email' => '전자우편 주소',
+    'password' => '비밀번호',
+    'password_confirm' => '비밀번호 확인',
+    'password_hint' => '8 글자를 넘어야 합니다.',
+    'forgot_password' => '비밀번호를 잊으셨나요?',
     'remember_me' => '로그인 유지',
-    'ldap_email_hint' => '계정에 연결한 메일 주소를 입력하세요.',
+    'ldap_email_hint' => '계정에 연결한 전자우편 주소를 입력하세요.',
     'create_account' => '가입',
     'already_have_account' => '계정이 있나요?',
     'dont_have_account' => '계정이 없나요?',
@@ -33,9 +33,9 @@ return [
     'social_registration_text' => '소셜 계정으로 가입하고 로그인합니다.',
 
     'register_thanks' => '가입해 주셔서 감사합니다!',
-    'register_confirm' => '메일을 확인한 후 버튼을 눌러 :appName에 접근하세요.',
-    'registrations_disabled' => '가입할 수 없습니다.',
-    'registration_email_domain_invalid' => '이 메일 주소로는 이 사이트에 접근할 수 없습니다.',
+    'register_confirm' => '전자우편을 확인한 후 버튼을 눌러 :appName에 접근하세요.',
+    'registrations_disabled' => '가입 기능이 비활성화되어 있습니다',
+    'registration_email_domain_invalid' => '이 전자우편 주소로는 이 사이트에 접근할 수 없습니다.',
     'register_success' => '가입했습니다! 이제 로그인할 수 있습니다.',
 
     // Login auto-initiation
@@ -44,46 +44,46 @@ return [
     'auto_init_start_link' => '인증 진행',
 
     // Password Reset
-    'reset_password' => '패스워드 바꾸기',
-    'reset_password_send_instructions' => '메일 주소를 입력하세요. 이 주소로 해당 과정을 위한 링크를 보낼 것입니다.',
-    'reset_password_send_button' => '메일 보내기',
-    'reset_password_sent' => '패스워드를 바꿀 수 있는 링크를 :email로 보낼 것입니다.',
-    'reset_password_success' => '패스워드를 바꿨습니다.',
-    'email_reset_subject' => ':appName 패스워드 바꾸기',
-    'email_reset_text' => '패스워드를 바꿉니다.',
-    'email_reset_not_requested' => '원하지 않는다면 이 과정은 필요 없습니다.',
+    'reset_password' => '비밀번호 바꾸기',
+    'reset_password_send_instructions' => '전자우편 주소를 입력하세요. 이 주소로 해당 과정을 위한 링크를 보낼 것입니다.',
+    'reset_password_send_button' => '재설정 링크 보내기',
+    'reset_password_sent' => '비밀번호를 바꿀 수 있는 링크를 :email 전자우편 주소로 보낼 것입니다.',
+    'reset_password_success' => '비밀번호를 바꿨습니다.',
+    'email_reset_subject' => ':appName 비밀번호 바꾸기',
+    'email_reset_text' => '비밀번호를 바꿉니다.',
+    'email_reset_not_requested' => '비밀번호 재설정을 요청하지 않으셨다면 추가 조치가 필요하지 않습니다.',
 
     // Email Confirmation
-    'email_confirm_subject' => ':appName 메일 인증',
-    'email_confirm_greeting' => ':appName 가입해 주셔서 감사합니다!',
-    'email_confirm_text' => 'ë\8b¤ì\9d\8c ë²\84í\8a¼ì\9d\84 ë\88\8cë\9f¬ ì\9d¸ì¦\9d하세요:',
-    'email_confirm_action' => '메일 인증',
-    'email_confirm_send_error' => '메일을 보낼 수 없었습니다.',
-    'email_confirm_success' => '메일 인증을 성공했습니다. 이 메일 주소로 로그인할 수 있습니다.',
-    'email_confirm_resent' => '다시 보냈습니다. 메일함을 확인하세요.',
-    'email_confirm_thanks' => 'Thanks for confirming!',
-    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
+    'email_confirm_subject' => ':appName 전자우편 인증을 확인합니다',
+    'email_confirm_greeting' => ':appName 서비스에 가입해 주셔서 감사합니다!',
+    'email_confirm_text' => 'ë\8b¤ì\9d\8c ë²\84í\8a¼ì\9d\84 ë\88\8cë\9f¬ ì \84ì\9e\90ì\9a°í\8e¸ ì£¼ì\86\8c를 í\99\95ì\9d¸하세요:',
+    'email_confirm_action' => '전자우편 확인',
+    'email_confirm_send_error' => '전자우편 확인이 필요하지만 이 시스템에서 전자우편을 발송하지 못했습니다. 전자우편 주소가 제대로 설정되었는지 확인하려면 관리자에게 연락을 해주세요.',
+    'email_confirm_success' => '전자우편 인증을 성공했습니다. 이 전자우편 주소로 로그인할 수 있습니다.',
+    'email_confirm_resent' => '전자우편 확인을 다시 보냈습니다. 우편함을 확인해주세요.',
+    'email_confirm_thanks' => '확인해 주셔서 감사합니다!',
+    'email_confirm_thanks_desc' => '확인이 처리되는 동안 잠시 기다려주세요. 3초 안에 리디렉션되지 않는다면 아내에 있는 "계속" 링크를 눌러서 진행하세요.',
 
-    'email_not_confirmed' => 'ì\9d¸ì¦\9dí\95\98ì§\80 ì\95\8aì\95\98ì\8aµë\8b\88ë\8b¤.',
-    'email_not_confirmed_text' => 'ì\9d¸ì¦\9dì\9d\84 ì\99\84ë£\8cí\95\98ì§\80 ì\95\8aì\95\98ì\8aµë\8b\88ë\8b¤.',
-    'email_not_confirmed_click_link' => 'ë©\94ì\9d¼ì\9d\84 í\99\95ì\9d¸í\95\98ê³  ì\9d¸ì¦\9d 링크를 클릭하세요.',
-    'email_not_confirmed_resend' => '메일 주소가 없다면 다음을 입력하세요.',
-    'email_not_confirmed_resend_button' => '다시 보내기',
+    'email_not_confirmed' => 'ì \84ì\9e\90ì\9a°í\8e¸ ì£¼ì\86\8cê°\80 í\99\95ì\9d¸ë\90\98ì§\80 ì\95\8aì\95\98ì\8aµë\8b\88ë\8b¤.',
+    'email_not_confirmed_text' => 'ì \84ì\9e\90ì\9a°í\8e¸ í\99\95ì\9d¸ì\9d´ ì\95\84ì§\81 ì\99\84ë£\8cë\90\98ì§\80 ì\95\8aì\95\98ì\8aµë\8b\88ë\8b¤.',
+    'email_not_confirmed_click_link' => 'ë\93±ë¡\9dí\95\9c ì§\81í\9b\84ì\97\90 ë°\9cì\86¡ë\90\9c ì \84ì\9e\90ì\9a°í\8e¸ì\97\90 ì\9e\88ë\8a\94 í\99\95ì\9d¸ 링크를 클릭하세요.',
+    'email_not_confirmed_resend' => '전자우편 확인을 위해 발송된 전자우편을 찾을 수 없다면 아래의 폼을 다시 발행하여 전자우편 확인을 재발송할 수 있습니다.',
+    'email_not_confirmed_resend_button' => '전자우편 확인 재전송',
 
     // User Invite
-    'user_invite_email_subject' => ':appName에서 권유를 받았습니다.',
-    'user_invite_email_greeting' => ':appName에서 가입한 기록이 있습니다.',
-    'user_invite_email_text' => '다음 버튼을 눌러 확인하세요:',
-    'user_invite_email_action' => '패스워드 설정',
-    'user_invite_page_welcome' => ':appName에 오신 것을 환영합니다!',
-    'user_invite_page_text' => ':appName에 로그인할 때 입력할 패스워드를 설정하세요.',
-    'user_invite_page_confirm_button' => '패스워드 확인',
-    'user_invite_success_login' => '입력한 패스워드로 :appName에 로그인할 수 있습니다.',
+    'user_invite_email_subject' => ':appName 애플리케이션에서 초대를 받았습니다!',
+    'user_invite_email_greeting' => ':appName 애플리케이션에 가입한 기록이 있습니다.',
+    'user_invite_email_text' => '아래 버튼을 클릭하여 계정 비밀번호를 설정하고 접근 권한을 얻으세요:',
+    'user_invite_email_action' => '비밀번호 설정',
+    'user_invite_page_welcome' => ':appName 애플리케이션에 오신 것을 환영합니다!',
+    'user_invite_page_text' => '계정 설정을 마치고 접근 권한을 얻으려면 :appName 애플리케이션에 로그인할 때 사용할 비밀번호를 설정해야 합니다.',
+    'user_invite_page_confirm_button' => '비밀번호 확인',
+    'user_invite_success_login' => '비밀번호를 설정했습니다, 이제 입력한 비밀번호로 :appName 애플리케이션에 로그인할 수 있습니다!',
 
     // Multi-factor Authentication
     'mfa_setup' => '다중 인증 설정',
     'mfa_setup_desc' => '추가 보안 계층으로 다중 인증을 설정합니다.',
-    'mfa_setup_configured' => 'ì\84¤ì \95ë\90\98ì\96´ ì\9e\88ì\8aµë\8b\88ë\8b¤.',
+    'mfa_setup_configured' => 'ì\9d´ë¯¸ ì\84¤ì \95ë\90\98ì\97\88ì\8aµë\8b\88ë\8b¤',
     'mfa_setup_reconfigure' => '다시 설정',
     'mfa_setup_remove_confirmation' => '다중 인증을 해제할까요?',
     'mfa_setup_action' => '설정',
@@ -92,10 +92,10 @@ return [
     'mfa_option_totp_desc' => '다중 인증에는 Google Authenticator, Authy나 Microsoft Authenticator와 같은 TOTP 지원 모바일 앱이 필요합니다.',
     'mfa_option_backup_codes_title' => '백업 코드',
     'mfa_option_backup_codes_desc' => '일회성 백업 코드를 안전한 장소에 보관하세요.',
-    'mfa_gen_confirm_and_enable' => '활성화',
+    'mfa_gen_confirm_and_enable' => 'í\99\95ì\9d¸ ë°\8f í\99\9cì\84±í\99\94',
     'mfa_gen_backup_codes_title' => '백업 코드 설정',
     'mfa_gen_backup_codes_desc' => '코드 목록을 안전한 장소에 보관하세요. 코드 중 하나를 2FA에 쓸 수 있습니다.',
-    'mfa_gen_backup_codes_download' => '코드 받기',
+    'mfa_gen_backup_codes_download' => 'ì½\94ë\93\9c ë\82´ë ¤ë°\9b기',
     'mfa_gen_backup_codes_usage_warning' => '각 코드는 한 번씩만 유효합니다.',
     'mfa_gen_totp_title' => '모바일 앱 설정',
     'mfa_gen_totp_desc' => '다중 인증에는 Google Authenticator, Authy나 Microsoft Authenticator와 같은 TOTP 지원 모바일 앱이 필요합니다.',
@@ -106,7 +106,7 @@ return [
     'mfa_verify_access' => '접근 확인',
     'mfa_verify_access_desc' => '추가 인증으로 신원을 확인합니다. 설정한 방법 중 하나를 고르세요.',
     'mfa_verify_no_methods' => '설정한 방법이 없습니다.',
-    'mfa_verify_no_methods_desc' => '다중 인증을 설정하지 않았습니다.',
+    'mfa_verify_no_methods_desc' => '다중 인증을 설정하지 않았습니다. 접근 권한을 얻기 전에 하나 이상의 다중 인증을 설정해야 합니다.',
     'mfa_verify_use_totp' => '모바일 앱으로 인증하기',
     'mfa_verify_use_backup_codes' => '백업 코드로 인증하세요.',
     'mfa_verify_backup_code' => '백업 코드',
index 876a0029dd0d6c78db67652faca7dce901daf53d..c7b831d325697bb486836be8713790c9ba2f18ee 100644 (file)
@@ -42,8 +42,8 @@ return [
     'add' => '추가',
     'configure' => '설정',
     'fullscreen' => '전체화면',
-    'favourite' => '북마크',
-    'unfavourite' => 'ì¢\8bì\95\84í\95\98ì§\80 ì\95\8aì\9d\8c',
+    'favourite' => '즐겨찾기',
+    'unfavourite' => 'ì¦\90겨찾기 í\95´ì \9c',
     'next' => '다음',
     'previous' => '이전',
     'filter_active' => '적용 중:',
@@ -54,8 +54,8 @@ return [
     // Sort Options
     'sort_options' => '정렬 기준',
     'sort_direction_toggle' => '순서 반전',
-    'sort_ascending' => '오름차 ì\88\9cì\84\9c',
-    'sort_descending' => '내림차 ì\88\9cì\84\9c',
+    'sort_ascending' => '오름차ì\88\9c',
+    'sort_descending' => '내림차ì\88\9c',
     'sort_name' => '제목',
     'sort_default' => '기본값',
     'sort_created_at' => '만든 날짜',
@@ -81,20 +81,20 @@ return [
     'none' => '없음',
 
     // Header
-    'homepage' => 'Homepage',
+    'homepage' => '홈페이지',
     'header_menu_expand' => '헤더 메뉴 펼치기',
     'profile_menu' => '프로필',
     'view_profile' => '프로필 보기',
     'edit_profile' => '프로필 바꾸기',
     'dark_mode' => '어두운 테마',
     'light_mode' => '밝은 테마',
-    'global_search' => 'Global Search',
+    'global_search' => '전역 검색',
 
     // Layout tabs
     'tab_info' => '정보',
-    'tab_info_label' => 'Tab: 보조 정보 보이기',
+    'tab_info_label' => ': 보조 정보 보이기',
     'tab_content' => '내용',
-    'tab_content_label' => 'Tab: 우선 항목 보이기',
+    'tab_content_label' => ': 우선 항목 보이기',
 
     // Email Content
     'email_action_help' => ':actionText를 클릭할 수 없을 때는 웹 브라우저에서 다음 링크로 접속할 수 있습니다.',
index a6730fc81b1c427ec001c747f6f379827a7a4087..8787df41551c03f25c59577989435c4ee391b239 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => '이미지 선택',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => '모든 이미지',
     'image_all_title' => '모든 이미지',
     'image_book_title' => '이 책에서 쓰고 있는 이미지',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => '이 이미지를 지울 건가요?',
     'image_select_image' => '이미지 선택',
     'image_dropzone' => '여기에 이미지를 드롭하거나 여기를 클릭하세요. 이미지를 올릴 수 있습니다.',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => '이미지 삭제함',
     'image_preview' => '이미지 미리 보기',
     'image_upload_success' => '이미지 올림',
     'image_update_success' => '이미지 정보 수정함',
     'image_delete_success' => '이미지 삭제함',
-    'image_upload_remove' => '삭제',
 
     // Code Editor
     'code_editor' => '코드 수정',
index 8bd2ffacdc0e210024c9bcb52bedab0e67521362..9764c7d356d157e611e031888bfe8eac3a04da5a 100644 (file)
@@ -66,7 +66,7 @@ return [
     'insert_link_title' => '링크 삽입/수정',
     'insert_horizontal_line' => '수평선 삽입',
     'insert_code_block' => '코드 블럭 삽입',
-    'edit_code_block' => 'Edit code block',
+    'edit_code_block' => '코드 블록 편집',
     'insert_drawing' => '그리기 삽입/수정',
     'drawing_manager' => '그리기 설정',
     'insert_media' => '미디어 삽입/수정',
@@ -129,39 +129,39 @@ return [
     'cell_border_dotted' => '점선',
     'cell_border_dashed' => '파선',
     'cell_border_double' => '겹선',
-    'cell_border_groove' => 'Groove',
-    'cell_border_ridge' => 'Ridge',
-    'cell_border_inset' => 'Inset',
-    'cell_border_outset' => 'Outset',
+    'cell_border_groove' => '테두리 음각',
+    'cell_border_ridge' => '테두리 양각',
+    'cell_border_inset' => '요소 음각',
+    'cell_border_outset' => '요소 양각',
     'cell_border_none' => '없음',
     'cell_border_hidden' => '숨김',
 
     // Images, links, details/summary & embed
     'source' => '원본',
     'alt_desc' => '대체 설명',
-    'embed' => 'Embed',
-    'paste_embed' => 'Paste your embed code below:',
+    'embed' => '포함',
+    'paste_embed' => '아래에 포함할 코드를 붙여넣습니다:',
     'url' => 'URL',
     'text_to_display' => '표시할 텍스트',
     'title' => '제목',
-    'open_link' => 'Open link',
-    'open_link_in' => 'Open link in...',
+    'open_link' => '링크 열기',
+    'open_link_in' => '다음에서 링크 열기...',
     'open_link_current' => '현재 창',
     'open_link_new' => '새 창',
-    'remove_link' => 'Remove link',
-    'insert_collapsible' => 'Insert collapsible block',
+    'remove_link' => '링크 제거',
+    'insert_collapsible' => '접을 수 있는 블록 삽입',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => '레이블 수정',
     'toggle_open_closed' => '열림/닫힘 전환',
-    'collapsible_edit' => 'Edit collapsible block',
+    'collapsible_edit' => '접을 수 있는 블록 편집',
     'toggle_label' => '레이블 보이기/숨기기',
 
     // About view
-    'about' => 'About the editor',
+    'about' => '이 편집기에 대하여',
     'about_title' => 'WYSIWYG 편집기에 대하여',
     'editor_license' => '편집기 라이선스 & 저작권',
-    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
-    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
+    'editor_tiny_license' => '이 편집기는 MIT 라이선스에 따라 제공되는 :tinyLink를 사용하여 제작되었습니다.',
+    'editor_tiny_license_link' => 'TinyMCE의 저작권 및 라이선스 세부 정보는 여기에서 확인할 수 있습니다.',
     'save_continue' => '저장하고 계속하기',
     'callouts_cycle' => '(Keep pressing to toggle through types)',
     'link_selector' => 'Link to content',
index 2011cadb8596467b7513d1666384c5a3006a78ab..ef0f304f3e970c85d469c1ad6b5c4e650ffc5f1c 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => '첨부 파일',
     'attachments_explain' => '파일이나 링크를 첨부하세요. 정보 탭에 나타납니다.',
     'attachments_explain_instant_save' => '여기에서 바꾼 내용은 바로 적용합니다.',
-    'attachments_items' => '첨부한 파일들',
     'attachments_upload' => '파일 올리기',
     'attachments_link' => '링크로 첨부',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => '링크 설정',
     'attachments_delete' => '이 첨부 파일을 지울 건가요?',
-    'attachments_dropzone' => '여기에 파일을 드롭하거나 여기를 클릭하세요.',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => '올린 파일 없음',
     'attachments_explain_link' => '파일을 올리지 않고 링크로 첨부할 수 있습니다.',
     'attachments_link_name' => '링크 이름',
index 4390ab7ca318ebed71c25a2b1cea114c9401fd21..bfe96a297d0c1b1c3f702cdd0461c617dacf3417 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => '섬네일을 못 만들었습니다. PHP에 GD 확장 도구를 설치하세요.',
     'server_upload_limit' => '파일 크기가 서버에서 허용하는 수치를 넘습니다.',
     'uploaded'  => '파일 크기가 서버에서 허용하는 수치를 넘습니다.',
-    'file_upload_timeout' => '파일을 올리는 데 걸리는 시간이 서버에서 허용하는 수치를 넘습니다.',
 
     // Drawing & Images
     'image_upload_error' => '이미지를 올리다 문제가 생겼습니다.',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => '첨부 파일이 없습니다.',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => '초안 문서를 유실했습니다. 인터넷 연결 상태를 확인하세요.',
index e9a47461b3d18a42a68699d729acf99b07b9eda5..400e0f5efade0f346b6d1572c9fc61dde09605da 100644 (file)
@@ -5,14 +5,14 @@
  */
 
 return [
-    'shortcuts' => 'Shortcuts',
-    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
-    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
-    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
-    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
-    'shortcuts_section_navigation' => 'Navigation',
-    'shortcuts_section_actions' => 'Common Actions',
-    'shortcuts_save' => 'Save Shortcuts',
-    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
-    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+    'shortcuts' => '단축키',
+    'shortcuts_interface' => '키보드 단축키',
+    'shortcuts_toggle_desc' => '여기에서 탐색과 행동에 사용될 수 있는 키보드 단축키를 활성화하거나 비활성화할 수 있습니다.',
+    'shortcuts_customize_desc' => '아래에서 각 단축키를 사용자 지정할 수 있습니다. 바로가기에 대한 입력을 선택한 후 원하는 키 조합을 누르기만 하면 됩니다.',
+    'shortcuts_toggle_label' => '키보드 단축키가 활성화되었습니다.',
+    'shortcuts_section_navigation' => '탐색',
+    'shortcuts_section_actions' => '일반 작업',
+    'shortcuts_save' => '단축키 저장',
+    'shortcuts_overlay_desc' => '참고: 바로가기가 활성화된 경우 "?"를 누르면 현재 화면에 표시되는 작업에 대해 사용 가능한 바로가기를 강조 표시하는 도우미 오버레이를 사용할 수 있습니다.',
+    'shortcuts_update_success' => '단축키 설정이 수정되었습니다!',
 ];
\ No newline at end of file
index fc6386aa9164577b46a108c41454e32561c1b1c9..9c6bcdb21ff27ea6d640c7021ff81f1aff0f6ee3 100644 (file)
@@ -33,9 +33,9 @@ return [
     'app_custom_html_desc' => '설정 페이지를 제외한 모든 페이지 head 태그 끝머리에 추가합니다.',
     'app_custom_html_disabled_notice' => '문제가 생겨도 설정 페이지에서 되돌릴 수 있어요.',
     'app_logo' => '사이트 로고',
-    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',
-    'app_icon' => 'Application Icon',
-    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',
+    'app_logo_desc' => '이 이미지는 애플리케이션 헤더 표시줄 등 여러 영역에서 사용됩니다. 이 이미지의 높이는 86픽셀이어야 합니다. 큰 이미지는 축소됩니다.',
+    'app_icon' => '애플리케이션 아이콘',
+    'app_icon_desc' => '이 아이콘은 브라우저 탭과 바로 가기 아이콘에 사용됩니다. 256픽셀의 정사각형 PNG 이미지여야 합니다.',
     'app_homepage' => '처음 페이지',
     'app_homepage_desc' => '고른 페이지에 설정한 권한은 무시합니다.',
     'app_homepage_select' => '문서 고르기',
@@ -49,12 +49,12 @@ return [
     'app_disable_comments_desc' => '모든 페이지에서 댓글을 숨깁니다.',
 
     // Color settings
-    'color_scheme' => 'Application Color Scheme',
-    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',
-    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',
-    'app_color' => 'Primary Color',
-    'link_color' => 'Default Link Color',
-    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
+    'color_scheme' => '애플리케이션 색상 스키마',
+    'color_scheme_desc' => '애플리케이션 사용자 인터페이스에서 사용할 색상을 설정합니다. 테마에 가장 잘 맞으면서 가독성을 보장하기 위해 어두운 모드와 밝은 모드에 대해 색상을 개별적으로 구성할 수 있습니다.',
+    'ui_colors_desc' => '애플리케이션 기본 색상과 기본 링크 색상을 설정합니다. 기본 색상은 주로 헤더 배너, 버튼 및 인터페이스 장식에 사용됩니다. 기본 링크 색상은 작성된 콘텐츠와 애플리케이션 인터페이스 모두에서 텍스트 기반 링크 및 작업에 사용됩니다.',
+    'app_color' => '주 색상',
+    'link_color' => '기본 링크 색상',
+    'content_colors_desc' => '페이지 구성 계층 구조의 모든 요소에 대한 색상을 설정합니다. 가독성을 위해 기본 색상과 비슷한 밝기의 색상을 선택하는 것이 좋습니다.',
     'bookshelf_color' => '책꽂이 색상',
     'book_color' => '책 색상',
     'chapter_color' => '챕터 색상',
@@ -93,10 +93,10 @@ return [
     'maint_send_test_email_mail_text' => '메일을 정상적으로 수신했습니다.',
     'maint_recycle_bin_desc' => '지워진 콘텐츠는 휴지통에 들어가 복원하거나 영구 삭제할 수 있습니다. 오래된 항목은 자동으로 지워집니다.',
     'maint_recycle_bin_open' => '휴지통 열기',
-    'maint_regen_references' => 'Regenerate References',
-    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
-    'maint_regen_references_success' => 'Reference index has been regenerated!',
-    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+    'maint_regen_references' => '참조 재생성',
+    'maint_regen_references_desc' => '이 작업은 데이터베이스 내에서 항목 간 참조 색인을 다시 생성합니다. 이 작업은 일반적으로 자동으로 처리되지만 오래된 콘텐츠나 비공식적인 방법으로 추가된 콘텐츠의 색인을 생성하는 데 유용할 수 있습니다.',
+    'maint_regen_references_success' => '참조 색인이 다시 생성되었습니다!',
+    'maint_timeout_command_note' => '참고: 이 작업을 실행하는 데 시간이 걸릴 수 있으며, 일부 웹 환경에서는 시간 초과 문제가 발생할 수 있습니다. 대신 터미널 명령을 사용하여 이 작업을 수행할 수 있습니다.',
 
     // Recycle Bin
     'recycle_bin' => '휴지통',
@@ -137,11 +137,11 @@ return [
     // Role Settings
     'roles' => '권한',
     'role_user_roles' => '사용자 권한',
-    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
-    'roles_x_users_assigned' => ':count user assigned|:count users assigned',
-    'roles_x_permissions_provided' => ':count permission|:count permissions',
-    'roles_assigned_users' => 'Assigned Users',
-    'roles_permissions_provided' => 'Provided Permissions',
+    'roles_index_desc' => '역할은 사용자를 그룹화하고 구성원에게 시스템 권한을 제공하기 위해 사용됩니다. 사용자가 여러 역할의 구성원인 경우 부여된 권한이 중첩되며 모든 권한을 상속받게 됩니다.',
+    'roles_x_users_assigned' => ':count 명의 사용자가 할당됨|:count 명의 사용자가 할당됨',
+    'roles_x_permissions_provided' => ':count 개의 권한|:count 개의 권한',
+    'roles_assigned_users' => '할당된 사용자',
+    'roles_permissions_provided' => '제공된 권한',
     'role_create' => '권한 만들기',
     'role_delete' => '권한 제거',
     'role_delete_confirm' => ':roleName(을)를 지웁니다.',
@@ -168,7 +168,7 @@ return [
     'roles_system_warning' => '위 세 권한은 자신의 권한이나 다른 유저의 권한을 바꿀 수 있습니다.',
     'role_asset_desc' => '책, 챕터, 문서별 권한은 이 설정에 우선합니다.',
     'role_asset_admins' => 'Admin 권한은 어디든 접근할 수 있지만 이 설정은 사용자 인터페이스에서 해당 활동을 표시할지 결정합니다.',
-    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
+    'role_asset_image_view_note' => '이는 이미지 관리자 내 가시성과 관련이 있습니다. 업로드된 이미지 파일의 실제 접근은 시스템의 이미지 저장 설정에 따라 달라집니다.',
     'role_all' => '모든 항목',
     'role_own' => '직접 만든 항목',
     'role_controlled_by_asset' => '저마다 다름',
@@ -178,7 +178,7 @@ return [
 
     // Users
     'users' => '사용자',
-    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
+    'users_index_desc' => '시스템 내에서 개별 사용자 계정을 생성하고 관리합니다. 사용자 계정은 로그인과 콘텐츠 및 활동의 속성에 사용됩니다. 접근 권한은 주로 역할 기반이지만 사용자 콘텐츠 소유권도 권한 및 접근에 영향을 줄 수 있습니다.',
     'user_profile' => '사용자 프로필',
     'users_add_new' => '사용자 만들기',
     'users_search' => '사용자 검색',
@@ -248,8 +248,8 @@ return [
 
     // Webhooks
     'webhooks' => '웹 훅',
-    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
-    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',
+    'webhooks_index_desc' => '웹훅은 시스템 내에서 특정 작업 및 이벤트가 발생할 때 외부 URL로 데이터를 전송하는 방법으로, 메시징 또는 알림 시스템과 같은 외부 플랫폼과 이벤트 기반 통합을 가능하게 합니다.',
+    'webhooks_x_trigger_events' => ':count 개의 이벤트 트리거|:count 개의 이벤트 트리거',
     'webhooks_create' => '웹 훅 만들기',
     'webhooks_none_created' => '웹 훅이 없습니다.',
     'webhooks_edit' => '웹 훅 수정',
index ce573dac85d6a8b8c637da8e29bea907986cb62e..3f23ded6e776c9cd33e72b40fa82aae852779967 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Nuotraukų pasirinkimas',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Visi',
     'image_all_title' => 'Rodyti visas nuotraukas',
     'image_book_title' => 'Peržiūrėti nuotraukas, įkeltas į šią knygą',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Ar jūs esate tikri, kad norite ištrinti šią nuotrauką?',
     'image_select_image' => 'Pasirinkti nuotrauką',
     'image_dropzone' => 'Tempkite nuotraukas arba spauskite šia, kad įkeltumėte',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Nuotraukos ištrintos',
     'image_preview' => 'Nuotraukų peržiūra',
     'image_upload_success' => 'Nuotrauka įkelta sėkmingai',
     'image_update_success' => 'Nuotraukos detalės sėkmingai atnaujintos',
     'image_delete_success' => 'Nuotrauka sėkmingai ištrinti',
-    'image_upload_remove' => 'Pašalinti',
 
     // Code Editor
     'code_editor' => 'Redaguoti kodą',
index fb823e8dc46b50652c5b0ab4d7eb98b09ea51c65..99533019a67dee343836319fa374b03015597f7c 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Priedai',
     'attachments_explain' => 'Įkelkite kelis failus arba pridėkite nuorodas savo puslapyje. Jie matomi puslapio šoninėje juostoje.',
     'attachments_explain_instant_save' => 'Pakeitimai čia yra išsaugomi akimirksniu.',
-    'attachments_items' => 'Pridėti elementai',
     'attachments_upload' => 'Įkelti failą',
     'attachments_link' => 'Pridėti nuorodą',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Nustatyti nuorodą',
     'attachments_delete' => 'Ar esate tikri, kad norite ištrinti šį priedą?',
-    'attachments_dropzone' => 'Numesti failus arba paspausti čia ir pridėti failą',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Failai nebuvo įkelti',
     'attachments_explain_link' => 'Jūs galite pridėti nuorodas, jei nenorite įkelti failo. Tai gali būti nuoroda į kitą puslapį arba nuoroda į failą debesyje.',
     'attachments_link_name' => 'Nuorodos pavadinimas',
index 8dd3b66fd07ecc17324b607927a1b64d63254d7b..c36313abdbe447a6e1d7556ece8514fd6327fcc2 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Serveris negali sukurti miniatiūros. Prašome patikrinkite, ar turite įdiegtą GD PHP plėtinį.',
     'server_upload_limit' => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',
     'uploaded'  => 'Serveris neleidžia įkelti tokio dydžio failų. Prašome bandykite mažesnį failo dydį.',
-    'file_upload_timeout' => 'Failo įkėlimo laikas baigėsi',
 
     // Drawing & Images
     'image_upload_error' => 'Įvyko klaida įkeliant vaizdą',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Priedas nerastas',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Juodraščio išsaugoti nepavyko. Įsitikinkite, jog turite interneto ryšį prieš išsaugant šį paslapį.',
index b66461d5a48d35b8955bbc46a48341084ad9bd59..ac3b068ae1df13532fc79f5826f7448541f938ed 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Attēla izvēle',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Visi',
     'image_all_title' => 'Skatīt visus attēlus',
     'image_book_title' => 'Apskatīt augšupielādētos attēlus šajā grāmatā',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Vai tiešām vēlaties dzēst šo attēlu?',
     'image_select_image' => 'Atlasīt attēlu',
     'image_dropzone' => 'Ievilkt attēlu vai klikšķinat šeit, lai augšupielādētu',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Dzēstie attēli',
     'image_preview' => 'Attēla priekšskatījums',
     'image_upload_success' => 'Attēls ir veiksmīgi augšupielādēts',
     'image_update_success' => 'Attēlā informācija ir veiksmīgi atjunināta',
     'image_delete_success' => 'Attēls veiksmīgi dzēsts',
-    'image_upload_remove' => 'Noņemt',
 
     // Code Editor
     'code_editor' => 'Rediģēt kodu',
index e39199d101d9a78729528c269eb70562d20b6d89..01fd83b69024b23ea7325e77b86c968a09425433 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Pielikumi',
     'attachments_explain' => 'Augšupielādējiet dažus failus vai pievieno saites, kas tiks parādītas jūsu lapā. Tie būs redzami lapas sānjoslā.',
     'attachments_explain_instant_save' => 'Izmaiņas šeit tiek saglabātas nekavējoties.',
-    'attachments_items' => 'Pievienotie vienumi',
     'attachments_upload' => 'Augšupielādēt failu',
     'attachments_link' => 'Pievienot saiti',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Uzstādīt saiti',
     'attachments_delete' => 'Vai tiešām vēlaties dzēst šo pielikumu?',
-    'attachments_dropzone' => 'Ievilkt failus vai klikšķināt šeit, lai pievieotu failus',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Neviens fails nav augšupielādēts',
     'attachments_explain_link' => 'Ja nevēlaties augšupielādēt failu, varat pievienot saiti. Tā var būt saite uz citu lapu vai saite uz failu mākonī.',
     'attachments_link_name' => 'Saites nosaukums',
index 3c697716da43d9570f7f410cc42e706f0b24fcd2..655405681e98b74a3b7a1941fa19450936a1f0b2 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Serveris nevar izveidot samazinātus attēlus. Lūdzu pārbaudiet, vai ir uzstādīts PHP GD paplašinājums.',
     'server_upload_limit' => 'Serveris neatļauj šāda izmēra failu ielādi. Lūdzu mēģiniet mazāka izmēra failu.',
     'uploaded'  => 'Serveris neatļauj šāda izmēra failu ielādi. Lūdzu mēģiniet mazāka izmēra failu.',
-    'file_upload_timeout' => 'Faila augšupielādē ir iestājies noilgums.',
 
     // Drawing & Images
     'image_upload_error' => 'Radās kļūda augšupielādējot attēlu',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Pielikums nav atrasts',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Neizdevās saglabāt uzmetumu. Pārliecinieties, ka jūsu interneta pieslēgums ir aktīvs pirms saglabājiet šo lapu',
index cfc28c4096400b86a0cdf1f18e90e012938d1380..7bc0313194746209c1d7f07b91444c4e0df4044f 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Velg bilde',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alle',
     'image_all_title' => 'Vis alle bilder',
     'image_book_title' => 'Vis bilder som er lastet opp i denne boken',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Vil du slette dette bildet?',
     'image_select_image' => 'Velg bilde',
     'image_dropzone' => 'Dra og slipp eller trykk her for å laste opp bilder',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Bilder slettet',
     'image_preview' => 'Hurtigvisning av bilder',
     'image_upload_success' => 'Bilde ble lastet opp',
     'image_update_success' => 'Bildedetaljer ble oppdatert',
     'image_delete_success' => 'Bilde ble slettet',
-    'image_upload_remove' => 'Fjern',
 
     // Code Editor
     'code_editor' => 'Endre kode',
index f1bdc12a25022f922648d44254e73dd96df0101a..62c46f49c1a561753c0bd9bd7dc8e7e869a9d85d 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Vedlegg',
     'attachments_explain' => 'Last opp vedlegg eller legg til lenker for å berike innholdet. Disse vil vises i sidestolpen på siden.',
     'attachments_explain_instant_save' => 'Endringer her blir lagret med en gang.',
-    'attachments_items' => 'Vedlegg',
     'attachments_upload' => 'Last opp vedlegg',
     'attachments_link' => 'Fest lenke',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Angi lenke',
     'attachments_delete' => 'Er du sikker på at du vil fjerne vedlegget?',
-    'attachments_dropzone' => 'Dra og slipp eller trykk her for å feste vedlegg',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Ingen vedlegg er lastet opp',
     'attachments_explain_link' => 'Du kan feste lenker til denne. Det kan være henvisning til andre sider, bøker etc. eller lenker fra nettet.',
     'attachments_link_name' => 'Lenkenavn',
index fe420c1d47a31ea4f3f77754e24e470182a2e33b..0f017567a43249a2b46ac7223fdbb9726c43eced 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Kan ikke opprette miniatyrbilder. GD PHP er ikke installert.',
     'server_upload_limit' => 'Vedlegget er for stort, forsøk med et mindre vedlegg.',
     'uploaded'  => 'Tjenesten aksepterer ikke vedlegg som er så stor.',
-    'file_upload_timeout' => 'Opplastingen gikk ut på tid.',
 
     // Drawing & Images
     'image_upload_error' => 'Bildet kunne ikke lastes opp, forsøk igjen.',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Vedlegget ble ikke funnet',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Kunne ikke lagre utkastet, forsikre deg om at du er tilkoblet tjeneren (Har du nettilgang?)',
index de1bbf7376e8f94351f376ba7d6d99dd766aa1ca..1a210c6488dcaaa2c4a4d8238756754967df12c2 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selecteer Afbeelding',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alles',
     'image_all_title' => 'Alle afbeeldingen weergeven',
     'image_book_title' => 'Bekijk afbeeldingen die naar dit boek zijn geüpload',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Weet u zeker dat u deze afbeelding wilt verwijderen?',
     'image_select_image' => 'Kies afbeelding',
     'image_dropzone' => 'Sleep afbeeldingen naar hier of klik hier om te uploaden',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Afbeeldingen verwijderd',
     'image_preview' => 'Afbeelding voorbeeld',
     'image_upload_success' => 'Afbeelding succesvol geüpload',
     'image_update_success' => 'Afbeeldingsdetails succesvol bijgewerkt',
     'image_delete_success' => 'Afbeelding succesvol verwijderd',
-    'image_upload_remove' => 'Verwijder',
 
     // Code Editor
     'code_editor' => 'Bewerk Code',
index bac2868fce1d6854679cb48b8c7d7f242f13885a..2d67a3dc0832ca3f6e8e8d6c9091e5a9504b440f 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Bijlages',
     'attachments_explain' => 'Upload bijlages of voeg een link toe. Deze worden zichtbaar in het navigatiepaneel.',
     'attachments_explain_instant_save' => 'Wijzigingen worden meteen opgeslagen.',
-    'attachments_items' => 'Bijlages',
     'attachments_upload' => 'Bestand uploaden',
     'attachments_link' => 'Link toevoegen',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Zet link',
     'attachments_delete' => 'Weet u zeker dat u deze bijlage wilt verwijderen?',
-    'attachments_dropzone' => 'Sleep hier een bestand of klik hier om een bestand toe te voegen',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Er zijn geen bestanden geüpload',
     'attachments_explain_link' => 'Je kunt een hyperlink toevoegen als je geen bestanden wilt uploaden. Dit kan een link naar een andere pagina op deze website zijn, maar ook een link naar een andere website.',
     'attachments_link_name' => 'Link naam',
index a78862eae4674ce9dfc526d1de46d77b55c04aba..04a0200654dc740b53c687ff69ac98f69d8ed3f9 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'De server kon geen miniaturen maken. Controleer of je de GD PHP extensie geïnstalleerd hebt.',
     'server_upload_limit' => 'De server staat geen uploads van deze grootte toe. Probeer een kleinere bestandsgrootte.',
     'uploaded'  => 'De server staat geen uploads van deze grootte toe. Probeer een kleinere bestandsgrootte.',
-    'file_upload_timeout' => 'Het uploaden van het bestand is verlopen.',
 
     // Drawing & Images
     'image_upload_error' => 'Er is een fout opgetreden bij het uploaden van de afbeelding',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Bijlage niet gevonden',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Kon het concept niet opslaan. Zorg ervoor dat je een werkende internetverbinding hebt',
index b03ca35505ad6fb6008857f2989f95de86454773..2de01266b7d5fa3e1ae7890bb68f611857bab591 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Wybór obrazka',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Wszystkie',
     'image_all_title' => 'Zobacz wszystkie obrazki',
     'image_book_title' => 'Zobacz obrazki zapisane w tej książce',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Czy na pewno chcesz usunąć ten obraz?',
     'image_select_image' => 'Wybierz obrazek',
     'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do przesłania',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Usunięte obrazki',
     'image_preview' => 'Podgląd obrazka',
     'image_upload_success' => 'Obrazek przesłany pomyślnie',
     'image_update_success' => 'Szczegóły obrazka zaktualizowane pomyślnie',
     'image_delete_success' => 'Obrazek usunięty pomyślnie',
-    'image_upload_remove' => 'Usuń',
 
     // Code Editor
     'code_editor' => 'Edytuj kod',
index eb46ed4ebe6a76af3dde4e048108e0c3e2bf16d9..1f3e242f4c925917c93091455181574112ba3f7a 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Załączniki',
     'attachments_explain' => 'Prześlij kilka plików lub załącz linki. Będą one widoczne na pasku bocznym strony.',
     'attachments_explain_instant_save' => 'Zmiany są zapisywane natychmiastowo.',
-    'attachments_items' => 'Załączniki',
     'attachments_upload' => 'Dodaj plik',
     'attachments_link' => 'Dodaj link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Ustaw link',
     'attachments_delete' => 'Jesteś pewien, że chcesz usunąć ten załącznik?',
-    'attachments_dropzone' => 'Upuść pliki lub kliknij tutaj by przesłać pliki',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nie przesłano żadnych plików',
     'attachments_explain_link' => 'Możesz załączyć link jeśli nie chcesz przesyłać pliku. Może być to link do innej strony lub link do pliku w chmurze.',
     'attachments_link_name' => 'Nazwa linku',
index d91e01f5a4ecd2548dc4ee9e14ac058a49234cdb..baea4f6aaa13ebae42f7ea2fdbe03d4bb672a55a 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Serwer nie może utworzyć miniaturek. Upewnij się że rozszerzenie GD PHP zostało zainstalowane.',
     'server_upload_limit' => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj przesłać plik o mniejszym rozmiarze.',
     'uploaded'  => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj przesłać plik o mniejszym rozmiarze.',
-    'file_upload_timeout' => 'Przesyłanie pliku przekroczyło limit czasu.',
 
     // Drawing & Images
     'image_upload_error' => 'Wystąpił błąd podczas przesyłania obrazka',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Nie znaleziono załącznika',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Zapis wersji roboczej nie powiódł się. Upewnij się, że posiadasz połączenie z internetem.',
index bdec2022696dfe1327b0fda480da12c425068ed1..67d29e205a65f82d03c0a819016ecc18dc14ddc3 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selecionar Imagem',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Todas',
     'image_all_title' => 'Visualizar todas as imagens',
     'image_book_title' => 'Visualizar imagens relacionadas a este livro',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Tem certeza de que deseja eliminar esta imagem?',
     'image_select_image' => 'Selecionar Imagem',
     'image_dropzone' => 'Arraste imagens ou carregue aqui para fazer upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Imagens Eliminadas',
     'image_preview' => 'Pré-visualização de Imagem',
     'image_upload_success' => 'Carregamento da imagem efetuado com sucesso',
     'image_update_success' => 'Detalhes da imagem atualizados com sucesso',
     'image_delete_success' => 'Imagem eliminada com sucesso',
-    'image_upload_remove' => 'Remover',
 
     // Code Editor
     'code_editor' => 'Editar Código',
index 347196a0dac890a5ab86440dad622a6dad07fc4e..44ebf22221c3221fcbd1bb3bb555a88b2e95f9a4 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Anexos',
     'attachments_explain' => 'Carregue alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',
     'attachments_explain_instant_save' => 'As mudanças são guardadas instantaneamente.',
-    'attachments_items' => 'Itens Anexados',
     'attachments_upload' => 'Carregamento de Arquivos',
     'attachments_link' => 'Anexar Link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Definir Link',
     'attachments_delete' => 'Tem certeza de que deseja eliminar este anexo?',
-    'attachments_dropzone' => 'Arraste arquivos para aqui ou clique para os anexar',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nenhum arquivo foi enviado',
     'attachments_explain_link' => 'Pode anexar um link se preferir não fazer o carregamento do arquivo. O link poderá ser para uma outra página ou para um arquivo na nuvem.',
     'attachments_link_name' => 'Nome do Link',
index 66dc0c49da32b57d58434e529be044cdc881282f..b2c8d909067174b14ecabee0f554883bfc5922d9 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',
     'server_upload_limit' => 'O servidor não permite o carregamento de arquivos com esse tamanho. Por favor, tente fazer o carregamento de arquivos mais pequenos.',
     'uploaded'  => 'O servidor não permite o carregamento de arquivos com esse tamanho. Por favor, tente fazer o carregamento de arquivos mais pequenos.',
-    'file_upload_timeout' => 'O carregamento do arquivo expirou.',
 
     // Drawing & Images
     'image_upload_error' => 'Ocorreu um erro no carregamento da imagem',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Anexo não encontrado',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Falha ao tentar guardar o rascunho. Certifique-se que a conexão de Internet está funcional antes de tentar guardar esta página',
index a1210d52ce99c92a784e66a063fdb48a53e76a77..32ce5ef93e5f96bbb7fff0b882c109273e0fed8e 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selecionar Imagem',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Todas',
     'image_all_title' => 'Visualizar todas as imagens',
     'image_book_title' => 'Visualizar imagens relacionadas a esse livro',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Tem certeza de que deseja excluir essa imagem?',
     'image_select_image' => 'Selecionar Imagem',
     'image_dropzone' => 'Arraste imagens ou clique aqui para fazer upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Imagens Excluídas',
     'image_preview' => 'Pré-Visualização de Imagem',
     'image_upload_success' => 'Upload de imagem efetuado com sucesso',
     'image_update_success' => 'Detalhes da imagem atualizados com sucesso',
     'image_delete_success' => 'Imagem excluída com sucesso',
-    'image_upload_remove' => 'Remover',
 
     // Code Editor
     'code_editor' => 'Editar Código',
index 6654f9d6fefdf937e20edea0b61c7e61b054876b..2aac777aefbbce9df3b0a3cb00cb039ccf8ca7cd 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Anexos',
     'attachments_explain' => 'Faça o upload de alguns arquivos ou anexe links para serem exibidos na sua página. Eles estarão visíveis na barra lateral à direita.',
     'attachments_explain_instant_save' => 'Mudanças são salvas instantaneamente.',
-    'attachments_items' => 'Itens Anexados',
     'attachments_upload' => 'Upload de Arquivos',
     'attachments_link' => 'Links Anexados',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Definir Link',
     'attachments_delete' => 'Tem certeza de que deseja excluir esse anexo?',
-    'attachments_dropzone' => 'Arraste arquivos para cá ou clique para anexar arquivos',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nenhum arquivo foi enviado',
     'attachments_explain_link' => 'Você pode anexar um link se preferir não fazer o upload do arquivo. O link poderá ser para uma outra página ou para um arquivo na nuvem.',
     'attachments_link_name' => 'Nome do Link',
index 34843670aec3b8b5b9c8546c445b5f8f0ba9679f..d6fa21c953ab3512fb133ace7afa50359318fb3f 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',
     'server_upload_limit' => 'O servidor não permite o upload de arquivos com esse tamanho. Por favor, tente fazer o upload de arquivos de menor tamanho.',
     'uploaded'  => 'O servidor não permite o upload de arquivos com esse tamanho. Por favor, tente fazer o upload de arquivos de menor tamanho.',
-    'file_upload_timeout' => 'O upload do arquivo expirou.',
 
     // Drawing & Images
     'image_upload_error' => 'Um erro aconteceu enquanto o servidor tentava efetuar o upload da imagem',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Anexo não encontrado',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Falha ao tentar salvar o rascunho. Certifique-se que a conexão de internet está funcional antes de tentar salvar essa página',
index 1284ca104149493a072aba0942f22a22882f3948..75beb135dcf6503542cb5b43d24854337e7f0888 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Selectează imaginea',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Tot',
     'image_all_title' => 'Vezi toate imaginile',
     'image_book_title' => 'Vezi imaginile încărcate în această carte',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Ești sigur că vrei să ștergi această imagine?',
     'image_select_image' => 'Selectează imaginea',
     'image_dropzone' => 'Trage imaginile sau apasă aici pentru a le încărca',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Imagini șterse',
     'image_preview' => 'Previzualizare imagine',
     'image_upload_success' => 'Imaginea a fost încărcată cu succes',
     'image_update_success' => 'Detalii imagine actualizate cu succes',
     'image_delete_success' => 'Imaginea a fost ștearsă',
-    'image_upload_remove' => 'Elimină',
 
     // Code Editor
     'code_editor' => 'Editare cod',
index 5b8ffd47346d3ad85e23816f671ccc0a96cda26a..24bc2dcfa5b63faffdc4734551e8a842ed6179b8 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Atașamente',
     'attachments_explain' => 'Încarcă unele fișiere sau atașează unele link-uri pentru a fi afișate pe pagina ta. Acestea sunt vizibile în bara laterală a paginii.',
     'attachments_explain_instant_save' => 'Modificările de aici sunt salvate instant.',
-    'attachments_items' => 'Elemente atașate',
     'attachments_upload' => 'Încarcă fișier',
     'attachments_link' => 'Atașare link',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Setează link',
     'attachments_delete' => 'Ești sigur că dorești să ștergi acest atașament?',
-    'attachments_dropzone' => 'Trage fișiere sau apasă aici pentru a atașa un fișier',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Niciun fișier nu a fost încărcat',
     'attachments_explain_link' => 'Poți atașa un link dacă ai prefera să nu încarci un fișier. Acesta poate fi un link către o altă pagină sau un link către un fișier în cloud.',
     'attachments_link_name' => 'Nume link',
index d5838ba60394786dab0c941b54ddbe8c3d3e1629..24f19292d89b55f1570323744739bd5153f77a91 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Serverul nu poate crea miniaturi. Verifică dacă este instalată extensia GD PHP.',
     'server_upload_limit' => 'Serverul nu permite încărcarea acestei dimensiuni. Te rog să încerci o dimensiune mai mică a fișierului.',
     'uploaded'  => 'Serverul nu permite încărcarea acestei dimensiuni. Te rog să încerci o dimensiune mai mică a fișierului.',
-    'file_upload_timeout' => 'Încărcarea fișierului a expirat.',
 
     // Drawing & Images
     'image_upload_error' => 'A apărut o eroare la încărcarea imaginii',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Atașamentul nu a fost găsit',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Nu s-a reușit salvarea ciornei. Asigură-te că ai conexiune la internet înainte de a salva această pagină',
index 12b1dd7cfbdd13723381068facf0aa7a66650953..0c59337103e32fab375c8a76f0d5cb0a4061c898 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Выбрать изображение',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Все',
     'image_all_title' => 'Просмотр всех изображений',
     'image_book_title' => 'Просмотр всех изображений, загруженных в эту книгу',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Вы уверены, что хотите удалить это изображение?',
     'image_select_image' => 'Выбрать изображение',
     'image_dropzone' => 'Перетащите изображение или кликните для загрузки',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Изображения удалены',
     'image_preview' => 'Предпросмотр изображения',
     'image_upload_success' => 'Изображение успешно загружено',
     'image_update_success' => 'Детали изображения успешно обновлены',
     'image_delete_success' => 'Изображение успешно удалено',
-    'image_upload_remove' => 'Удалить изображение',
 
     // Code Editor
     'code_editor' => 'Изменить код',
index 21ae4fdb99c5bcb26bfe2ec335bf25886ec1d28b..ff197ed521dba2a8295ad8abef059b3366637aa1 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Вложения',
     'attachments_explain' => 'Загрузите несколько файлов или добавьте ссылку для отображения на своей странице. Они видны на боковой панели страницы.',
     'attachments_explain_instant_save' => 'Изменения здесь сохраняются мгновенно.',
-    'attachments_items' => 'Прикрепленные элементы',
     'attachments_upload' => 'Загрузить файл',
     'attachments_link' => 'Присоединить ссылку',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Установить ссылку',
     'attachments_delete' => 'Вы уверены, что хотите удалить это вложение?',
-    'attachments_dropzone' => 'Перетащите файл сюда или нажмите здесь, чтобы загрузить файл',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Файлы не загружены',
     'attachments_explain_link' => 'Вы можете присоединить ссылку, если вы предпочитаете не загружать файл. Это может быть ссылка на другую страницу или ссылка на файл в облаке.',
     'attachments_link_name' => 'Название ссылки',
index 8aa231d421b8c43cdde248c71685a99d1d0b1b42..a2413ed41e855d647d369e42c7d7023017a89056 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Сервер не может создавать эскизы. Убедитесь, что у вас установлено расширение GD PHP.',
     'server_upload_limit' => 'Сервер не разрешает загрузку файлов такого размера. Попробуйте уменьшить размер файла.',
     'uploaded'  => 'Сервер не позволяет загружать файлы такого размера. Пожалуйста, попробуйте файл меньше.',
-    'file_upload_timeout' => 'Время загрузки файла истекло.',
 
     // Drawing & Images
     'image_upload_error' => 'Произошла ошибка при загрузке изображения',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Вложение не найдено',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Не удалось сохранить черновик. Перед сохранением этой страницы убедитесь, что у вас есть подключение к Интернету.',
index a6bae619c47be8ecc19bf88725909c61634d2985..9efecb15de35ed6715b020817685e78c7f80bb70 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Vybrať obrázok',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Všetko',
     'image_all_title' => 'Zobraziť všetky obrázky',
     'image_book_title' => 'Zobraziť obrázky nahrané do tejto knihy',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Naozaj chcete vymazať tento obrázok?',
     'image_select_image' => 'Vybrať obrázok',
     'image_dropzone' => 'Presuňte obrázky sem alebo kliknite sem pre nahranie',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Obrázky zmazané',
     'image_preview' => 'Náhľad obrázka',
     'image_upload_success' => 'Obrázok úspešne nahraný',
     'image_update_success' => 'Detaily obrázka úspešne aktualizované',
     'image_delete_success' => 'Obrázok úspešne zmazaný',
-    'image_upload_remove' => 'Odstrániť',
 
     // Code Editor
     'code_editor' => 'Upraviť kód',
index e9423817ef57585e5ae10ae0969493a75c8218b1..61ac9ca9e2343084b52b38fa5c6b9250d6fa90af 100644 (file)
@@ -69,48 +69,48 @@ return [
     'edit_code_block' => 'Upraviť blok kódu',
     'insert_drawing' => 'Vložiť/upraviť výkres',
     'drawing_manager' => 'Manažér kreslenia',
-    'insert_media' => 'Insert/edit media',
-    'insert_media_title' => 'Insert/Edit Media',
-    'clear_formatting' => 'Clear formatting',
-    'source_code' => 'Source code',
-    'source_code_title' => 'Source Code',
-    'fullscreen' => 'Fullscreen',
-    'image_options' => 'Image options',
+    'insert_media' => 'Vložiť/Upraviť média',
+    'insert_media_title' => 'Vložiť/Upraviť média',
+    'clear_formatting' => 'Vymazať formátovanie',
+    'source_code' => 'Zdrojový kód',
+    'source_code_title' => 'Zdrojový kód',
+    'fullscreen' => 'Celá obrazovka',
+    'image_options' => 'Možnosti obrázka',
 
     // Tables
-    'table_properties' => 'Table properties',
-    'table_properties_title' => 'Table Properties',
-    'delete_table' => 'Delete table',
-    'insert_row_before' => 'Insert row before',
-    'insert_row_after' => 'Insert row after',
-    'delete_row' => 'Delete row',
-    'insert_column_before' => 'Insert column before',
-    'insert_column_after' => 'Insert column after',
-    'delete_column' => 'Delete column',
-    'table_cell' => 'Cell',
-    'table_row' => 'Row',
-    'table_column' => 'Column',
-    'cell_properties' => 'Cell properties',
-    'cell_properties_title' => 'Cell Properties',
-    'cell_type' => 'Cell type',
-    'cell_type_cell' => 'Cell',
-    'cell_scope' => 'Scope',
-    'cell_type_header' => 'Header cell',
-    'merge_cells' => 'Merge cells',
-    'split_cell' => 'Split cell',
-    'table_row_group' => 'Row Group',
-    'table_column_group' => 'Column Group',
-    'horizontal_align' => 'Horizontal align',
-    'vertical_align' => 'Vertical align',
-    'border_width' => 'Border width',
-    'border_style' => 'Border style',
-    'border_color' => 'Border color',
-    'row_properties' => 'Row properties',
-    'row_properties_title' => 'Row Properties',
-    'cut_row' => 'Cut row',
-    'copy_row' => 'Copy row',
-    'paste_row_before' => 'Paste row before',
-    'paste_row_after' => 'Paste row after',
+    'table_properties' => 'Vlastnosti tabuľky',
+    'table_properties_title' => 'Vlastnosti tabuľky',
+    'delete_table' => 'Vymazať tabuľku',
+    'insert_row_before' => 'Vložiť riadok pred',
+    'insert_row_after' => 'Vložiť riadok za',
+    'delete_row' => 'Vymazať riadok',
+    'insert_column_before' => 'Vložiť stĺpec pred',
+    'insert_column_after' => 'Vložiť stĺpec za',
+    'delete_column' => 'Vymazať stĺpec',
+    'table_cell' => 'Bunka',
+    'table_row' => 'Riadok',
+    'table_column' => 'Stĺpec',
+    'cell_properties' => 'Vlastnosti bunky',
+    'cell_properties_title' => 'Vlastnosti bunky',
+    'cell_type' => 'Typ bunky',
+    'cell_type_cell' => 'Bunka',
+    'cell_scope' => 'Rozsah',
+    'cell_type_header' => 'Bunka hlavičky',
+    'merge_cells' => 'Zlúčiť bunky',
+    'split_cell' => 'Rozdeliť bunku',
+    'table_row_group' => 'Skupina riadkov',
+    'table_column_group' => 'Skupina stĺpcov',
+    'horizontal_align' => 'Horizontálne zarovnanie',
+    'vertical_align' => 'Vertikálne zarovnanie',
+    'border_width' => 'Šírka orámovania',
+    'border_style' => 'Štýl orámovania',
+    'border_color' => 'Farba orámovania',
+    'row_properties' => 'Vlastnosti riadku',
+    'row_properties_title' => 'Vlastnosti riadku',
+    'cut_row' => 'Vystrihnúť riadok',
+    'copy_row' => 'Kopírovať riadok',
+    'paste_row_before' => 'Pridať riadok pred',
+    'paste_row_after' => 'Pridať riadok za',
     'row_type' => 'Typ riadku',
     'row_type_header' => 'Hlavička',
     'row_type_body' => 'Telo',
@@ -165,10 +165,10 @@ return [
     'save_continue' => 'Uložiť a pokračovať',
     'callouts_cycle' => '(Podržte stlačené, aby ste prepínali medzi typmi)',
     'link_selector' => 'Prejsť na obsah',
-    'shortcuts' => 'Shortcuts',
-    'shortcut' => 'Shortcut',
-    'shortcuts_intro' => 'The following shortcuts are available in the editor:',
+    'shortcuts' => 'Skratky',
+    'shortcut' => 'Skratka',
+    'shortcuts_intro' => 'V editore sú k dispozícii nasledujúce skratky:',
     'windows_linux' => '(Windows/Linux)',
     'mac' => '(Mac)',
-    'description' => 'Description',
+    'description' => 'Popis',
 ];
index a8b897ab2ab604b3914046cf3368c6f44f1d8c0e..3ed20f63daa2d996b53be1ebf6d9425f75fe17a6 100644 (file)
@@ -23,9 +23,9 @@ return [
     'meta_updated' => 'Aktualizované :timeLength',
     'meta_updated_name' => 'Aktualizované :timeLength používateľom :user',
     'meta_owned_name' => 'Vlastník :user',
-    'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
+    'meta_reference_page_count' => 'Referencia na :count page|Referencia na :count pages',
     'entity_select' => 'Entita vybraná',
-    'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
+    'entity_select_lack_permission' => 'Na výber tejto položky nemáte potrebné povolenia',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Moje nedávne koncepty',
     'my_recently_viewed' => 'Nedávno mnou zobrazené',
@@ -42,15 +42,15 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'Oprávnenia',
-    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
-    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
-    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_desc' => 'Tu nastavte povolenia na prepísanie predvolených povolení poskytnutých rolami používateľov.',
+    'permissions_book_cascade' => 'Povolenia nastavené pre knihy sa automaticky prenesú do podriadených kapitol a strán, pokiaľ nemajú definované vlastné povolenia.',
+    'permissions_chapter_cascade' => 'Povolenia nastavené pre kapitoly sa automaticky prenesú na podradené stránky, pokiaľ nemajú definované vlastné povolenia.',
     'permissions_save' => 'Uložiť oprávnenia',
     'permissions_owner' => 'Vlastník',
-    'permissions_role_everyone_else' => 'Everyone Else',
-    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
-    'permissions_role_override' => 'Override permissions for role',
-    'permissions_inherit_defaults' => 'Inherit defaults',
+    'permissions_role_everyone_else' => 'Všetci ostatní',
+    'permissions_role_everyone_else_desc' => 'Nastavte povolenia pre všetky roly, ktoré nie sú špecificky prepísané.',
+    'permissions_role_override' => 'Prepísať povolenia pre rolu',
+    'permissions_inherit_defaults' => 'Zdediť predvolené hodnoty',
 
     // Search
     'search_results' => 'Výsledky hľadania',
@@ -93,23 +93,23 @@ return [
     'shelves_save' => 'Uložiť policu',
     'shelves_books' => 'Knihy na tejto polici',
     'shelves_add_books' => 'Pridať knihy do tejto police',
-    'shelves_drag_books' => 'Drag books below to add them to this shelf',
+    'shelves_drag_books' => 'Potiahnite knihy nižšie a pridajte ich do tejto police',
     'shelves_empty_contents' => 'Táto polica nemá priradené žiadne knihy',
     'shelves_edit_and_assign' => 'Uprav policu a priraď knihy',
-    'shelves_edit_named' => 'Edit Shelf :name',
-    'shelves_edit' => 'Edit Shelf',
-    'shelves_delete' => 'Delete Shelf',
-    'shelves_delete_named' => 'Delete Shelf :name',
-    'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
-    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
-    'shelves_permissions' => 'Shelf Permissions',
-    'shelves_permissions_updated' => 'Shelf Permissions Updated',
-    'shelves_permissions_active' => 'Shelf Permissions Active',
-    'shelves_permissions_cascade_warning' => 'Permissions on shelves 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_edit_named' => 'Upraviť policu :name',
+    'shelves_edit' => 'Upraviť policu',
+    'shelves_delete' => 'Odstrániť policu',
+    'shelves_delete_named' => 'Odstrániť policu :name',
+    'shelves_delete_explain' => "Týmto vymažete policu s názvom ': name'. Obsahované knihy sa neodstránia.",
+    'shelves_delete_confirmation' => 'Ste si istý, že chcete zmazať túto policu?',
+    'shelves_permissions' => 'Povolenia police',
+    'shelves_permissions_updated' => 'Povolenia police aktualizované',
+    'shelves_permissions_active' => 'Povolenia police aktívne',
+    'shelves_permissions_cascade_warning' => 'Povolenia na poličkách sa automaticky nepriraďujú k obsiahnutým knihám. Je to preto, že kniha môže existovať na viacerých poličkách. Povolenia však možno skopírovať do kníh pomocou možnosti uvedenej nižšie.',
     'shelves_copy_permissions_to_books' => 'Kopírovať oprávnenia pre knihy',
     'shelves_copy_permissions' => 'Kopírovať oprávnenia',
-    'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
-    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
+    'shelves_copy_permissions_explain' => 'Týmto sa použijú aktuálne nastavenia povolení tejto police na všetky knihy, ktoré obsahuje. Pred aktiváciou sa uistite, že všetky zmeny povolení tejto police boli uložené.',
+    'shelves_copy_permission_success' => 'Oprávnenia police boli skopírované {0}:count kníh|{1}:count kniha|[2,3,4]:count knihy|[5,*]:count kníh',
 
     // Books
     'book' => 'Kniha',
@@ -141,7 +141,7 @@ return [
     'books_search_this' => 'Hľadať v tejto knihe',
     'books_navigation' => 'Navigácia knihy',
     'books_sort' => 'Zoradiť obsah knihy',
-    'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.',
+    'books_sort_desc' => 'Presúvajte kapitoly a strany v rámci knihy, aby ste reorganizovali jej obsah. Je možné pridať ďalšie knihy, čo umožňuje jednoduché presúvanie kapitol a strán medzi knihami.',
     'books_sort_named' => 'Zoradiť knihu :bookName',
     'books_sort_name' => 'Zoradiť podľa mena',
     'books_sort_created' => 'Zoradiť podľa dátumu vytvorenia',
@@ -150,19 +150,19 @@ return [
     'books_sort_chapters_last' => 'Kapitoly ako posledné',
     'books_sort_show_other' => 'Zobraziť ostatné knihy',
     'books_sort_save' => 'Uložiť nové zoradenie',
-    'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
-    'books_sort_move_up' => 'Move Up',
-    'books_sort_move_down' => 'Move Down',
-    'books_sort_move_prev_book' => 'Move to Previous Book',
-    'books_sort_move_next_book' => 'Move to Next Book',
-    'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
-    'books_sort_move_next_chapter' => 'Move Into Next Chapter',
-    'books_sort_move_book_start' => 'Move to Start of Book',
-    'books_sort_move_book_end' => 'Move to End of Book',
-    'books_sort_move_before_chapter' => 'Move to Before Chapter',
-    'books_sort_move_after_chapter' => 'Move to After Chapter',
-    'books_copy' => 'Copy Book',
-    'books_copy_success' => 'Book successfully copied',
+    'books_sort_show_other_desc' => 'Pridajte ďalšie knihy, aby ste ich zahrnuli do operácie triedenia a umožnili jednoduchú reorganizáciu medzi knihami.',
+    'books_sort_move_up' => 'Posunúť vyššie',
+    'books_sort_move_down' => 'Posunúť nižšie',
+    'books_sort_move_prev_book' => 'Presun na predchádzajúcu knihu',
+    'books_sort_move_next_book' => 'Presun na nasledujúcu knihu',
+    'books_sort_move_prev_chapter' => 'Presun na predchádzajúcu kapitolu',
+    'books_sort_move_next_chapter' => 'Presun na ďalšiu kapitolu',
+    'books_sort_move_book_start' => 'Presun na začiatok knihy',
+    'books_sort_move_book_end' => 'Presun na koniec knihy',
+    'books_sort_move_before_chapter' => 'Prejsť na Pred kapitolou',
+    'books_sort_move_after_chapter' => 'Prejsť na Po kapitole',
+    'books_copy' => 'Kopírovať knihu',
+    'books_copy_success' => 'Kniha bola skopírovaná',
 
     // Chapters
     'chapter' => 'Kapitola',
@@ -181,14 +181,14 @@ return [
     'chapters_move' => 'Presunúť kapitolu',
     'chapters_move_named' => 'Presunúť kapitolu :chapterName',
     'chapter_move_success' => 'Kapitola presunutá do :bookName',
-    'chapters_copy' => 'Copy Chapter',
-    'chapters_copy_success' => 'Chapter successfully copied',
+    'chapters_copy' => 'Kopírovať kapitolu',
+    'chapters_copy_success' => 'Kapitola bola úspešne skopírovaná',
     'chapters_permissions' => 'Oprávnenia kapitoly',
     'chapters_empty' => 'V tejto kapitole nie sú teraz žiadne stránky.',
     'chapters_permissions_active' => 'Oprávnenia kapitoly aktívne',
     'chapters_permissions_success' => 'Oprávnenia kapitoly aktualizované',
     'chapters_search_this' => 'Hladať v kapitole',
-    'chapter_sort_book' => 'Sort Book',
+    'chapter_sort_book' => 'Triediť knihu',
 
     // Pages
     'page' => 'Stránka',
@@ -215,19 +215,19 @@ return [
     'pages_edit_draft_save_at' => 'Koncept uložený pod ',
     'pages_edit_delete_draft' => 'Uložiť koncept',
     'pages_edit_discard_draft' => 'Zrušiť koncept',
-    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
-    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
-    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
-    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
+    'pages_edit_switch_to_markdown' => 'Prepnite na Markdown Editor',
+    'pages_edit_switch_to_markdown_clean' => '(Vyčistiť obsah)',
+    'pages_edit_switch_to_markdown_stable' => '(Stabilný obsah)',
+    'pages_edit_switch_to_wysiwyg' => 'Prepnite na WYSIWYG Editor',
     'pages_edit_set_changelog' => 'Nastaviť záznam zmien',
     'pages_edit_enter_changelog_desc' => 'Zadajte krátky popis zmien, ktoré ste urobili',
     'pages_edit_enter_changelog' => 'Zadať záznam zmien',
-    'pages_editor_switch_title' => 'Switch Editor',
-    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
-    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
-    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
-    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
-    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
+    'pages_editor_switch_title' => 'Prepnúť editor',
+    'pages_editor_switch_are_you_sure' => 'Naozaj chcete zmeniť editor pre túto stránku?',
+    'pages_editor_switch_consider_following' => 'Pri zmene editorov zvážte nasledujúce:',
+    'pages_editor_switch_consideration_a' => 'Po uložení budú novú možnosť editora používať všetci budúci editori vrátane tých, ktorí nemusia byť schopní sami zmeniť typ editora.',
+    'pages_editor_switch_consideration_b' => 'Toto môže za určitých okolností potenciálne viesť k strate podrobností a syntaxe.',
+    'pages_editor_switch_consideration_c' => 'Zmeny v značke alebo v protokole zmien vykonané od posledného uloženia nezostanú pri tejto zmene zachované.',
     'pages_save' => 'Uložiť stránku',
     'pages_title' => 'Titulok stránky',
     'pages_name' => 'Názov stránky',
@@ -236,8 +236,8 @@ return [
     'pages_md_insert_image' => 'Vložiť obrázok',
     'pages_md_insert_link' => 'Vložiť odkaz na entitu',
     'pages_md_insert_drawing' => 'Vložiť kresbu',
-    'pages_md_show_preview' => 'Show preview',
-    'pages_md_sync_scroll' => 'Sync preview scroll',
+    'pages_md_show_preview' => 'Zobraziť náhľad',
+    'pages_md_sync_scroll' => 'Posúvanie ukážky synchronizácie',
     'pages_not_in_chapter' => 'Stránka nie je v kapitole',
     'pages_move' => 'Presunúť stránku',
     'pages_move_success' => 'Stránka presunutá do ":parentName"',
@@ -248,17 +248,17 @@ return [
     'pages_permissions_success' => 'Oprávnenia stránky aktualizované',
     'pages_revision' => 'Revízia',
     'pages_revisions' => 'Revízie stránky',
-    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
+    'pages_revisions_desc' => 'Nižšie sú uvedené všetky predchádzajúce revízie tejto stránky. Ak to povolenia umožňujú, môžete sa pozrieť späť, porovnať a obnoviť staré verzie stránok. Úplná história stránky sa tu nemusí úplne prejaviť, pretože v závislosti od konfigurácie systému môžu byť staré revízie automaticky odstránené.',
     'pages_revisions_named' => 'Revízie stránky :pageName',
     'pages_revision_named' => 'Revízia stránky :pageName',
     'pages_revision_restored_from' => 'Obnovené z #:id; :summary',
     'pages_revisions_created_by' => 'Vytvoril',
     'pages_revisions_date' => 'Dátum revízie',
     'pages_revisions_number' => 'č.',
-    'pages_revisions_sort_number' => 'Revision Number',
+    'pages_revisions_sort_number' => 'Číslo revízie',
     'pages_revisions_numbered' => 'Revízia č. :id',
     'pages_revisions_numbered_changes' => 'Zmeny revízie č. ',
-    'pages_revisions_editor' => 'Editor Type',
+    'pages_revisions_editor' => 'Typ editora',
     'pages_revisions_changelog' => 'Záznam zmien',
     'pages_revisions_changes' => 'Zmeny',
     'pages_revisions_current' => 'Aktuálna verzia',
@@ -269,7 +269,7 @@ return [
     'pages_edit_content_link' => 'Upraviť obsah',
     'pages_permissions_active' => 'Oprávnienia stránky aktívne',
     'pages_initial_revision' => 'Prvé zverejnenie',
-    'pages_references_update_revision' => 'System auto-update of internal links',
+    'pages_references_update_revision' => 'Automatická aktualizácia systému interných odkazov',
     'pages_initial_name' => 'Nová stránka',
     'pages_editing_draft_notification' => 'Práve upravujete koncept, ktorý bol naposledy uložený :timeDiff.',
     'pages_draft_edited_notification' => 'Táto stránka bola odvtedy upravená. Odporúča sa odstrániť tento koncept.',
@@ -292,7 +292,7 @@ return [
     'shelf_tags' => 'Štítky knižníc',
     'tag' => 'Štítok',
     'tags' =>  'Štítky',
-    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
+    'tags_index_desc' => 'Značky možno použiť na obsah v rámci systému a použiť flexibilnú formu kategorizácie. Značky môžu mať kľúč aj hodnotu, pričom hodnota je voliteľná. Po použití je možné obsah vyhľadávať pomocou názvu a hodnoty značky.',
     'tag_name' =>  'Názov štítku',
     'tag_value' => 'Hodnota štítku (Voliteľné)',
     'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.",
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Prílohy',
     'attachments_explain' => 'Nahrajte nejaké súbory alebo priložte zopár odkazov pre zobrazenie na Vašej stránke. Budú viditeľné v bočnom paneli.',
     'attachments_explain_instant_save' => 'Zmeny budú okamžite uložené.',
-    'attachments_items' => 'Priložené položky',
     'attachments_upload' => 'Nahrať súbor',
     'attachments_link' => 'Priložiť odkaz',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Nastaviť odkaz',
     'attachments_delete' => 'Naozaj chcete odstrániť túto prílohu?',
-    'attachments_dropzone' => 'Presuňte súbory alebo klinknite sem pre priloženie súboru',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Žiadne súbory neboli nahrané',
     'attachments_explain_link' => 'Ak nechcete priložiť súbor, môžete priložiť odkaz. Môže to byť odkaz na inú stránku alebo odkaz na súbor v cloude.',
     'attachments_link_name' => 'Názov odkazu',
@@ -374,27 +374,27 @@ return [
     'revision_cannot_delete_latest' => 'Nie je možné vymazať poslednú revíziu.',
 
     // 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' => 'Pri kopírovaní obsahu zvážte nižšie uvedené.',
+    'copy_consider_permissions' => 'Vlastné nastavenia povolení sa neskopírujú.',
+    'copy_consider_owner' => 'Stanete sa vlastníkom všetkého skopírovaného obsahu.',
+    'copy_consider_images' => 'Súbory obrázkov stránky nebudú duplikované a pôvodné obrázky si zachovajú svoj vzťah k stránke, na ktorú boli pôvodne nahrané.',
+    'copy_consider_attachments' => 'Prílohy strán sa neskopírujú.',
+    'copy_consider_access' => 'Zmena umiestnenia, vlastníka alebo povolení môže mať za následok to, že tento obsah bude prístupný tým, ktorí k nemu predtým prístup nemali.',
 
     // Conversions
-    'convert_to_shelf' => 'Convert to Shelf',
-    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
-    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
-    'convert_book' => 'Convert Book',
-    'convert_book_confirm' => 'Are you sure you want to convert this book?',
-    'convert_undo_warning' => 'This cannot be as easily undone.',
-    'convert_to_book' => 'Convert to Book',
-    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
-    'convert_chapter' => 'Convert Chapter',
-    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
+    'convert_to_shelf' => 'Konvertovať na Shelf',
+    'convert_to_shelf_contents_desc' => 'Túto knihu môžete previesť na novú policu s rovnakým obsahom. Kapitoly obsiahnuté v tejto knihe budú prevedené do nových kníh. Ak táto kniha obsahuje nejaké strany, ktoré nie sú v kapitole, táto kniha bude premenovaná a bude obsahovať tieto strany a táto kniha sa stane súčasťou novej police.',
+    'convert_to_shelf_permissions_desc' => 'Všetky povolenia nastavené pre túto knihu sa skopírujú do novej police a do všetkých nových podriadených kníh, ktoré nemajú vynútené vlastné povolenia. Všimnite si, že povolenia na policiach sa automaticky neprenášajú na obsah v rámci nich, ako je to v prípade kníh.',
+    'convert_book' => 'Previesť knihu',
+    'convert_book_confirm' => 'Ste si istý, že chcete previesť túto knihu?',
+    'convert_undo_warning' => 'To sa nedá tak ľahko vrátiť späť.',
+    'convert_to_book' => 'Previesť na knihu',
+    'convert_to_book_desc' => 'Túto kapitolu môžete previesť do novej knihy s rovnakým obsahom. Všetky povolenia nastavené v tejto kapitole sa skopírujú do novej knihy, ale žiadne zdedené povolenia z nadradenej knihy sa neskopírujú, čo by mohlo viesť k zmene riadenia prístupu.',
+    'convert_chapter' => 'Previesť kapitolu',
+    'convert_chapter_confirm' => 'Ste si istý, že chcete previesť túto kapitolu?',
 
     // References
-    'references' => 'References',
-    'references_none' => 'There are no tracked references to this item.',
-    'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+    'references' => 'Referencie',
+    'references_none' => 'Neexistujú žiadne sledované referencie na túto položku.',
+    'references_to_desc' => 'Nižšie sú zobrazené všetky známe stránky v systéme, ktoré odkazujú na túto položku.',
 ];
index 139633ca530ea2a43ffa8b76b71d414b99093287..716abfab77d191a786d0b5b3fd04303ec314abed 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Server nedokáže vytvoriť náhľady. Skontrolujte prosím, či máte nainštalované GD rozšírenie PHP.',
     'server_upload_limit' => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',
     'uploaded'  => 'Server nedovoľuje nahrávanie súborov s takouto veľkosťou. Skúste prosím menší súbor.',
-    'file_upload_timeout' => 'Nahrávanie súboru vypršalo.',
 
     // Drawing & Images
     'image_upload_error' => 'Pri nahrávaní obrázka nastala chyba',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Príloha nenájdená',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Koncept nemohol byť uložený. Uistite sa, že máte pripojenie k internetu pre uložením tejto stránky',
index 9f337815b9b566ea254af16ddbd00d72ac02ca59..c1ea6593ca80ed8f104639dc1ebbe34d7fa667de 100644 (file)
@@ -10,8 +10,8 @@ return [
     'settings' => 'Nastavenia',
     'settings_save' => 'Uložiť nastavenia',
     'settings_save_success' => 'Nastavenia uložené',
-    'system_version' => 'System Version',
-    'categories' => 'Categories',
+    'system_version' => 'Verzia systému',
+    'categories' => 'Kategórie',
 
     // App Settings
     'app_customization' => 'Prispôsobenia',
@@ -27,15 +27,15 @@ return [
     'app_secure_images' => 'Povoliť nahrávanie súborov so zvýšeným zabezpečením?',
     'app_secure_images_toggle' => 'Povoliť nahrávanie obrázkov s vyšším zabezpečením',
     'app_secure_images_desc' => 'Kvôli výkonu sú všetky obrázky verejné. Táto možnosť pridá pred URL obrázka náhodný, ťažko uhádnuteľný reťazec. Aby ste zabránili jednoduchému prístupu, uistite sa, že indexy priečinkov nie sú povolené.',
-    'app_default_editor' => 'Default Page Editor',
-    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
+    'app_default_editor' => 'Predvolený editor stránky',
+    'app_default_editor_desc' => 'Vyberte, ktorý editor sa bude používať ako predvolený pri úprave nových stránok. Je to možné prepísať na úrovni stránky, kde to umožňujú povolenia.',
     'app_custom_html' => 'Vlastný HTML obsah hlavičky',
     'app_custom_html_desc' => 'Všetok text pridaný sem bude vložený naspodok <head> sekcie na každej stránke. Môže sa to zísť pri zmene štýlu alebo pre pridanie analytického kódu.',
     'app_custom_html_disabled_notice' => 'Vlastný obsah hlavičky HTML je na tejto stránke s nastaveniami zakázaný, aby sa zabezpečilo, že sa dajú vrátiť zmeny, ktoré nastali.',
     'app_logo' => 'Logo aplikácie',
-    'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',
-    'app_icon' => 'Application Icon',
-    'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',
+    'app_logo_desc' => 'Používa sa to okrem iného v lište hlavičky aplikácie. Tento obrázok by mal mať výšku 86 pixelov. Veľké obrázky budú zmenšené.',
+    'app_icon' => 'Ikona aplikácie',
+    'app_icon_desc' => 'Táto ikona sa používa pre karty prehliadača a ikony odkazov. Mala by byť vo formáte štvorcového obrázku PNG s veľkosťou 256 pixelov.',
     'app_homepage' => 'Domovská stránka aplikácie',
     'app_homepage_desc' => 'Vyberte zobrazenie, ktoré sa má zobraziť na domovskej stránke namiesto predvoleného zobrazenia. Povolenia stránky sa pre vybraté stránky ignorujú.',
     'app_homepage_select' => 'Vybrať stránku',
@@ -49,12 +49,12 @@ return [
     'app_disable_comments_desc' => 'Zakázať komentáre na všetkých stránkach aplikácie. Existujúce komentáre sa nezobrazujú.',
 
     // Color settings
-    'color_scheme' => 'Application Color Scheme',
-    'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',
-    'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',
-    'app_color' => 'Primary Color',
-    'link_color' => 'Default Link Color',
-    'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
+    'color_scheme' => 'Farebná schéma aplikácie',
+    'color_scheme_desc' => 'Nastavte farby, ktoré sa majú použiť v používateľskom rozhraní aplikácie. Farby možno konfigurovať oddelene pre tmavý a svetlý režim, aby čo najlepšie vyhovovali téme a zabezpečili čitateľnosť.',
+    'ui_colors_desc' => 'Nastavte primárnu farbu aplikácie a predvolenú farbu odkazu. Primárna farba sa používa hlavne pre banner hlavičky, tlačidlá a dekorácie rozhrania. Predvolená farba odkazu sa používa pre textové odkazy a akcie v rámci písaného obsahu aj v rozhraní aplikácie.',
+    'app_color' => 'Hlavná farba',
+    'link_color' => 'Predvolená farba odkazu',
+    'content_colors_desc' => 'Nastaví farby pre všetky prvky v hierarchii organizácie stránky. Kvôli čitateľnosti sa odporúča vybrať farby s podobným jasom ako predvolené farby.',
     'bookshelf_color' => 'Farba police',
     'book_color' => 'Farba knihy',
     'chapter_color' => 'Farba kapitoly',
@@ -93,10 +93,10 @@ return [
     'maint_send_test_email_mail_text' => 'Gratulujeme! Keď ste dostali toto e-mailové upozornenie, zdá sa, že vaše nastavenia e-mailu sú nakonfigurované správne.',
     'maint_recycle_bin_desc' => 'Vymazané police, knihy, kapitoly a strany sa odošlú do koša, aby sa dali obnoviť alebo natrvalo odstrániť. Staršie položky z koša môžu byť po chvíli automaticky odstránené v závislosti od konfigurácie systému.',
     'maint_recycle_bin_open' => 'Otvoriť kôš',
-    'maint_regen_references' => 'Regenerate References',
-    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
-    'maint_regen_references_success' => 'Reference index has been regenerated!',
-    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+    'maint_regen_references' => 'Obnoviť referencie',
+    'maint_regen_references_desc' => 'Táto akcia znovu vytvorí referenčný index medzi položkami v databáze. Toto sa zvyčajne vykonáva automaticky, ale táto akcia môže byť užitočná na indexovanie starého obsahu alebo obsahu pridaného neoficiálnymi metódami.',
+    'maint_regen_references_success' => 'Referenčný index bol vygenerovaný!',
+    'maint_timeout_command_note' => 'Poznámka: Spustenie tejto akcie môže chvíľu trvať, čo môže v niektorých webových prostrediach viesť k problémom s časovým limitom. Alternatívne sa táto akcia vykoná pomocou príkazu v termináli.',
 
     // Recycle Bin
     'recycle_bin' => 'Kôš',
@@ -137,11 +137,11 @@ return [
     // Role Settings
     'roles' => 'Roly',
     'role_user_roles' => 'Používateľské roly',
-    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_index_desc' => 'Roly sa používajú na zoskupovanie používateľov a poskytovanie systémových povolení ich členom. Keď je používateľ členom viacerých rolí, udelené privilégiá sa nahromadia a používateľ zdedí všetky schopnosti.',
     'roles_x_users_assigned' => ':count user assigned|:count users assigned',
-    'roles_x_permissions_provided' => ':count permission|:count permissions',
-    'roles_assigned_users' => 'Assigned Users',
-    'roles_permissions_provided' => 'Provided Permissions',
+    'roles_x_permissions_provided' => ':počet povolení|:počet povolení',
+    'roles_assigned_users' => 'Priradení užívatelia',
+    'roles_permissions_provided' => 'Poskytnuté povolenia',
     'role_create' => 'Vytvoriť novú rolu',
     'role_delete' => 'Zmazať rolu',
     'role_delete_confirm' => 'Toto zmaže rolu menom \':roleName\'.',
@@ -163,12 +163,12 @@ return [
     'role_access_api' => 'API prístupového systému',
     'role_manage_settings' => 'Spravovať nastavenia aplikácie',
     'role_export_content' => 'Exportovať obsah',
-    'role_editor_change' => 'Change page editor',
+    'role_editor_change' => 'Zmeniť editor stránky',
     'role_asset' => 'Oprávnenia majetku',
     'roles_system_warning' => 'Uvedomte si, že prístup ku ktorémukoľvek z vyššie uvedených troch povolení môže používateľovi umožniť zmeniť svoje vlastné privilégiá alebo privilégiá ostatných v systéme. Roly s týmito povoleniami priraďujte iba dôveryhodným používateľom.',
     'role_asset_desc' => 'Tieto oprávnenia regulujú prednastavený prístup k zdroju v systéme. Oprávnenia pre knihy, kapitoly a stránky majú vyššiu prioritu.',
     'role_asset_admins' => 'Správcovia majú automaticky prístup ku všetkému obsahu, ale tieto možnosti môžu zobraziť alebo skryť možnosti používateľského rozhrania.',
-    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
+    'role_asset_image_view_note' => 'Toto sa týka viditeľnosti v rámci správcu obrázkov. Skutočný prístup k nahratým súborom obrázkov bude závisieť od možnosti ukladania obrázkov systému.',
     'role_all' => 'Všetko',
     'role_own' => 'Vlastné',
     'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané',
@@ -178,7 +178,7 @@ return [
 
     // Users
     'users' => 'Používatelia',
-    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
+    'users_index_desc' => 'Vytvárajte a spravujte individuálne používateľské účty v rámci systému. Používateľské účty sa používajú na prihlásenie a priradenie obsahu a aktivity. Prístupové povolenia sú primárne založené na rolách, ale vlastníctvo obsahu používateľa popri iných faktoroch môže mať vplyv aj na povolenia a prístup.',
     'user_profile' => 'Profil používateľa',
     'users_add_new' => 'Pridať nového používateľa',
     'users_search' => 'Hľadať medzi používateľmi',
@@ -189,7 +189,7 @@ return [
     'users_role' => 'Používateľské roly',
     'users_role_desc' => 'Vyberte, ku ktorým rolám bude tento používateľ priradený. Ak je používateľ priradený k viacerým rolám, povolenia z týchto rolí sa nahromadia a získajú všetky schopnosti priradených rolí.',
     'users_password' => 'Heslo používateľa',
-    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
+    'users_password_desc' => 'Nastavte heslo používané na prihlásenie do aplikácie. Musí mať aspoň 8 znakov.',
     'users_send_invite_text' => 'Môžete sa rozhodnúť poslať tomuto používateľovi e-mail s pozvánkou, ktorý mu umožní nastaviť si vlastné heslo, v opačnom prípade mu ho môžete nastaviť sami.',
     'users_send_invite_option' => 'Odoslať e-mail s pozvánkou pre používateľa',
     'users_external_auth_id' => 'Externé autentifikačné ID',
@@ -247,33 +247,33 @@ return [
     'user_api_token_delete_success' => 'Kľúč rozhrania API bol úspešne odstránený',
 
     // Webhooks
-    'webhooks' => 'Webhooks',
-    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks' => 'Webhooky',
+    'webhooks_index_desc' => 'Webhooky predstavujú spôsob odosielania údajov na externé adresy URL, keď sa v systéme vyskytnú určité akcie a udalosti, čo umožňuje integráciu založenú na udalostiach s externými platformami, ako sú systémy na odosielanie správ alebo notifikačné systémy.',
     'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',
-    '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_create' => 'Vytvoriť nový webhook',
+    'webhooks_none_created' => 'Žiadne webhooky zatiaľ neboli vytvorené.',
+    'webhooks_edit' => 'Upraviť Webhook',
+    'webhooks_save' => 'Uložiť Webhook',
+    'webhooks_details' => 'Detaily Webhooku',
+    'webhooks_details_desc' => 'Poskytnite užívateľsky prívetivý názov a koncový bod POST ako miesto, kam sa majú odosielať údaje webhooku.',
+    'webhooks_events' => 'Udalosti webhooku',
+    'webhooks_events_desc' => 'Vyberte všetky udalosti, ktoré by mali spustiť volanie tohto webhooku.',
+    'webhooks_events_warning' => 'Majte na pamäti, že tieto udalosti sa spustia pre všetky vybraté udalosti, aj keď sa používajú vlastné povolenia. Uistite sa, že používanie tohto webhooku neodhalí dôverný obsah.',
+    'webhooks_events_all' => 'Všetky systémové udalosti',
+    'webhooks_name' => 'Názov webhooku',
+    'webhooks_timeout' => 'Časový limit žiadosti webhooku (v sekundách)',
+    'webhooks_endpoint' => 'Koncový bod webhooku',
+    'webhooks_active' => 'Webhook je aktívny',
+    'webhook_events_table_header' => 'Udalosti',
+    'webhooks_delete' => 'Odstrániť webhook',
+    'webhooks_delete_warning' => 'Týmto sa tento webhook s názvom „:webhookName“ úplne odstráni zo systému.',
+    'webhooks_delete_confirm' => 'Naozaj chcete odstrániť tento webhook?',
+    'webhooks_format_example' => 'Príklad formátu webhooku',
+    'webhooks_format_example_desc' => 'Údaje webhooku sa odosielajú ako žiadosť POST do nakonfigurovaného koncového bodu ako JSON podľa nižšie uvedeného formátu. Vlastnosti „related_item“ a „url“ sú voliteľné a budú závisieť od typu spustenej udalosti.',
+    'webhooks_status' => 'Stav webhooku',
+    'webhooks_last_called' => 'Naposledy volané:',
+    'webhooks_last_errored' => 'Posledná chyba:',
+    'webhooks_last_error_message' => 'Posledná chybová správa:',
 
 
     //! If editing translations files directly please ignore this in all
index d1b454e6568b1531e3b34bdeb1c9d61769e5bb4d..8380f5d2cb030f8a523d345b9b0b582b3ec8502e 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Izberi slike',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Vse',
     'image_all_title' => 'Prikaži vse slike',
     'image_book_title' => 'Prikaži slike naložene v to knjigo',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Ste prepričani, da želite izbrisati to sliko?',
     'image_select_image' => 'Izberite sliko',
     'image_dropzone' => 'Povlecite slike ali kliknite tukaj za nalaganje',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Slike so bile izbrisane',
     'image_preview' => 'Predogled slike',
     'image_upload_success' => 'Slika uspešno naložena',
     'image_update_success' => 'Podatki slike uspešno posodobljeni',
     'image_delete_success' => 'Slika uspešno izbrisana',
-    'image_upload_remove' => 'Odstrani',
 
     // Code Editor
     'code_editor' => 'Uredi kodo',
index 087a13330b24c26321e1820e1328e705db357369..95060297fc0b4e5b48872bafb0d44768415da0b4 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Priponke',
     'attachments_explain' => 'Naložite nekaj datotek ali pripnite nekaj povezav, da jih prikažete na vaši strani. Vidne so v stranski orodni vrstici.',
     'attachments_explain_instant_save' => 'Spremembe tukaj so takoj shranjene.',
-    'attachments_items' => 'Priloženi elementi',
     'attachments_upload' => 'Naloži datoteko',
     'attachments_link' => 'Pripni povezavo',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Nastavi povezavo',
     'attachments_delete' => 'Ali ste prepričani, da želite izbrisati to priponko?',
-    'attachments_dropzone' => 'Spustite datoteke ali kliknite tukaj, če želite priložiti datoteko',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Nobena datoteka ni bila naložena',
     'attachments_explain_link' => 'Lahko pripnete povezavo, če ne želite naložiti datoteke. Lahko je povezava na drugo stran ali povezava do dateteke v oblaku.',
     'attachments_link_name' => 'Ime povezave',
index 9fd8c5b73e93e590e73de379e18b50dc32ddff93..1dea0a82a64c6a4c1b6bff2086e7a9d1d94d3c4d 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Strežnik ne more izdelati sličice. Prosimo preverite če imate GD PHP razširitev nameščeno.',
     'server_upload_limit' => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite z manjšo velikostjo datoteke.',
     'uploaded'  => 'Strežnik ne dovoli nalaganj take velikosti. Prosimo poskusite zmanjšati velikost datoteke.',
-    'file_upload_timeout' => 'Čas nalaganjanja datoteke je potekel.',
 
     // Drawing & Images
     'image_upload_error' => 'Prišlo je do napake med nalaganjem slike',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Priloga ni najdena',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Osnutka ni bilo mogoče shraniti. Pred shranjevanjem te strani se prepričajte, da imate internetno povezavo',
index 68b141945e1d30d130762981d1aa2789941d152a..0b679560a7e222044f2af55d3a583a0e9372cae4 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Val av bild',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Alla',
     'image_all_title' => 'Visa alla bilder',
     'image_book_title' => 'Visa bilder som laddats upp till den aktuella boken',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Är du säker på att du vill radera denna bild?',
     'image_select_image' => 'Välj bild',
     'image_dropzone' => 'Släpp bilder här eller klicka för att ladda upp',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Bilder borttagna',
     'image_preview' => 'Förhandsgranskning',
     'image_upload_success' => 'Bilden har laddats upp',
     'image_update_success' => 'Bildens uppgifter har ändrats',
     'image_delete_success' => 'Bilden har tagits bort',
-    'image_upload_remove' => 'Radera',
 
     // Code Editor
     'code_editor' => 'Redigera kod',
index 1aa3a56c378361ad8b1376588fe876c6b74183a3..0056c671ad849cf6e67d4faa22ed9fe6289c5221 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Bilagor',
     'attachments_explain' => 'Ladda upp filer eller bifoga länkar till ditt innehåll. Dessa visas i sidokolumnen.',
     'attachments_explain_instant_save' => 'Ändringar här sparas omgående.',
-    'attachments_items' => 'Bifogat innehåll',
     'attachments_upload' => 'Ladda upp fil',
     'attachments_link' => 'Bifoga länk',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Ange länk',
     'attachments_delete' => 'Är du säker på att du vill ta bort bilagan?',
-    'attachments_dropzone' => 'Släpp filer här eller klicka för att ladda upp',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Inga filer har laddats upp',
     'attachments_explain_link' => 'Du kan bifoga en länk om du inte vill ladda upp en fil. Detta kan vara en länk till en annan sida eller till en fil i molnet.',
     'attachments_link_name' => 'Länknamn',
index b9b6504daeea095d0734b048a654eda562a86d49..0465080be98135a06b05b98466f1cc0cfbafb93b 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Servern kan inte skapa miniatyrer. Kontrollera att du har PHPs GD-tillägg aktiverat.',
     'server_upload_limit' => 'Servern tillåter inte så här stora filer. Prova en mindre fil.',
     'uploaded'  => 'Servern tillåter inte så här stora filer. Prova en mindre fil.',
-    'file_upload_timeout' => 'Filuppladdningen har tagits ut.',
 
     // Drawing & Images
     'image_upload_error' => 'Ett fel inträffade vid uppladdningen',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Bilagan hittades ej',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Kunde inte spara utkastet. Kontrollera att du är ansluten till internet.',
index 009c48f23bb8f1bd495acc4ad6fcd73859056455..9c53a83476ce1fb9001d8514bc399b5c1a5a6d58 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Görsel Seç',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Hepsi',
     'image_all_title' => 'Bütün görselleri görüntüle',
     'image_book_title' => 'Bu kitaba ait görselleri görüntüle',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Bu resmi silmek istediğinizden emin misiniz?',
     'image_select_image' => 'Görsel Seç',
     'image_dropzone' => 'Görselleri sürükleyin ya da seçin',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Görseller Silindi',
     'image_preview' => 'Görsel Ön İzlemesi',
     'image_upload_success' => 'Görsel başarıyla yüklendi',
     'image_update_success' => 'Görsel detayları başarıyla güncellendi',
     'image_delete_success' => 'Görsel başarıyla silindi',
-    'image_upload_remove' => 'Kaldır',
 
     // Code Editor
     'code_editor' => 'Kodu Düzenle',
index 03c344610d9c9d5a2b9731724380317ee45e9e99..7e42c1065dd74dfa090b9f73df895bd70832ccf6 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Ekler',
     'attachments_explain' => 'Sayfanızda göstermek için dosyalar yükleyin veya bağlantılar ekleyin. Bunlar, sayfaya ait yan menüde gösterilecektir.',
     'attachments_explain_instant_save' => 'Burada yapılan değişiklikler anında kaydedilir.',
-    'attachments_items' => 'Eklenmiş Ögeler',
     'attachments_upload' => 'Dosya Yükle',
     'attachments_link' => 'Link Ekle',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Bağlantıyı Ata',
     'attachments_delete' => 'Bu eki silmek istediğinize emin misiniz?',
-    'attachments_dropzone' => 'Dosyaları sürükleyin veya seçin',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Hiçbir dosya yüklenmedi',
     'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz bağlantı ekleyebilirsiniz. Bu bağlantı başka bir sayfanın veya bulut depolamadaki bir dosyanın bağlantısı olabilir.',
     'attachments_link_name' => 'Bağlantı Adı',
index d4f9e53370ac62549b291a7cc3b79195cc0303c1..5216b2570532f110f84ffe94f0957e86954a6e8a 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Sunucu, görsel ön izlemelerini oluşturamadı. Lütfen GD PHP eklentisinin kurulu olduğundan emin olun.',
     'server_upload_limit' => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük bir dosya deneyin.',
     'uploaded'  => 'Sunucu bu boyutta dosya yüklemenize izin vermiyor. Lütfen daha küçük bir dosya deneyin.',
-    'file_upload_timeout' => 'Dosya yüklemesi zaman aşımına uğradı',
 
     // Drawing & Images
     'image_upload_error' => 'Görsel yüklenirken bir hata meydana geldi',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Ek bulunamadı',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Taslak kaydetme başarısız oldu. Bu sayfayı kaydetmeden önce internet bağlantınız olduğundan emin olun',
index a37d196eb522c7e2afa6af82a75feba12b637f1e..0a75ad23056ef629214b39019250d896874fded8 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Вибрати зображення',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Всі',
     'image_all_title' => 'Переглянути всі зображення',
     'image_book_title' => 'Переглянути зображення, завантажені в цю книгу',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Ви дійсно хочете видалити це зображення?',
     'image_select_image' => 'Вибрати зображення',
     'image_dropzone' => 'Перетягніть зображення, або натисніть тут для завантаження',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Зображень видалено',
     'image_preview' => 'Попередній перегляд зображення',
     'image_upload_success' => 'Зображення завантажено успішно',
     'image_update_success' => 'Деталі зображення успішно оновлені',
     'image_delete_success' => 'Зображення успішно видалено',
-    'image_upload_remove' => 'Видалити',
 
     // Code Editor
     'code_editor' => 'Редагувати код',
index 27e6fcfcd0a2547f71d7839415371999c9796ea8..d0e6afb668302fb7958bf8652dde7aad7e843a27 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Вкладення',
     'attachments_explain' => 'Завантажте файли, або додайте посилання, які відображатимуться на вашій сторінці. Їх буде видно на бічній панелі сторінки.',
     'attachments_explain_instant_save' => 'Зміни тут зберігаються миттєво.',
-    'attachments_items' => 'Додані елементи',
     'attachments_upload' => 'Завантажити файл',
     'attachments_link' => 'Приєднати посилання',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Встановити посилання',
     'attachments_delete' => 'Дійсно хочете видалити це вкладення?',
-    'attachments_dropzone' => 'Перетягніть файли, або натисніть тут щоб прикріпити файл',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Файли не завантажені',
     'attachments_explain_link' => 'Ви можете приєднати посилання, якщо не бажаєте завантажувати файл. Це може бути посилання на іншу сторінку або посилання на файл у хмарі.',
     'attachments_link_name' => 'Назва посилання',
index 2b2cf1100c2b3e99d85310d7dc1d69a5fe53f5d0..3c01a35a80e253e40fa3cc1ad1ba39ab29611d2a 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Сервер не може створювати ескізи. Будь ласка, перевірте, чи встановлено розширення GD PHP.',
     'server_upload_limit' => 'Сервер не дозволяє завантажувати файли такого розміру. Спробуйте менший розмір файлу.',
     'uploaded'  => 'Сервер не дозволяє завантажувати файли такого розміру. Спробуйте менший розмір файлу.',
-    'file_upload_timeout' => 'Тайм-аут при завантаженні файлу',
 
     // Drawing & Images
     'image_upload_error' => 'Виникла помилка під час завантаження зображення',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Вкладення не знайдено',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Не вдалося зберегти чернетку. Перед збереженням цієї сторінки переконайтеся, що у вас є зв\'язок з сервером.',
index c6ae940d89c37272aac030baf7e8480482d86b8c..3cad1c7ed9b292483a223b9a4a38f5fb46533271 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Rasmni tanlash',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Barchasi',
     'image_all_title' => 'Barcha rasmlarni ko\'rish',
     'image_book_title' => 'Ushbu kitobga yuklangan barcha rasmlarni ko\'rish',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
     'image_select_image' => 'Rasmni tanlash',
     'image_dropzone' => 'Drop images or click here to upload',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Images Deleted',
     'image_preview' => 'Image Preview',
     'image_upload_success' => 'Image uploaded successfully',
     'image_update_success' => 'Image details successfully updated',
     'image_delete_success' => 'Image successfully deleted',
-    'image_upload_remove' => 'Olib tashlash',
 
     // Code Editor
     'code_editor' => 'Kodni tahrirlash',
index 30a415b8614893005f86155f8a02d7b52802083d..2a765e72a8af5331f24250fb170ad5e8445f7b5d 100644 (file)
@@ -311,12 +311,12 @@ return [
     '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_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     '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_dropzone' => 'Drop files here to upload',
     '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',
index 703d0edbef0635a4963c2eb7e294f0d7c65bf0b1..6991f96e4359facd80e789406e28054c8432b2b0 100644 (file)
@@ -45,7 +45,6 @@ return [
     '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.',
-    'file_upload_timeout' => 'The file upload has timed out.',
 
     // Drawing & Images
     'image_upload_error' => 'An error occurred uploading the image',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Attachment not found',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
index 8a061625cd6128ad48e34c76439b368b4c4b2de5..4af522517d2d8f20d5234c26c1784ab348cc8b04 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => 'Chọn Ảnh',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => 'Tất cả',
     'image_all_title' => 'Xem tất cả các ảnh',
     'image_book_title' => 'Xem các ảnh đã được tải lên sách này',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => 'Bạn có chắc chắn muốn xóa hình ảnh này?',
     'image_select_image' => 'Chọn Ảnh',
     'image_dropzone' => 'Thả các ảnh hoặc bấm vào đây để tải lên',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => 'Các ảnh đã được xóa',
     'image_preview' => 'Xem trước Ảnh',
     'image_upload_success' => 'Ảnh đã tải lên thành công',
     'image_update_success' => 'Chi tiết ảnh được cập nhật thành công',
     'image_delete_success' => 'Ảnh đã được xóa thành công',
-    'image_upload_remove' => 'Xóa bỏ',
 
     // Code Editor
     'code_editor' => 'Sửa Mã',
index bb401b1d3e6030cc6fb00c85a1fe9bbb866f4b58..063dcb1f8040584ff0bbe88571f8a652da515ac6 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => 'Các Đính kèm',
     'attachments_explain' => 'Cập nhật một số tập tin và đính một số liên kết để hiển thị trên trang của bạn. Chúng được hiện trong sidebar của trang.',
     'attachments_explain_instant_save' => 'Các thay đổi ở đây sẽ được lưu ngay lập tức.',
-    'attachments_items' => 'Đính kèm các Mục',
     'attachments_upload' => 'Tải lên Tập tin',
     'attachments_link' => 'Đính kèm Liên kết',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Đặt Liên kết',
     'attachments_delete' => 'Bạn có chắc chắn muốn xóa tập tin đính kèm này?',
-    'attachments_dropzone' => 'Thả các tập tin hoặc bấm vào đây để đính kèm một tập tin',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => 'Không có tập tin nào được tải lên',
     'attachments_explain_link' => 'Bạn có thể đính kèm một liên kết nếu bạn lựa chọn không tải lên tập tin. Liên kết này có thể trỏ đến một trang khác hoặc một tập tin ở trên mạng (đám mây).',
     'attachments_link_name' => 'Tên Liên kết',
index cee3b618fe82f8a6a229a77bc74b28b1636524b0..89fa34e97db51d43f0a164866532e87b97ca7268 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => 'Máy chủ không thể tạo ảnh nhỏ. Vui lòng kiểm tra bạn đã cài đặt tiện ích mở rộng GD PHP.',
     'server_upload_limit' => 'Máy chủ không cho phép tải lên kích thước này. Vui lòng thử lại với tệp tin nhỏ hơn.',
     'uploaded'  => 'Máy chủ không cho phép tải lên kích thước này. Vui lòng thử lại với tệp tin nhỏ hơn.',
-    'file_upload_timeout' => 'Đã quá thời gian tải lên tệp tin.',
 
     // Drawing & Images
     'image_upload_error' => 'Đã xảy ra lỗi khi đang tải lên ảnh',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => 'Không tìm thấy đính kèm',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => 'Lưu bản nháp thất bại. Đảm bảo rằng bạn có kết nối đến internet trước khi lưu trang này',
index ab0b7cb469ec5a25179e1b26902cf47fd2401aa2..a8a9d26c8ad2420557fdd553d502dbe8210f5ab6 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => '选择图片',
+    'image_upload' => '上传图片',
+    'image_intro' => '您可以在此选择和管理以前上传到系统的图片。',
+    'image_intro_upload' => '上传一张新图片,通过拖放图片到这个窗口,或者使用上面的“上传图片”按钮',
     'image_all' => '全部',
     'image_all_title' => '查看所有图片',
     'image_book_title' => '查看上传到本书的图片',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => '您确认要删除此图片吗?',
     'image_select_image' => '选择图片',
     'image_dropzone' => '拖放图片或点击此处上传',
+    'image_dropzone_drop' => '将图片拖放到此处上传',
     'images_deleted' => '图片已删除',
     'image_preview' => '图片预览',
     'image_upload_success' => '图片上传成功',
     'image_update_success' => '图片详细信息更新成功',
     'image_delete_success' => '图片删除成功',
-    'image_upload_remove' => '去掉',
 
     // Code Editor
     'code_editor' => '编辑代码',
index 6d6faea7680b699ee626427117ba33e7796868b9..66c4a21588227dbe8f6d059df322c60dc21b5a40 100644 (file)
@@ -311,12 +311,12 @@ return [
     'attachments' => '附件',
     'attachments_explain' => '上传一些文件或附加一些链接显示在您的网页上。这些在页面的侧边栏中可见。',
     'attachments_explain_instant_save' => '这里的更改将立即保存。',
-    'attachments_items' => '附加项目',
     'attachments_upload' => '上传文件',
     'attachments_link' => '附加链接',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => '设置链接',
     'attachments_delete' => '您确定要删除此附件吗?',
-    'attachments_dropzone' => '删除文件或点击此处添加文件',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => '尚未上传文件',
     'attachments_explain_link' => '如果您不想上传文件,则可以附加链接,这可以是指向其他页面的链接,也可以是指向云端文件的链接。',
     'attachments_link_name' => '链接名',
index 4145d8e38fcfd52852db842c66bc0915bd38d67a..7ca1a9a087ff5f914a32ba91288ae675cfe6809f 100644 (file)
@@ -45,7 +45,6 @@ return [
     'cannot_create_thumbs' => '服务器无法创建缩略图,请检查您是否安装了GD PHP扩展。',
     'server_upload_limit' => '服务器不允许上传此大小的文件。 请尝试较小的文件。',
     'uploaded'  => '服务器不允许上传此大小的文件。 请尝试较小的文件。',
-    'file_upload_timeout' => '文件上传已超时。',
 
     // Drawing & Images
     'image_upload_error' => '上传图片时发生错误',
@@ -54,6 +53,7 @@ return [
 
     // Attachments
     'attachment_not_found' => '找不到附件',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => '无法保存草稿,确保您在保存页面之前已经连接到互联网',
index 1a04caa2f71c5a11714310757c421c70ae7bd459..2605107aeb56a8235c59b8d4eea8fd2750d5ff43 100644 (file)
@@ -68,9 +68,9 @@ return [
     'user_delete_notification' => '使用者移除成功',
 
     // Roles
-    'role_create_notification' => 'Role successfully created',
-    'role_update_notification' => 'Role successfully updated',
-    'role_delete_notification' => 'Role successfully deleted',
+    'role_create_notification' => '建立角色成功',
+    'role_update_notification' => '更新角色成功',
+    'role_delete_notification' => '刪除角色成功',
 
     // Other
     'commented_on'                => '評論',
index 62f515f5ef8e1b6f274e9beea77da030289bc658..1b9c496850515e6fb34a179bc77aa6c0233542b7 100644 (file)
@@ -6,6 +6,9 @@ return [
 
     // Image Manager
     'image_select' => '選取圖片',
+    'image_upload' => 'Upload Image',
+    'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
+    'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
     'image_all' => '全部',
     'image_all_title' => '檢視所有圖片',
     'image_book_title' => '檢視上傳到此書本的圖片',
@@ -18,12 +21,12 @@ return [
     'image_delete_confirm_text' => '您確認想要刪除這個圖片?',
     'image_select_image' => '選取圖片',
     'image_dropzone' => '拖曳圖片或點擊此處上傳',
+    'image_dropzone_drop' => 'Drop images here to upload',
     'images_deleted' => '圖片已刪除',
     'image_preview' => '圖片預覽',
     'image_upload_success' => '圖片上傳成功',
     'image_update_success' => '圖片詳細資訊更新成功',
     'image_delete_success' => '圖片刪除成功',
-    'image_upload_remove' => '移除',
 
     // Code Editor
     'code_editor' => '編輯程式碼',
index 903b49a0be586105021b9ad7d31ceca8aaf4bd9d..4189e39dd0d470c578cd0bda2d7dab16e8e58552 100644 (file)
@@ -8,7 +8,7 @@
 return [
     // General editor terms
     'general' => '通用',
-    'advanced' => 'Advanced',
+    'advanced' => '進階設定',
     'none' => '無',
     'cancel' => '取消',
     'save' => '保存',
@@ -31,7 +31,7 @@ return [
     'header_large' => '大標題',
     'header_medium' => '中標題',
     'header_small' => '小標題',
-    'header_tiny' => 'Tiny Header',
+    'header_tiny' => '小標題',
     'paragraph' => '段落',
     'blockquote' => '引用塊',
     'inline_code' => '行內程式碼',
@@ -48,48 +48,48 @@ return [
     'subscript' => '下標',
     'text_color' => '文本顏色',
     'custom_color' => '自訂顏色',
-    'remove_color' => 'Remove color',
-    'background_color' => 'Background color',
-    'align_left' => 'Align left',
-    'align_center' => 'Align center',
-    'align_right' => 'Align right',
-    'align_justify' => 'Justify',
-    'list_bullet' => 'Bullet list',
-    'list_numbered' => 'Numbered list',
+    'remove_color' => '移除颜色',
+    'background_color' => '背景顏色',
+    'align_left' => '置左',
+    'align_center' => '置中',
+    'align_right' => '置右',
+    'align_justify' => '兩端對齊',
+    'list_bullet' => '項目列表',
+    'list_numbered' => '編號列表',
     'list_task' => '任務清單',
     'indent_increase' => '增加縮進',
     'indent_decrease' => '減少縮進',
-    'table' => 'Table',
-    'insert_image' => 'Insert image',
-    'insert_image_title' => 'Insert/Edit Image',
-    'insert_link' => 'Insert/edit link',
-    'insert_link_title' => 'Insert/Edit Link',
-    'insert_horizontal_line' => 'Insert horizontal line',
-    'insert_code_block' => 'Insert code block',
-    'edit_code_block' => 'Edit code block',
-    'insert_drawing' => 'Insert/edit drawing',
-    'drawing_manager' => 'Drawing manager',
-    'insert_media' => 'Insert/edit media',
-    'insert_media_title' => 'Insert/Edit Media',
-    'clear_formatting' => 'Clear formatting',
-    'source_code' => 'Source code',
-    'source_code_title' => 'Source Code',
-    'fullscreen' => 'Fullscreen',
-    'image_options' => 'Image options',
+    'table' => '表格',
+    'insert_image' => '插入圖片',
+    'insert_image_title' => '插入/編輯圖像',
+    'insert_link' => '插入/編輯連結',
+    'insert_link_title' => '插入/編輯連結',
+    'insert_horizontal_line' => '插入水平線',
+    'insert_code_block' => '插入程式碼區塊',
+    'edit_code_block' => '編輯程式碼區塊',
+    'insert_drawing' => '插入/編輯向量圖',
+    'drawing_manager' => '向量圖管理員',
+    'insert_media' => '插入/編輯影片',
+    'insert_media_title' => '插入/編輯影片',
+    'clear_formatting' => '清除格式',
+    'source_code' => '原始碼',
+    'source_code_title' => '原始碼',
+    'fullscreen' => '全螢幕',
+    'image_options' => '圖片選項',
 
     // Tables
-    'table_properties' => 'Table properties',
-    'table_properties_title' => 'Table Properties',
-    'delete_table' => 'Delete table',
-    'insert_row_before' => 'Insert row before',
-    'insert_row_after' => 'Insert row after',
-    'delete_row' => 'Delete row',
-    'insert_column_before' => 'Insert column before',
-    'insert_column_after' => 'Insert column after',
-    'delete_column' => 'Delete column',
-    'table_cell' => 'Cell',
-    'table_row' => 'Row',
-    'table_column' => 'Column',
+    'table_properties' => '表格屬性',
+    'table_properties_title' => '表格屬性',
+    'delete_table' => '刪除表格',
+    'insert_row_before' => '插入上方列',
+    'insert_row_after' => '插入下方列',
+    'delete_row' => '刪除列',
+    'insert_column_before' => '插入欄位於左方',
+    'insert_column_after' => '插入欄位於右方',
+    'delete_column' => '刪除欄',
+    'table_cell' => '儲存格',
+    'table_row' => '',
+    'table_column' => '',
     'cell_properties' => 'Cell properties',
     'cell_properties_title' => 'Cell Properties',
     'cell_type' => 'Cell type',
index 00100cbf6fbda53915b85421d2bcde6c43b02abe..faeccda61e38de96d1b361809276ad4454b662fc 100644 (file)
@@ -222,7 +222,7 @@ return [
     'pages_edit_set_changelog' => '設定變更日誌',
     'pages_edit_enter_changelog_desc' => '輸入對您所做變動的簡易描述',
     'pages_edit_enter_changelog' => '輸入變更日誌',
-    'pages_editor_switch_title' => 'Switch Editor',
+    'pages_editor_switch_title' => '切換編輯器',
     'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
     'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
     'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
@@ -236,7 +236,7 @@ return [
     'pages_md_insert_image' => '插入圖片',
     'pages_md_insert_link' => '插入連結',
     'pages_md_insert_drawing' => '插入繪圖',
-    'pages_md_show_preview' => 'Show preview',
+    'pages_md_show_preview' => '顯示預覽',
     'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => '頁面不在章節中',
     'pages_move' => '移動頁面',
@@ -311,12 +311,12 @@ return [
     'attachments' => '附件',
     'attachments_explain' => '上傳一些檔案或附加連結以顯示在您的網頁上。將顯示在在頁面的側邊欄。',
     'attachments_explain_instant_save' => '此處的變動將會立刻儲存。',
-    'attachments_items' => '附件',
     'attachments_upload' => '上傳檔案',
     'attachments_link' => '附加連結',
+    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => '設定連結',
     'attachments_delete' => '您確定要刪除此附件嗎?',
-    'attachments_dropzone' => '拖曳檔案或點擊此處來附加檔案',
+    'attachments_dropzone' => 'Drop files here to upload',
     'attachments_no_files' => '尚未上傳檔案',
     'attachments_explain_link' => '如果您不想上傳檔案,則可以附加連結。這可以是指向其他頁面的連結,也可以是指向雲端檔案的連結。',
     'attachments_link_name' => '連結名稱',
index eb986e6069d66c1c089deb3f438e28393f711287..16534ff9115ddb43665213ca83ee5f327166c72e 100644 (file)
@@ -45,15 +45,15 @@ return [
     'cannot_create_thumbs' => '伺服器無法建立縮圖。請檢查您是否安裝了 PHP 的 GD 擴充程式。',
     'server_upload_limit' => '伺服器不允許上傳這個大的檔案。請嘗試較小的檔案。',
     'uploaded'  => '伺服器不允許上傳這個大的檔案。請嘗試較小的檔案。',
-    'file_upload_timeout' => '檔案上傳逾時。',
 
     // Drawing & Images
     'image_upload_error' => '上傳圖片時發生錯誤',
     'image_upload_type_error' => '上傳圖片類型無效',
-    'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.',
+    'drawing_data_not_found' => '無法載入繪圖資料,繪圖檔案可能不存在,或您可能沒有權限存取它。',
 
     // Attachments
     'attachment_not_found' => '找不到附件',
+    'attachment_upload_error' => 'An error occurred uploading the attachment file',
 
     // Pages
     'page_draft_autosave_fail' => '無法儲存草稿。請確保您在儲存此頁面前已連線至網際網路',
index 734e26cf5218936203987e83b743da1fef4aa246..86e6c6f16e3c1c83de51ad1362292b48357aa36e 100644 (file)
@@ -34,7 +34,7 @@ return [
     'app_custom_html_disabled_notice' => '在此設定頁面上停用了自訂 HTML 標題內容,以確保任何重大變更都能被還原。',
     'app_logo' => '應用程式圖示',
     'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.',
-    'app_icon' => 'Application Icon',
+    'app_icon' => '應用程式圖示',
     'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',
     'app_homepage' => '應用程式首頁',
     'app_homepage_desc' => '選取要作為首頁的頁面,這將會取代預設首頁。選定頁面的頁面權限將會被忽略。',
@@ -52,8 +52,8 @@ return [
     'color_scheme' => 'Application Color Scheme',
     'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.',
     'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.',
-    'app_color' => 'Primary Color',
-    'link_color' => 'Default Link Color',
+    'app_color' => '主要顏色',
+    'link_color' => '連結預設顏色',
     'content_colors_desc' => 'Set 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' => '書架顔色',
     'book_color' => '書本顔色',
index eff4bea7d66230f2898afe53e85e739a6d3fc325..50987602f1ec13c14d9378d7c12fa849ccb0bfcf 100644 (file)
 {
   "name": "bookstack",
-  "lockfileVersion": 2,
+  "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "dependencies": {
-        "clipboard": "^2.0.11",
-        "codemirror": "^5.65.5",
-        "dropzone": "^5.9.3",
+        "@codemirror/commands": "^6.2.2",
+        "@codemirror/lang-css": "^6.1.1",
+        "@codemirror/lang-html": "^6.4.3",
+        "@codemirror/lang-javascript": "^6.1.6",
+        "@codemirror/lang-json": "^6.0.1",
+        "@codemirror/lang-markdown": "^6.1.1",
+        "@codemirror/lang-php": "^6.0.1",
+        "@codemirror/lang-xml": "^6.0.2",
+        "@codemirror/language": "^6.6.0",
+        "@codemirror/legacy-modes": "^6.3.2",
+        "@codemirror/state": "^6.2.0",
+        "@codemirror/theme-one-dark": "^6.1.1",
+        "@codemirror/view": "^6.9.4",
+        "@lezer/highlight": "^1.1.4",
+        "@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
+        "@ssddanbrown/codemirror-lang-twig": "^1.0.0",
+        "codemirror": "^6.0.1",
         "markdown-it": "^13.0.1",
         "markdown-it-task-lists": "^2.1.1",
         "snabbdom": "^3.5.1",
         "sortablejs": "^1.15.0"
       },
       "devDependencies": {
+        "@lezer/generator": "^1.2.2",
         "chokidar-cli": "^3.0",
-        "esbuild": "^0.17.3",
+        "esbuild": "^0.17.16",
+        "eslint": "^8.38.0",
+        "eslint-config-airbnb-base": "^15.0.0",
+        "eslint-plugin-import": "^2.27.5",
         "livereload": "^0.9.3",
         "npm-run-all": "^4.1.5",
         "punycode": "^2.3.0",
-        "sass": "^1.57.0"
+        "sass": "^1.62.0"
+      }
+    },
+    "node_modules/@codemirror/autocomplete": {
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.5.1.tgz",
+      "integrity": "sha512-/Sv9yJmqyILbZ26U4LBHnAtbikuVxWUp+rQ8BXuRGtxZfbfKOY/WPbsUtvSP2h0ZUZMlkxV/hqbKRFzowlA6xw==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.6.0",
+        "@lezer/common": "^1.0.0"
+      },
+      "peerDependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/commands": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.2.tgz",
+      "integrity": "sha512-s9lPVW7TxXrI/7voZ+HmD/yiAlwAYn9PH5SUVSUhsxXHhv4yl5eZ3KLntSoTynfdgVYM0oIpccQEWRBQgmNZyw==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.2.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-css": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.1.1.tgz",
+      "integrity": "sha512-P6jdNEHyRcqqDgbvHYyC9Wxkek0rnG3a9aVSRi4a7WrjPbQtBTaOmvYpXmm13zZMAatO4Oqpac+0QZs7sy+LnQ==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@lezer/css": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-html": {
+      "version": "6.4.3",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.3.tgz",
+      "integrity": "sha512-VKzQXEC8nL69Jg2hvAFPBwOdZNvL8tMFOrdFwWpU+wc6a6KEkndJ/19R5xSaglNX6v2bttm8uIEFYxdQDcIZVQ==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/lang-css": "^6.0.0",
+        "@codemirror/lang-javascript": "^6.0.0",
+        "@codemirror/language": "^6.4.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.2.2",
+        "@lezer/common": "^1.0.0",
+        "@lezer/css": "^1.1.0",
+        "@lezer/html": "^1.3.0"
+      }
+    },
+    "node_modules/@codemirror/lang-javascript": {
+      "version": "6.1.6",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.6.tgz",
+      "integrity": "sha512-TTK28z+vJQY9GAefLTDDptI2LMqMfAiuTpt8s9SsNwocjVQ1v9yTzfReMf1hYhspQCdhfa7fdKnQJ78mKe/bHQ==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/language": "^6.6.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/javascript": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-json": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
+      "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@lezer/json": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-markdown": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.1.1.tgz",
+      "integrity": "sha512-n87Ms6Y5UYb1UkFu8sRzTLfq/yyF1y2AYiWvaVdbBQi5WDj1tFk5N+AKA+WC0Jcjc1VxvrCCM0iizjdYYi9sFQ==",
+      "dependencies": {
+        "@codemirror/lang-html": "^6.0.0",
+        "@codemirror/language": "^6.3.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/markdown": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-php": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
+      "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
+      "dependencies": {
+        "@codemirror/lang-html": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/php": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-xml": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.0.2.tgz",
+      "integrity": "sha512-JQYZjHL2LAfpiZI2/qZ/qzDuSqmGKMwyApYmEUUCTxLM4MWS7sATUEfIguZQr9Zjx/7gcdnewb039smF6nC2zw==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/language": "^6.4.0",
+        "@codemirror/state": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/xml": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/language": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
+      "integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0",
+        "style-mod": "^4.0.0"
+      }
+    },
+    "node_modules/@codemirror/legacy-modes": {
+      "version": "6.3.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.2.tgz",
+      "integrity": "sha512-ki5sqNKWzKi5AKvpVE6Cna4Q+SgxYuYVLAZFSsMjGBWx5qSVa+D+xipix65GS3f2syTfAD9pXKMX4i4p49eneQ==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0"
+      }
+    },
+    "node_modules/@codemirror/lint": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.2.1.tgz",
+      "integrity": "sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "node_modules/@codemirror/search": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.3.0.tgz",
+      "integrity": "sha512-rBhZxzT34CarfhgCZGhaLBScABDN3iqJxixzNuINp9lrb3lzm0nTpR77G1VrxGO3HOGK7j62jcJftQM7eCOIuw==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "node_modules/@codemirror/state": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
+      "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA=="
+    },
+    "node_modules/@codemirror/theme-one-dark": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.1.tgz",
+      "integrity": "sha512-+CfzmScfJuD6uDF5bHJkAjWTQ2QAAHxODCPxUEgcImDYcJLT+4l5vLnBHmDVv46kCC5uUJGMrBJct2Z6JbvqyQ==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/highlight": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/view": {
+      "version": "6.9.4",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.4.tgz",
+      "integrity": "sha512-Ov2H9gwlGUxiH94zWxlLtTlyogSFaQDIYjtSEcfzgh7MkKmKVchkmr4JbtR5zBev3jY5DVtKvUC8yjd1bKW55A==",
+      "dependencies": {
+        "@codemirror/state": "^6.1.4",
+        "style-mod": "^4.0.0",
+        "w3c-keyname": "^2.2.4"
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz",
-      "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.17.tgz",
+      "integrity": "sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz",
-      "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz",
+      "integrity": "sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz",
-      "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.17.tgz",
+      "integrity": "sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz",
-      "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz",
+      "integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz",
-      "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz",
+      "integrity": "sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz",
-      "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz",
+      "integrity": "sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz",
-      "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz",
+      "integrity": "sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz",
-      "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz",
+      "integrity": "sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz",
-      "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz",
+      "integrity": "sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz",
-      "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz",
+      "integrity": "sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz",
-      "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz",
+      "integrity": "sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==",
       "cpu": [
         "loong64"
       ],
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz",
-      "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz",
+      "integrity": "sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==",
       "cpu": [
         "mips64el"
       ],
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz",
-      "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz",
+      "integrity": "sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==",
       "cpu": [
         "ppc64"
       ],
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz",
-      "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz",
+      "integrity": "sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==",
       "cpu": [
         "riscv64"
       ],
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz",
-      "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz",
+      "integrity": "sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==",
       "cpu": [
         "s390x"
       ],
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz",
-      "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz",
+      "integrity": "sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz",
-      "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz",
+      "integrity": "sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz",
-      "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz",
+      "integrity": "sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz",
-      "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz",
+      "integrity": "sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz",
-      "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz",
+      "integrity": "sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz",
-      "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz",
+      "integrity": "sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz",
-      "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz",
+      "integrity": "sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==",
       "cpu": [
         "x64"
       ],
         "node": ">=12"
       }
     },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "dependencies": {
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
+      "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+      "dev": true,
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
+      "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+      "dev": true,
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.5.1",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
+      "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
+      "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.11.8",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+      "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+      "dev": true,
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^1.2.1",
+        "debug": "^4.1.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+      "dev": true
+    },
+    "node_modules/@lezer/common": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz",
+      "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng=="
+    },
+    "node_modules/@lezer/css": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.1.tgz",
+      "integrity": "sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==",
+      "dependencies": {
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/generator": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.2.2.tgz",
+      "integrity": "sha512-O//eH9jTPM1GnbZruuD23xU68Pkuragonn1DEIom4Kt/eJN/QFt7Vzvp1YjV/XBmoUKC+2ySPgrA5fMF9FMM2g==",
+      "dev": true,
+      "dependencies": {
+        "@lezer/common": "^1.0.2",
+        "@lezer/lr": "^1.3.0"
+      },
+      "bin": {
+        "lezer-generator": "dist/lezer-generator.cjs"
+      }
+    },
+    "node_modules/@lezer/highlight": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.4.tgz",
+      "integrity": "sha512-IECkFmw2l7sFcYXrV8iT9GeY4W0fU4CxX0WMwhmhMIVjoDdD1Hr6q3G2NqVtLg/yVe5n7i4menG3tJ2r4eCrPQ==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/html": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.4.tgz",
+      "integrity": "sha512-HdJYMVZcT4YsMo7lW3ipL4NoyS2T67kMPuSVS5TgLGqmaCjEU/D6xv7zsa1ktvTK5lwk7zzF1e3eU6gBZIPm5g==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/javascript": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.2.tgz",
+      "integrity": "sha512-77qdAD4zanmImPiAu4ibrMUzRc79UHoccdPa+Ey5iwS891TAkhnMAodUe17T7zV7tnF7e9HXM0pfmjoGEhrppg==",
+      "dependencies": {
+        "@lezer/highlight": "^1.1.3",
+        "@lezer/lr": "^1.3.0"
+      }
+    },
+    "node_modules/@lezer/json": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz",
+      "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==",
+      "dependencies": {
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/lr": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.3.tgz",
+      "integrity": "sha512-JPQe3mwJlzEVqy67iQiiGozhcngbO8QBgpqZM6oL1Wj/dXckrEexpBLeFkq0edtW5IqnPRFxA24BHJni8Js69w==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/markdown": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.0.2.tgz",
+      "integrity": "sha512-8CY0OoZ6V5EzPjSPeJ4KLVbtXdLBd8V6sRCooN5kHnO28ytreEGTyrtU/zUwo/XLRzGr/e1g44KlzKi3yWGB5A==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0",
+        "@lezer/highlight": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/php": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.1.tgz",
+      "integrity": "sha512-aqdCQJOXJ66De22vzdwnuC502hIaG9EnPK2rSi+ebXyUd+j7GAX1mRjWZOVOmf3GST1YUfUCu6WXDiEgDGOVwA==",
+      "dependencies": {
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.1.0"
+      }
+    },
+    "node_modules/@lezer/xml": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.1.tgz",
+      "integrity": "sha512-jMDXrV953sDAUEMI25VNrI9dz94Ai96FfeglytFINhhwQ867HKlCE2jt3AwZTCT7M528WxdDWv/Ty8e9wizwmQ==",
+      "dependencies": {
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@ssddanbrown/codemirror-lang-smarty": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@ssddanbrown/codemirror-lang-smarty/-/codemirror-lang-smarty-1.0.0.tgz",
+      "integrity": "sha512-F0ut1kmdbT3eORk3xVIKfQsGCZiQdh+6sLayBa0+FTex2gyIQlVQZRRA7bPSlchI3uZtWwNnqGNz5O/QLWRlFg=="
+    },
+    "node_modules/@ssddanbrown/codemirror-lang-twig": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@ssddanbrown/codemirror-lang-twig/-/codemirror-lang-twig-1.0.0.tgz",
+      "integrity": "sha512-7WIMIh8Ssc54TooGCY57WU2rKEqZZrcV2tZSVRPtd0gKYsrDEKCSLWpQjUWEx7bdgh3NKHUjq1O4ugIzI/+dwQ==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+      "dev": true
+    },
+    "node_modules/acorn": {
+      "version": "8.8.2",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "dev": true,
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
     "node_modules/ansi-regex": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
       }
     },
     "node_modules/anymatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
-      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
       "dev": true,
       "dependencies": {
         "normalize-path": "^3.0.0",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
     },
+    "node_modules/array-buffer-byte-length": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+      "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "is-array-buffer": "^3.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array-includes": {
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+      "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4",
+        "get-intrinsic": "^1.1.3",
+        "is-string": "^1.0.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flat": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+      "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/array.prototype.flatmap": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+      "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4",
+        "es-shim-unscopables": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/available-typed-arrays": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/camelcase": {
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
         "node": ">= 8.10.0"
       }
     },
-    "node_modules/clipboard": {
-      "version": "2.0.11",
-      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
-      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
-      "dependencies": {
-        "good-listener": "^1.2.2",
-        "select": "^1.1.2",
-        "tiny-emitter": "^2.0.0"
-      }
-    },
     "node_modules/cliui": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
       }
     },
     "node_modules/codemirror": {
-      "version": "5.65.9",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
-      "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/commands": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/search": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0"
+      }
     },
     "node_modules/color-convert": {
       "version": "1.9.3",
       "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "dev": true
     },
+    "node_modules/confusing-browser-globals": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
+      "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
+      "dev": true
+    },
+    "node_modules/crelt": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
+      "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
+    },
     "node_modules/cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
         "node": ">=4.8"
       }
     },
-    "node_modules/decamelize": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+    "node_modules/debug": {
+      "version": "4.3.4",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
       "dev": true,
+      "dependencies": {
+        "ms": "2.1.2"
+      },
       "engines": {
-        "node": ">=0.10.0"
-      }
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true
     },
     "node_modules/define-properties": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
-      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+      "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
       "dev": true,
       "dependencies": {
         "has-property-descriptors": "^1.0.0",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/delegate": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
-      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
-    },
-    "node_modules/dropzone": {
-      "version": "5.9.3",
-      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz",
-      "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA=="
+    "node_modules/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
     },
     "node_modules/emoji-regex": {
       "version": "7.0.3",
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.20.4",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
-      "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
+      "version": "1.21.2",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
+      "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
       "dev": true,
       "dependencies": {
+        "array-buffer-byte-length": "^1.0.0",
+        "available-typed-arrays": "^1.0.5",
         "call-bind": "^1.0.2",
+        "es-set-tostringtag": "^2.0.1",
         "es-to-primitive": "^1.2.1",
-        "function-bind": "^1.1.1",
         "function.prototype.name": "^1.1.5",
-        "get-intrinsic": "^1.1.3",
+        "get-intrinsic": "^1.2.0",
         "get-symbol-description": "^1.0.0",
+        "globalthis": "^1.0.3",
+        "gopd": "^1.0.1",
         "has": "^1.0.3",
         "has-property-descriptors": "^1.0.0",
+        "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.3",
+        "internal-slot": "^1.0.5",
+        "is-array-buffer": "^3.0.2",
         "is-callable": "^1.2.7",
         "is-negative-zero": "^2.0.2",
         "is-regex": "^1.1.4",
         "is-shared-array-buffer": "^1.0.2",
         "is-string": "^1.0.7",
+        "is-typed-array": "^1.1.10",
         "is-weakref": "^1.0.2",
-        "object-inspect": "^1.12.2",
+        "object-inspect": "^1.12.3",
         "object-keys": "^1.1.1",
         "object.assign": "^4.1.4",
         "regexp.prototype.flags": "^1.4.3",
         "safe-regex-test": "^1.0.0",
-        "string.prototype.trimend": "^1.0.5",
-        "string.prototype.trimstart": "^1.0.5",
-        "unbox-primitive": "^1.0.2"
+        "string.prototype.trim": "^1.2.7",
+        "string.prototype.trimend": "^1.0.6",
+        "string.prototype.trimstart": "^1.0.6",
+        "typed-array-length": "^1.0.4",
+        "unbox-primitive": "^1.0.2",
+        "which-typed-array": "^1.1.9"
       },
       "engines": {
         "node": ">= 0.4"
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+      "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.1.3",
+        "has": "^1.0.3",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-shim-unscopables": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+      "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+      "dev": true,
+      "dependencies": {
+        "has": "^1.0.3"
+      }
+    },
     "node_modules/es-to-primitive": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       }
     },
     "node_modules/esbuild": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
-      "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==",
+      "version": "0.17.17",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz",
+      "integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.17.3",
-        "@esbuild/android-arm64": "0.17.3",
-        "@esbuild/android-x64": "0.17.3",
-        "@esbuild/darwin-arm64": "0.17.3",
-        "@esbuild/darwin-x64": "0.17.3",
-        "@esbuild/freebsd-arm64": "0.17.3",
-        "@esbuild/freebsd-x64": "0.17.3",
-        "@esbuild/linux-arm": "0.17.3",
-        "@esbuild/linux-arm64": "0.17.3",
-        "@esbuild/linux-ia32": "0.17.3",
-        "@esbuild/linux-loong64": "0.17.3",
-        "@esbuild/linux-mips64el": "0.17.3",
-        "@esbuild/linux-ppc64": "0.17.3",
-        "@esbuild/linux-riscv64": "0.17.3",
-        "@esbuild/linux-s390x": "0.17.3",
-        "@esbuild/linux-x64": "0.17.3",
-        "@esbuild/netbsd-x64": "0.17.3",
-        "@esbuild/openbsd-x64": "0.17.3",
-        "@esbuild/sunos-x64": "0.17.3",
-        "@esbuild/win32-arm64": "0.17.3",
-        "@esbuild/win32-ia32": "0.17.3",
-        "@esbuild/win32-x64": "0.17.3"
+        "@esbuild/android-arm": "0.17.17",
+        "@esbuild/android-arm64": "0.17.17",
+        "@esbuild/android-x64": "0.17.17",
+        "@esbuild/darwin-arm64": "0.17.17",
+        "@esbuild/darwin-x64": "0.17.17",
+        "@esbuild/freebsd-arm64": "0.17.17",
+        "@esbuild/freebsd-x64": "0.17.17",
+        "@esbuild/linux-arm": "0.17.17",
+        "@esbuild/linux-arm64": "0.17.17",
+        "@esbuild/linux-ia32": "0.17.17",
+        "@esbuild/linux-loong64": "0.17.17",
+        "@esbuild/linux-mips64el": "0.17.17",
+        "@esbuild/linux-ppc64": "0.17.17",
+        "@esbuild/linux-riscv64": "0.17.17",
+        "@esbuild/linux-s390x": "0.17.17",
+        "@esbuild/linux-x64": "0.17.17",
+        "@esbuild/netbsd-x64": "0.17.17",
+        "@esbuild/openbsd-x64": "0.17.17",
+        "@esbuild/sunos-x64": "0.17.17",
+        "@esbuild/win32-arm64": "0.17.17",
+        "@esbuild/win32-ia32": "0.17.17",
+        "@esbuild/win32-x64": "0.17.17"
       }
     },
     "node_modules/escape-string-regexp": {
         "node": ">=0.8.0"
       }
     },
-    "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
-      "dependencies": {
-        "to-regex-range": "^5.0.1"
+    "node_modules/eslint": {
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
+      "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.4.0",
+        "@eslint/eslintrc": "^2.0.2",
+        "@eslint/js": "8.38.0",
+        "@humanwhocodes/config-array": "^0.11.8",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "ajv": "^6.10.0",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.4.0",
+        "espree": "^9.5.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "grapheme-splitter": "^1.0.4",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.0.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-sdsl": "^4.1.4",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.1",
+        "strip-ansi": "^6.0.1",
+        "strip-json-comments": "^3.1.0",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
       },
       "engines": {
-        "node": ">=8"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/find-up": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
-      "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+    "node_modules/eslint-config-airbnb-base": {
+      "version": "15.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz",
+      "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==",
       "dev": true,
       "dependencies": {
-        "locate-path": "^3.0.0"
+        "confusing-browser-globals": "^1.0.10",
+        "object.assign": "^4.1.2",
+        "object.entries": "^1.1.5",
+        "semver": "^6.3.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": "^10.12.0 || >=12.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^7.32.0 || ^8.2.0",
+        "eslint-plugin-import": "^2.25.2"
       }
     },
-    "node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+    "node_modules/eslint-config-airbnb-base/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
       "dev": true,
-      "hasInstallScript": true,
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      "bin": {
+        "semver": "bin/semver.js"
       }
     },
-    "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
-    },
-    "node_modules/function.prototype.name": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
-      "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+    "node_modules/eslint-import-resolver-node": {
+      "version": "0.3.7",
+      "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+      "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.19.0",
-        "functions-have-names": "^1.2.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "debug": "^3.2.7",
+        "is-core-module": "^2.11.0",
+        "resolve": "^1.22.1"
       }
     },
-    "node_modules/functions-have-names": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
-      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+    "node_modules/eslint-import-resolver-node/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
       "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "dependencies": {
+        "ms": "^2.1.1"
       }
     },
-    "node_modules/get-caller-file": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+    "node_modules/eslint-module-utils": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+      "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
       "dev": true,
+      "dependencies": {
+        "debug": "^3.2.7"
+      },
       "engines": {
-        "node": "6.* || 8.* || >= 10.*"
+        "node": ">=4"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
       }
     },
-    "node_modules/get-intrinsic": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
-      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+    "node_modules/eslint-module-utils/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
       "dev": true,
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has": "^1.0.3",
-        "has-symbols": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "ms": "^2.1.1"
       }
     },
-    "node_modules/get-symbol-description": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-      "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+    "node_modules/eslint-plugin-import": {
+      "version": "2.27.5",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
+      "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "array-includes": "^3.1.6",
+        "array.prototype.flat": "^1.3.1",
+        "array.prototype.flatmap": "^1.3.1",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.7",
+        "eslint-module-utils": "^2.7.4",
+        "has": "^1.0.3",
+        "is-core-module": "^2.11.0",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.values": "^1.1.6",
+        "resolve": "^1.22.1",
+        "semver": "^6.3.0",
+        "tsconfig-paths": "^3.14.1"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=4"
       },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
       }
     },
-    "node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+    "node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+      "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
       "dev": true,
       "dependencies": {
-        "is-glob": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
+        "ms": "^2.1.1"
       }
     },
-    "node_modules/good-listener": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
-      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+    "node_modules/eslint-plugin-import/node_modules/doctrine": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+      "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+      "dev": true,
       "dependencies": {
-        "delegate": "^3.1.2"
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "node_modules/graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
-      "dev": true
+    "node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver.js"
+      }
     },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+    "node_modules/eslint-scope": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
       "dev": true,
       "dependencies": {
-        "function-bind": "^1.1.1"
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
       },
       "engines": {
-        "node": ">= 0.4.0"
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/has-bigints": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
-      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
+      "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
       "dev": true,
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/has-flag": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+    "node_modules/eslint/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
       "dev": true,
       "engines": {
-        "node": ">=4"
+        "node": ">=8"
       }
     },
-    "node_modules/has-property-descriptors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
-      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+    "node_modules/eslint/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.1.1"
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
-    "node_modules/has-symbols": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
-      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+    "node_modules/eslint/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
       "dev": true,
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/chalk/chalk?sponsor=1"
       }
     },
-    "node_modules/has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+    "node_modules/eslint/node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
       "dev": true,
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "color-name": "~1.1.4"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=7.0.0"
       }
     },
-    "node_modules/hosted-git-info": {
-      "version": "2.8.9",
-      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
-      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
-      "dev": true
-    },
-    "node_modules/immutable": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
-      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+    "node_modules/eslint/node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
-    "node_modules/internal-slot": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
-      "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+    "node_modules/eslint/node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
       "dev": true,
       "dependencies": {
-        "get-intrinsic": "^1.1.0",
-        "has": "^1.0.3",
-        "side-channel": "^1.0.4"
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">= 8"
       }
     },
-    "node_modules/is-arrayish": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-      "dev": true
-    },
-    "node_modules/is-bigint": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
-      "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+    "node_modules/eslint/node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
       "dev": true,
-      "dependencies": {
-        "has-bigints": "^1.0.1"
+      "engines": {
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+    "node_modules/eslint/node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
       "dev": true,
       "dependencies": {
-        "binary-extensions": "^2.0.0"
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-boolean-object": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
-      "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+    "node_modules/eslint/node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
+        "is-glob": "^4.0.3"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=10.13.0"
       }
     },
-    "node_modules/is-callable": {
-      "version": "1.2.7",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
-      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+    "node_modules/eslint/node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
       "dev": true,
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-core-module": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
-      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
-      "dev": true,
-      "dependencies": {
-        "has": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/is-date-object": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
-      "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+    "node_modules/eslint/node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
       "dev": true,
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "p-locate": "^5.0.0"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-fullwidth-code-point": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+    "node_modules/eslint/node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
       "dev": true,
       "dependencies": {
-        "is-extglob": "^2.1.1"
+        "yocto-queue": "^0.1.0"
       },
       "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-negative-zero": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.12.0"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-number-object": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
-      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+    "node_modules/eslint/node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
       "dev": true,
       "dependencies": {
-        "has-tostringtag": "^1.0.0"
+        "p-limit": "^3.0.2"
       },
       "engines": {
-        "node": ">= 0.4"
+        "node": ">=10"
       },
       "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-regex": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
-      "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+    "node_modules/eslint/node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
       "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-tostringtag": "^1.0.0"
-      },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-shared-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/is-string": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
-      "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+    "node_modules/eslint/node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
       "dev": true,
-      "dependencies": {
-        "has-tostringtag": "^1.0.0"
-      },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=8"
       }
     },
-    "node_modules/is-symbol": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
-      "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+    "node_modules/eslint/node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
       "dev": true,
       "dependencies": {
-        "has-symbols": "^1.0.2"
+        "shebang-regex": "^3.0.0"
       },
       "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/is-weakref": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
-      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true
-    },
-    "node_modules/json-parse-better-errors": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
-      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
-      "dev": true
-    },
-    "node_modules/linkify-it": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
-      "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
-      "dependencies": {
-        "uc.micro": "^1.0.1"
+        "node": ">=8"
       }
     },
-    "node_modules/livereload": {
-      "version": "0.9.3",
-      "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz",
-      "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==",
+    "node_modules/eslint/node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
       "dev": true,
-      "dependencies": {
-        "chokidar": "^3.5.0",
-        "livereload-js": "^3.3.1",
-        "opts": ">= 1.2.0",
-        "ws": "^7.4.3"
-      },
-      "bin": {
-        "livereload": "bin/livereload.js"
-      },
       "engines": {
-        "node": ">=8.0.0"
+        "node": ">=8"
       }
     },
-    "node_modules/livereload-js": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz",
-      "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
-      "dev": true
-    },
-    "node_modules/load-json-file": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
-      "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+    "node_modules/eslint/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
       "dev": true,
       "dependencies": {
-        "graceful-fs": "^4.1.2",
-        "parse-json": "^4.0.0",
-        "pify": "^3.0.0",
-        "strip-bom": "^3.0.0"
+        "ansi-regex": "^5.0.1"
       },
       "engines": {
-        "node": ">=4"
+        "node": ">=8"
       }
     },
-    "node_modules/locate-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
-      "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+    "node_modules/eslint/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
       "dev": true,
       "dependencies": {
-        "p-locate": "^3.0.0",
-        "path-exists": "^3.0.0"
+        "has-flag": "^4.0.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=8"
       }
     },
-    "node_modules/lodash.debounce": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
-      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
-      "dev": true
-    },
-    "node_modules/lodash.throttle": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
-      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
-      "dev": true
-    },
-    "node_modules/markdown-it": {
-      "version": "13.0.1",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
-      "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
+    "node_modules/eslint/node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
       "dependencies": {
-        "argparse": "^2.0.1",
-        "entities": "~3.0.1",
-        "linkify-it": "^4.0.1",
-        "mdurl": "^1.0.1",
-        "uc.micro": "^1.0.5"
+        "isexe": "^2.0.0"
       },
       "bin": {
-        "markdown-it": "bin/markdown-it.js"
-      }
-    },
-    "node_modules/markdown-it-task-lists": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
-      "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
-    },
-    "node_modules/mdurl": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
-      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
-    },
-    "node_modules/memorystream": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
-      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.10.0"
-      }
-    },
-    "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
+        "node-which": "bin/node-which"
       },
       "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/nice-try": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
-      "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
-      "dev": true
-    },
-    "node_modules/normalize-package-data": {
-      "version": "2.5.0",
-      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
-      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
-      "dev": true,
-      "dependencies": {
-        "hosted-git-info": "^2.1.4",
-        "resolve": "^1.10.0",
-        "semver": "2 || 3 || 4 || 5",
-        "validate-npm-package-license": "^3.0.1"
-      }
-    },
-    "node_modules/normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/npm-run-all": {
-      "version": "4.1.5",
-      "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
-      "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^3.2.1",
-        "chalk": "^2.4.1",
-        "cross-spawn": "^6.0.5",
-        "memorystream": "^0.3.1",
-        "minimatch": "^3.0.4",
-        "pidtree": "^0.3.0",
-        "read-pkg": "^3.0.0",
-        "shell-quote": "^1.6.1",
-        "string.prototype.padend": "^3.0.0"
-      },
-      "bin": {
-        "npm-run-all": "bin/npm-run-all/index.js",
-        "run-p": "bin/run-p/index.js",
-        "run-s": "bin/run-s/index.js"
-      },
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/object-inspect": {
-      "version": "1.12.2",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
-      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/object-keys": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/object.assign": {
-      "version": "4.1.4",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
-      "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "has-symbols": "^1.0.3",
-        "object-keys": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/opts": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz",
-      "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
-      "dev": true
-    },
-    "node_modules/p-limit": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
-      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
-      "dev": true,
-      "dependencies": {
-        "p-try": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-locate": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
-      "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
-      "dev": true,
-      "dependencies": {
-        "p-limit": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/p-try": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
-      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/parse-json": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-      "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
-      "dev": true,
-      "dependencies": {
-        "error-ex": "^1.3.1",
-        "json-parse-better-errors": "^1.0.1"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/path-exists": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/path-key": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
-    "node_modules/path-type": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
-      "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
-      "dev": true,
-      "dependencies": {
-        "pify": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/jonschlinkert"
-      }
-    },
-    "node_modules/pidtree": {
-      "version": "0.3.1",
-      "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
-      "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
-      "dev": true,
-      "bin": {
-        "pidtree": "bin/pidtree.js"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/pify": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-      "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/punycode": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
-      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/read-pkg": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
-      "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
-      "dev": true,
-      "dependencies": {
-        "load-json-file": "^4.0.0",
-        "normalize-package-data": "^2.3.2",
-        "path-type": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "dev": true,
-      "dependencies": {
-        "picomatch": "^2.2.1"
-      },
-      "engines": {
-        "node": ">=8.10.0"
-      }
-    },
-    "node_modules/regexp.prototype.flags": {
-      "version": "1.4.3",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
-      "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3",
-        "functions-have-names": "^1.2.2"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/require-directory": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/require-main-filename": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
-      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
-      "dev": true
-    },
-    "node_modules/resolve": {
-      "version": "1.22.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
-      "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
-      "dev": true,
-      "dependencies": {
-        "is-core-module": "^2.9.0",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      },
-      "bin": {
-        "resolve": "bin/resolve"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/safe-regex-test": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
-      "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.3",
-        "is-regex": "^1.1.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/sass": {
-      "version": "1.57.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
-      "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
-      "dev": true,
-      "dependencies": {
-        "chokidar": ">=3.0.0 <4.0.0",
-        "immutable": "^4.0.0",
-        "source-map-js": ">=0.6.2 <2.0.0"
-      },
-      "bin": {
-        "sass": "sass.js"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      }
-    },
-    "node_modules/select": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
-      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
-    },
-    "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true,
-      "bin": {
-        "semver": "bin/semver"
-      }
-    },
-    "node_modules/set-blocking": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
-      "dev": true
-    },
-    "node_modules/shebang-command": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
-      "dev": true,
-      "dependencies": {
-        "shebang-regex": "^1.0.0"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/shebang-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/shell-quote": {
-      "version": "1.7.4",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
-      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
-      "dev": true,
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/snabbdom": {
-      "version": "3.5.1",
-      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz",
-      "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
-      "engines": {
-        "node": ">=8.3.0"
-      }
-    },
-    "node_modules/sortablejs": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
-      "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
-    },
-    "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/spdx-correct": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
-      "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
-      "dev": true,
-      "dependencies": {
-        "spdx-expression-parse": "^3.0.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "node_modules/spdx-exceptions": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
-      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
-      "dev": true
-    },
-    "node_modules/spdx-expression-parse": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
-      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
-      "dev": true,
-      "dependencies": {
-        "spdx-exceptions": "^2.1.0",
-        "spdx-license-ids": "^3.0.0"
-      }
-    },
-    "node_modules/spdx-license-ids": {
-      "version": "3.0.12",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
-      "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
-      "dev": true
-    },
-    "node_modules/string-width": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
-      "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
-      "dev": true,
-      "dependencies": {
-        "emoji-regex": "^7.0.1",
-        "is-fullwidth-code-point": "^2.0.0",
-        "strip-ansi": "^5.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/string.prototype.padend": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz",
-      "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.19.1"
-      },
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/string.prototype.trimend": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
-      "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.19.5"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/string.prototype.trimstart": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
-      "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.1.4",
-        "es-abstract": "^1.19.5"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/strip-ansi": {
-      "version": "5.2.0",
-      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
-      "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-regex": "^4.1.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/strip-bom": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/supports-color": {
-      "version": "5.5.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-      "dev": true,
-      "dependencies": {
-        "has-flag": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/tiny-emitter": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
-      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
-    },
-    "node_modules/to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
-      "dependencies": {
-        "is-number": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=8.0"
-      }
-    },
-    "node_modules/uc.micro": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
-      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
-    },
-    "node_modules/unbox-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
-      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
-      "dev": true,
-      "dependencies": {
-        "call-bind": "^1.0.2",
-        "has-bigints": "^1.0.2",
-        "has-symbols": "^1.0.3",
-        "which-boxed-primitive": "^1.0.2"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/validate-npm-package-license": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
-      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
-      "dev": true,
-      "dependencies": {
-        "spdx-correct": "^3.0.0",
-        "spdx-expression-parse": "^3.0.0"
-      }
-    },
-    "node_modules/which": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
-      "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
-      "dev": true,
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "which": "bin/which"
-      }
-    },
-    "node_modules/which-boxed-primitive": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
-      "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
-      "dev": true,
-      "dependencies": {
-        "is-bigint": "^1.0.1",
-        "is-boolean-object": "^1.1.0",
-        "is-number-object": "^1.0.4",
-        "is-string": "^1.0.5",
-        "is-symbol": "^1.0.3"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
-      }
-    },
-    "node_modules/which-module": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
-      "dev": true
-    },
-    "node_modules/wrap-ansi": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
-      "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^3.2.0",
-        "string-width": "^3.0.0",
-        "strip-ansi": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/ws": {
-      "version": "7.5.9",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
-      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.3.0"
-      },
-      "peerDependencies": {
-        "bufferutil": "^4.0.1",
-        "utf-8-validate": "^5.0.2"
-      },
-      "peerDependenciesMeta": {
-        "bufferutil": {
-          "optional": true
-        },
-        "utf-8-validate": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/y18n": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
-      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
-      "dev": true
-    },
-    "node_modules/yargs": {
-      "version": "13.3.2",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
-      "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
-      "dev": true,
-      "dependencies": {
-        "cliui": "^5.0.0",
-        "find-up": "^3.0.0",
-        "get-caller-file": "^2.0.1",
-        "require-directory": "^2.1.1",
-        "require-main-filename": "^2.0.0",
-        "set-blocking": "^2.0.0",
-        "string-width": "^3.0.0",
-        "which-module": "^2.0.0",
-        "y18n": "^4.0.0",
-        "yargs-parser": "^13.1.2"
-      }
-    },
-    "node_modules/yargs-parser": {
-      "version": "13.1.2",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
-      "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
-      "dev": true,
-      "dependencies": {
-        "camelcase": "^5.0.0",
-        "decamelize": "^1.2.0"
-      }
-    }
-  },
-  "dependencies": {
-    "@esbuild/android-arm": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz",
-      "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/android-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz",
-      "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/android-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.3.tgz",
-      "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/darwin-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz",
-      "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/darwin-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz",
-      "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/freebsd-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz",
-      "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/freebsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz",
-      "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-arm": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz",
-      "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz",
-      "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-ia32": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz",
-      "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-loong64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz",
-      "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-mips64el": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz",
-      "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-ppc64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz",
-      "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-riscv64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz",
-      "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-s390x": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz",
-      "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/linux-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz",
-      "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/netbsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz",
-      "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/openbsd-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz",
-      "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/sunos-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz",
-      "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-arm64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz",
-      "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-ia32": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz",
-      "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==",
-      "dev": true,
-      "optional": true
-    },
-    "@esbuild/win32-x64": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz",
-      "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==",
-      "dev": true,
-      "optional": true
-    },
-    "ansi-regex": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
-      "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
-      "dev": true
-    },
-    "ansi-styles": {
-      "version": "3.2.1",
-      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-      "dev": true,
-      "requires": {
-        "color-convert": "^1.9.0"
-      }
-    },
-    "anymatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
-      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
-      "dev": true,
-      "requires": {
-        "normalize-path": "^3.0.0",
-        "picomatch": "^2.0.4"
-      }
-    },
-    "argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
-    },
-    "balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
-    },
-    "binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-      "dev": true
-    },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
-      "requires": {
-        "fill-range": "^7.0.1"
-      }
-    },
-    "call-bind": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
-      "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
-      "dev": true,
-      "requires": {
-        "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.0.2"
-      }
-    },
-    "camelcase": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
-      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
-      "dev": true
-    },
-    "chalk": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-      "dev": true,
-      "requires": {
-        "ansi-styles": "^3.2.1",
-        "escape-string-regexp": "^1.0.5",
-        "supports-color": "^5.3.0"
-      }
-    },
-    "chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-      "dev": true,
-      "requires": {
-        "anymatch": "~3.1.2",
-        "braces": "~3.0.2",
-        "fsevents": "~2.3.2",
-        "glob-parent": "~5.1.2",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
-      }
-    },
-    "chokidar-cli": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz",
-      "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==",
-      "dev": true,
-      "requires": {
-        "chokidar": "^3.5.2",
-        "lodash.debounce": "^4.0.8",
-        "lodash.throttle": "^4.1.1",
-        "yargs": "^13.3.0"
+        "node": ">= 8"
       }
     },
-    "clipboard": {
-      "version": "2.0.11",
-      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
-      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
-      "requires": {
-        "good-listener": "^1.2.2",
-        "select": "^1.1.2",
-        "tiny-emitter": "^2.0.0"
+    "node_modules/espree": {
+      "version": "9.5.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
+      "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==",
+      "dev": true,
+      "dependencies": {
+        "acorn": "^8.8.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
       }
     },
-    "cliui": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
-      "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+    "node_modules/esquery": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
       "dev": true,
-      "requires": {
-        "string-width": "^3.1.0",
-        "strip-ansi": "^5.2.0",
-        "wrap-ansi": "^5.1.0"
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
       }
     },
-    "codemirror": {
-      "version": "5.65.9",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
-      "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
-    },
-    "color-convert": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "dev": true,
-      "requires": {
-        "color-name": "1.1.3"
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
       }
     },
-    "color-name": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-      "dev": true
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true
-    },
-    "cross-spawn": {
-      "version": "6.0.5",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
-      "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
       "dev": true,
-      "requires": {
-        "nice-try": "^1.0.4",
-        "path-key": "^2.0.1",
-        "semver": "^5.5.0",
-        "shebang-command": "^1.2.0",
-        "which": "^1.2.9"
+      "engines": {
+        "node": ">=4.0"
       }
     },
-    "decamelize": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
-      "dev": true
-    },
-    "define-properties": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
-      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true,
-      "requires": {
-        "has-property-descriptors": "^1.0.0",
-        "object-keys": "^1.1.1"
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "delegate": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
-      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
-    },
-    "dropzone": {
-      "version": "5.9.3",
-      "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz",
-      "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA=="
-    },
-    "emoji-regex": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
-      "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "dev": true
     },
-    "entities": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
-      "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
     },
-    "error-ex": {
-      "version": "1.3.2",
-      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
-      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
-      "dev": true,
-      "requires": {
-        "is-arrayish": "^0.2.1"
-      }
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true
     },
-    "es-abstract": {
-      "version": "1.20.4",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
-      "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
+    "node_modules/fastq": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
       "dev": true,
-      "requires": {
-        "call-bind": "^1.0.2",
-        "es-to-primitive": "^1.2.1",
-        "function-bind": "^1.1.1",
-        "function.prototype.name": "^1.1.5",
-        "get-intrinsic": "^1.1.3",
-        "get-symbol-description": "^1.0.0",
-        "has": "^1.0.3",
-        "has-property-descriptors": "^1.0.0",
-        "has-symbols": "^1.0.3",
-        "internal-slot": "^1.0.3",
-        "is-callable": "^1.2.7",
-        "is-negative-zero": "^2.0.2",
-        "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.2",
-        "is-string": "^1.0.7",
-        "is-weakref": "^1.0.2",
-        "object-inspect": "^1.12.2",
-        "object-keys": "^1.1.1",
-        "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.4.3",
-        "safe-regex-test": "^1.0.0",
-        "string.prototype.trimend": "^1.0.5",
-        "string.prototype.trimstart": "^1.0.5",
-        "unbox-primitive": "^1.0.2"
+      "dependencies": {
+        "reusify": "^1.0.4"
       }
     },
-    "es-to-primitive": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
-      "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
       "dev": true,
-      "requires": {
-        "is-callable": "^1.1.4",
-        "is-date-object": "^1.0.1",
-        "is-symbol": "^1.0.2"
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
       }
     },
-    "esbuild": {
-      "version": "0.17.3",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.3.tgz",
-      "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==",
-      "dev": true,
-      "requires": {
-        "@esbuild/android-arm": "0.17.3",
-        "@esbuild/android-arm64": "0.17.3",
-        "@esbuild/android-x64": "0.17.3",
-        "@esbuild/darwin-arm64": "0.17.3",
-        "@esbuild/darwin-x64": "0.17.3",
-        "@esbuild/freebsd-arm64": "0.17.3",
-        "@esbuild/freebsd-x64": "0.17.3",
-        "@esbuild/linux-arm": "0.17.3",
-        "@esbuild/linux-arm64": "0.17.3",
-        "@esbuild/linux-ia32": "0.17.3",
-        "@esbuild/linux-loong64": "0.17.3",
-        "@esbuild/linux-mips64el": "0.17.3",
-        "@esbuild/linux-ppc64": "0.17.3",
-        "@esbuild/linux-riscv64": "0.17.3",
-        "@esbuild/linux-s390x": "0.17.3",
-        "@esbuild/linux-x64": "0.17.3",
-        "@esbuild/netbsd-x64": "0.17.3",
-        "@esbuild/openbsd-x64": "0.17.3",
-        "@esbuild/sunos-x64": "0.17.3",
-        "@esbuild/win32-arm64": "0.17.3",
-        "@esbuild/win32-ia32": "0.17.3",
-        "@esbuild/win32-x64": "0.17.3"
-      }
-    },
-    "escape-string-regexp": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-      "dev": true
-    },
-    "fill-range": {
+    "node_modules/fill-range": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
       "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
       }
     },
-    "find-up": {
+    "node_modules/find-up": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
       "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "locate-path": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+      "dev": true,
+      "dependencies": {
+        "flatted": "^3.1.0",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.2.7",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+      "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+      "dev": true
+    },
+    "node_modules/for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "dev": true,
+      "dependencies": {
+        "is-callable": "^1.1.3"
       }
     },
-    "fsevents": {
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true
+    },
+    "node_modules/fsevents": {
       "version": "2.3.2",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
       "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
       "dev": true,
-      "optional": true
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
     },
-    "function-bind": {
+    "node_modules/function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
-    "function.prototype.name": {
+    "node_modules/function.prototype.name": {
       "version": "1.1.5",
       "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
       "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.1.3",
         "es-abstract": "^1.19.0",
         "functions-have-names": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "functions-have-names": {
+    "node_modules/functions-have-names": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
       "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
-      "dev": true
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "get-caller-file": {
+    "node_modules/get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
     },
-    "get-intrinsic": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
-      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
+    "node_modules/get-intrinsic": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
+      "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
         "has-symbols": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "get-symbol-description": {
+    "node_modules/get-symbol-description": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
       "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "get-intrinsic": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "glob-parent": {
+    "node_modules/glob-parent": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
       "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
       }
     },
-    "good-listener": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
-      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
-      "requires": {
-        "delegate": "^3.1.2"
+    "node_modules/globals": {
+      "version": "13.20.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+      "dev": true,
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/globalthis": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+      "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+      "dev": true,
+      "dependencies": {
+        "define-properties": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+      "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.1.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "graceful-fs": {
-      "version": "4.2.10",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
-      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true
+    },
+    "node_modules/grapheme-splitter": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
       "dev": true
     },
-    "has": {
+    "node_modules/has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "function-bind": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
       }
     },
-    "has-bigints": {
+    "node_modules/has-bigints": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
       "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
-      "dev": true
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "has-flag": {
+    "node_modules/has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
     },
-    "has-property-descriptors": {
+    "node_modules/has-property-descriptors": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
       "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "get-intrinsic": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "has-symbols": {
+    "node_modules/has-symbols": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
       "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "has-tostringtag": {
+    "node_modules/has-tostringtag": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
       "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-symbols": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "hosted-git-info": {
+    "node_modules/hosted-git-info": {
       "version": "2.8.9",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
       "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
       "dev": true
     },
-    "immutable": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
-      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+    "node_modules/ignore": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+      "dev": true,
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/immutable": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
+      "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
       "dev": true
     },
-    "internal-slot": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
-      "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "dev": true,
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true
+    },
+    "node_modules/internal-slot": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
+      "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
       "dev": true,
-      "requires": {
-        "get-intrinsic": "^1.1.0",
+      "dependencies": {
+        "get-intrinsic": "^1.2.0",
         "has": "^1.0.3",
         "side-channel": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/is-array-buffer": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+      "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "get-intrinsic": "^1.2.0",
+        "is-typed-array": "^1.1.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-arrayish": {
+    "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
       "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
       "dev": true
     },
-    "is-bigint": {
+    "node_modules/is-bigint": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
       "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-bigints": "^1.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-binary-path": {
+    "node_modules/is-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
       "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
       }
     },
-    "is-boolean-object": {
+    "node_modules/is-boolean-object": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
       "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-callable": {
+    "node_modules/is-callable": {
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
       "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "is-core-module": {
-      "version": "2.11.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
-      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+    "node_modules/is-core-module": {
+      "version": "2.12.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
+      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-date-object": {
+    "node_modules/is-date-object": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
       "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-extglob": {
+    "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
       "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "is-fullwidth-code-point": {
+    "node_modules/is-fullwidth-code-point": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
       "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
     },
-    "is-glob": {
+    "node_modules/is-glob": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
       "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "is-negative-zero": {
+    "node_modules/is-negative-zero": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
       "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "is-number": {
+    "node_modules/is-number": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
       "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.12.0"
+      }
     },
-    "is-number-object": {
+    "node_modules/is-number-object": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
       "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
       }
     },
-    "is-regex": {
+    "node_modules/is-regex": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
       "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-shared-array-buffer": {
+    "node_modules/is-shared-array-buffer": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
       "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-string": {
+    "node_modules/is-string": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
       "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-symbol": {
+    "node_modules/is-symbol": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
       "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-symbols": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "is-weakref": {
+    "node_modules/is-typed-array": {
+      "version": "1.1.10",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+      "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-weakref": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
       "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "isexe": {
+    "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
-    "json-parse-better-errors": {
+    "node_modules/js-sdsl": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
+      "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
+      "dev": true,
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/js-sdsl"
+      }
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-parse-better-errors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
       "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
       "dev": true
     },
-    "linkify-it": {
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true
+    },
+    "node_modules/json5": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+      "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/linkify-it": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
       "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
-      "requires": {
+      "dependencies": {
         "uc.micro": "^1.0.1"
       }
     },
-    "livereload": {
+    "node_modules/livereload": {
       "version": "0.9.3",
       "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz",
       "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "chokidar": "^3.5.0",
         "livereload-js": "^3.3.1",
         "opts": ">= 1.2.0",
         "ws": "^7.4.3"
+      },
+      "bin": {
+        "livereload": "bin/livereload.js"
+      },
+      "engines": {
+        "node": ">=8.0.0"
       }
     },
-    "livereload-js": {
+    "node_modules/livereload-js": {
       "version": "3.4.1",
       "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz",
       "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
       "dev": true
     },
-    "load-json-file": {
+    "node_modules/load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
       "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "graceful-fs": "^4.1.2",
         "parse-json": "^4.0.0",
         "pify": "^3.0.0",
         "strip-bom": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
-    "locate-path": {
+    "node_modules/locate-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
       "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "p-locate": "^3.0.0",
         "path-exists": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
-    "lodash.debounce": {
+    "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
       "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
       "dev": true
     },
-    "lodash.throttle": {
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true
+    },
+    "node_modules/lodash.throttle": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
       "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
       "dev": true
     },
-    "markdown-it": {
+    "node_modules/markdown-it": {
       "version": "13.0.1",
       "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
       "integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
-      "requires": {
+      "dependencies": {
         "argparse": "^2.0.1",
         "entities": "~3.0.1",
         "linkify-it": "^4.0.1",
         "mdurl": "^1.0.1",
         "uc.micro": "^1.0.5"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.js"
       }
     },
-    "markdown-it-task-lists": {
+    "node_modules/markdown-it-task-lists": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz",
       "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA=="
     },
-    "mdurl": {
+    "node_modules/mdurl": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
       "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
     },
-    "memorystream": {
+    "node_modules/memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
       "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10.0"
+      }
     },
-    "minimatch": {
+    "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
       "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "nice-try": {
+    "node_modules/ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+      "dev": true
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true
+    },
+    "node_modules/nice-try": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "dev": true
     },
-    "normalize-package-data": {
+    "node_modules/normalize-package-data": {
       "version": "2.5.0",
       "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
       "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "hosted-git-info": "^2.1.4",
         "resolve": "^1.10.0",
         "semver": "2 || 3 || 4 || 5",
         "validate-npm-package-license": "^3.0.1"
       }
     },
-    "normalize-path": {
+    "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "npm-run-all": {
+    "node_modules/npm-run-all": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
       "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "ansi-styles": "^3.2.1",
         "chalk": "^2.4.1",
         "cross-spawn": "^6.0.5",
         "read-pkg": "^3.0.0",
         "shell-quote": "^1.6.1",
         "string.prototype.padend": "^3.0.0"
+      },
+      "bin": {
+        "npm-run-all": "bin/npm-run-all/index.js",
+        "run-p": "bin/run-p/index.js",
+        "run-s": "bin/run-s/index.js"
+      },
+      "engines": {
+        "node": ">= 4"
       }
     },
-    "object-inspect": {
-      "version": "1.12.2",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
-      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
-      "dev": true
+    "node_modules/object-inspect": {
+      "version": "1.12.3",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+      "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "object-keys": {
+    "node_modules/object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
       "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      }
     },
-    "object.assign": {
+    "node_modules/object.assign": {
       "version": "4.1.4",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
       "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.1.4",
         "has-symbols": "^1.0.3",
         "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.entries": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz",
+      "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/object.values": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+      "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "opts": {
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "dev": true,
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.3"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/opts": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz",
       "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
       "dev": true
     },
-    "p-limit": {
+    "node_modules/p-limit": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
       "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "p-locate": {
+    "node_modules/p-locate": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
       "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "p-limit": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
-    "p-try": {
+    "node_modules/p-try": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
       "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
     },
-    "parse-json": {
+    "node_modules/parse-json": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
       "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "error-ex": "^1.3.1",
         "json-parse-better-errors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
-    "path-exists": {
+    "node_modules/path-exists": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
       "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "path-key": {
+    "node_modules/path-key": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
       "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
     },
-    "path-parse": {
+    "node_modules/path-parse": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
       "dev": true
     },
-    "path-type": {
+    "node_modules/path-type": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
       "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "pify": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
-    "picomatch": {
+    "node_modules/picomatch": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
       "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
     },
-    "pidtree": {
+    "node_modules/pidtree": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
       "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
-      "dev": true
+      "dev": true,
+      "bin": {
+        "pidtree": "bin/pidtree.js"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
     },
-    "pify": {
+    "node_modules/pify": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
       "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.8.0"
+      }
     },
-    "punycode": {
+    "node_modules/punycode": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
       "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
     },
-    "read-pkg": {
+    "node_modules/read-pkg": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
       "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "load-json-file": "^4.0.0",
         "normalize-package-data": "^2.3.2",
         "path-type": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
-    "readdirp": {
+    "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
       "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
       }
     },
-    "regexp.prototype.flags": {
+    "node_modules/regexp.prototype.flags": {
       "version": "1.4.3",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
       "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.1.3",
         "functions-have-names": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "require-directory": {
+    "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "require-main-filename": {
+    "node_modules/require-main-filename": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "dev": true
     },
-    "resolve": {
-      "version": "1.22.1",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
-      "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+    "node_modules/resolve": {
+      "version": "1.22.3",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz",
+      "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==",
       "dev": true,
-      "requires": {
-        "is-core-module": "^2.9.0",
+      "dependencies": {
+        "is-core-module": "^2.12.0",
         "path-parse": "^1.0.7",
         "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
       }
     },
-    "safe-regex-test": {
+    "node_modules/safe-regex-test": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
       "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "get-intrinsic": "^1.1.3",
         "is-regex": "^1.1.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "sass": {
-      "version": "1.57.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.57.1.tgz",
-      "integrity": "sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==",
+    "node_modules/sass": {
+      "version": "1.62.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz",
+      "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
         "immutable": "^4.0.0",
         "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
       }
     },
-    "select": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
-      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
-    },
-    "semver": {
+    "node_modules/semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
       "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-      "dev": true
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
     },
-    "set-blocking": {
+    "node_modules/set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
       "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
-    "shebang-command": {
+    "node_modules/shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
       "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "shebang-regex": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
-    "shebang-regex": {
+    "node_modules/shebang-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
       "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "shell-quote": {
-      "version": "1.7.4",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
-      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
-      "dev": true
+    "node_modules/shell-quote": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
+      "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "side-channel": {
+    "node_modules/side-channel": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
       "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.0",
         "get-intrinsic": "^1.0.2",
         "object-inspect": "^1.9.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "snabbdom": {
+    "node_modules/snabbdom": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz",
-      "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA=="
+      "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
+      "engines": {
+        "node": ">=8.3.0"
+      }
     },
-    "sortablejs": {
+    "node_modules/sortablejs": {
       "version": "1.15.0",
       "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
       "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w=="
     },
-    "source-map-js": {
+    "node_modules/source-map-js": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
       "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
     },
-    "spdx-correct": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
-      "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==",
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "spdx-expression-parse": "^3.0.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
-    "spdx-exceptions": {
+    "node_modules/spdx-exceptions": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
       "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
       "dev": true
     },
-    "spdx-expression-parse": {
+    "node_modules/spdx-expression-parse": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
       "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "spdx-exceptions": "^2.1.0",
         "spdx-license-ids": "^3.0.0"
       }
     },
-    "spdx-license-ids": {
-      "version": "3.0.12",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
-      "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.13",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz",
+      "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==",
       "dev": true
     },
-    "string-width": {
+    "node_modules/string-width": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
       "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "emoji-regex": "^7.0.1",
         "is-fullwidth-code-point": "^2.0.0",
         "strip-ansi": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
-    "string.prototype.padend": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz",
-      "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==",
+    "node_modules/string.prototype.padend": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz",
+      "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3",
-        "es-abstract": "^1.19.1"
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "string.prototype.trimend": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
-      "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
+    "node_modules/string.prototype.trim": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
+      "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.1.4",
-        "es-abstract": "^1.19.5"
+        "es-abstract": "^1.20.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "string.prototype.trimstart": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
-      "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
+    "node_modules/string.prototype.trimend": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+      "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/string.prototype.trimstart": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+      "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "define-properties": "^1.1.4",
-        "es-abstract": "^1.19.5"
+        "es-abstract": "^1.20.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "strip-ansi": {
+    "node_modules/strip-ansi": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
       "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "ansi-regex": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
-    "strip-bom": {
+    "node_modules/strip-bom": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
       "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/style-mod": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz",
+      "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw=="
     },
-    "supports-color": {
+    "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
       "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
       }
     },
-    "supports-preserve-symlinks-flag": {
+    "node_modules/supports-preserve-symlinks-flag": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
-    "tiny-emitter": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
-      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true
     },
-    "to-regex-range": {
+    "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
       "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/tsconfig-paths": {
+      "version": "3.14.2",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+      "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+      "dev": true,
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/typed-array-length": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+      "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "is-typed-array": "^1.1.9"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "uc.micro": {
+    "node_modules/uc.micro": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
       "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
     },
-    "unbox-primitive": {
+    "node_modules/unbox-primitive": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
       "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "call-bind": "^1.0.2",
         "has-bigints": "^1.0.2",
         "has-symbols": "^1.0.3",
         "which-boxed-primitive": "^1.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "dependencies": {
+        "punycode": "^2.1.0"
       }
     },
-    "validate-npm-package-license": {
+    "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
       "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "spdx-correct": "^3.0.0",
         "spdx-expression-parse": "^3.0.0"
       }
     },
-    "which": {
+    "node_modules/w3c-keyname": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
+      "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg=="
+    },
+    "node_modules/which": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
       "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "isexe": "^2.0.0"
+      },
+      "bin": {
+        "which": "bin/which"
       }
     },
-    "which-boxed-primitive": {
+    "node_modules/which-boxed-primitive": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
       "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "is-bigint": "^1.0.1",
         "is-boolean-object": "^1.1.0",
         "is-number-object": "^1.0.4",
         "is-string": "^1.0.5",
         "is-symbol": "^1.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "which-module": {
+    "node_modules/which-module": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
       "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
       "dev": true
     },
-    "wrap-ansi": {
+    "node_modules/which-typed-array": {
+      "version": "1.1.9",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+      "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+      "dev": true,
+      "dependencies": {
+        "available-typed-arrays": "^1.0.5",
+        "call-bind": "^1.0.2",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "has-tostringtag": "^1.0.0",
+        "is-typed-array": "^1.1.10"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrap-ansi": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
       "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "ansi-styles": "^3.2.0",
         "string-width": "^3.0.0",
         "strip-ansi": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
-    "ws": {
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true
+    },
+    "node_modules/ws": {
       "version": "7.5.9",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
       "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
       "dev": true,
-      "requires": {}
+      "engines": {
+        "node": ">=8.3.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
     },
-    "y18n": {
+    "node_modules/y18n": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
       "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
       "dev": true
     },
-    "yargs": {
+    "node_modules/yargs": {
       "version": "13.3.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
       "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "cliui": "^5.0.0",
         "find-up": "^3.0.0",
         "get-caller-file": "^2.0.1",
         "yargs-parser": "^13.1.2"
       }
     },
-    "yargs-parser": {
+    "node_modules/yargs-parser": {
       "version": "13.1.2",
       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
       "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
       "dev": true,
-      "requires": {
+      "dependencies": {
         "camelcase": "^5.0.0",
         "decamelize": "^1.2.0"
       }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
     }
   }
 }
index 89fb0749293a7b7a252ad78c8ec4453a58c032f2..499e32833e3f3abc009b914f3d59dddfa89db7f5 100644 (file)
     "build:css:watch": "sass ./resources/sass:./public/dist --watch --embed-sources",
     "build:css:production": "sass ./resources/sass:./public/dist -s compressed",
     "build:js:dev": "node dev/build/esbuild.js",
-    "build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
+    "build:js:watch": "chokidar --initial \"./resources/**/*.js\" \"./resources/**/*.mjs\" -c \"npm run build:js:dev\"",
     "build:js:production": "node dev/build/esbuild.js production",
     "build": "npm-run-all --parallel build:*:dev",
     "production": "npm-run-all --parallel build:*:production",
     "dev": "npm-run-all --parallel watch livereload",
     "watch": "npm-run-all --parallel build:*:watch",
     "livereload": "livereload ./public/dist/",
-    "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
+    "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads",
+    "lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"",
+    "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\""
   },
   "devDependencies": {
+    "@lezer/generator": "^1.2.2",
     "chokidar-cli": "^3.0",
-    "esbuild": "^0.17.3",
+    "esbuild": "^0.17.16",
+    "eslint": "^8.38.0",
+    "eslint-config-airbnb-base": "^15.0.0",
+    "eslint-plugin-import": "^2.27.5",
     "livereload": "^0.9.3",
     "npm-run-all": "^4.1.5",
     "punycode": "^2.3.0",
-    "sass": "^1.57.0"
+    "sass": "^1.62.0"
   },
   "dependencies": {
-    "clipboard": "^2.0.11",
-    "codemirror": "^5.65.5",
-    "dropzone": "^5.9.3",
+    "@codemirror/commands": "^6.2.2",
+    "@codemirror/lang-css": "^6.1.1",
+    "@codemirror/lang-html": "^6.4.3",
+    "@codemirror/lang-javascript": "^6.1.6",
+    "@codemirror/lang-json": "^6.0.1",
+    "@codemirror/lang-markdown": "^6.1.1",
+    "@codemirror/lang-php": "^6.0.1",
+    "@codemirror/lang-xml": "^6.0.2",
+    "@codemirror/language": "^6.6.0",
+    "@codemirror/legacy-modes": "^6.3.2",
+    "@codemirror/state": "^6.2.0",
+    "@codemirror/theme-one-dark": "^6.1.1",
+    "@codemirror/view": "^6.9.4",
+    "@lezer/highlight": "^1.1.4",
+    "@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
+    "@ssddanbrown/codemirror-lang-twig": "^1.0.0",
+    "codemirror": "^6.0.1",
     "markdown-it": "^13.0.1",
     "markdown-it-task-lists": "^2.1.1",
     "snabbdom": "^3.5.1",
     "sortablejs": "^1.15.0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "browser": true,
+      "es2021": true
+    },
+    "extends": "airbnb-base",
+    "ignorePatterns": [
+      "resources/**/*-stub.js"
+    ],
+    "overrides": [],
+    "parserOptions": {
+      "ecmaVersion": "latest",
+      "sourceType": "module"
+    },
+    "rules": {
+      "indent": [
+        "error",
+        4
+      ],
+      "arrow-parens": [
+        "error",
+        "as-needed"
+      ],
+      "padded-blocks": [
+        "error",
+        {
+          "blocks": "never",
+          "classes": "always"
+        }
+      ],
+      "object-curly-spacing": [
+        "error",
+        "never"
+      ],
+      "space-before-function-paren": [
+        "error",
+        {
+          "anonymous": "never",
+          "named": "never",
+          "asyncArrow": "always"
+        }
+      ],
+      "import/prefer-default-export": "off",
+      "no-plusplus": [
+        "error",
+        {
+          "allowForLoopAfterthoughts": true
+        }
+      ],
+      "arrow-body-style": "off",
+      "no-restricted-syntax": "off",
+      "no-continue": "off",
+      "prefer-destructuring": "off",
+      "class-methods-use-this": "off",
+      "no-param-reassign": "off",
+      "no-console": [
+        "warn",
+        {
+          "allow": [
+            "error",
+            "warn"
+          ]
+        }
+      ],
+      "no-new": "off",
+      "max-len": [
+        "error",
+        {
+          "code": 110,
+          "tabWidth": 4,
+          "ignoreUrls": true,
+          "ignoreComments": false,
+          "ignoreRegExpLiterals": true,
+          "ignoreStrings": true,
+          "ignoreTemplateLiterals": true
+        }
+      ]
+    }
   }
 }
index 8a526a70428258ed3ba4d5148baea83c478a507f..704372c5c64f123a42702d62809653ceda95199c 100644 (file)
@@ -27,6 +27,7 @@
     <server name="DB_CONNECTION" value="mysql_testing"/>
     <server name="BCRYPT_ROUNDS" value="4"/>
     <server name="MAIL_DRIVER" value="array"/>
+    <server name="MAIL_VERIFY_SSL" value="true"/>
     <server name="LOG_CHANNEL" value="single"/>
     <server name="AUTH_METHOD" value="standard"/>
     <server name="AUTH_AUTO_INITIATE" value="false"/>
index 28822dd8ec07abbf8af8513b8c0cdbce925d28a1..e68010170815ee7ae807dfdf758494386a400cc5 100644 (file)
--- a/readme.md
+++ b/readme.md
@@ -9,6 +9,7 @@
 
 [![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/)
 [![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://discord.gg/ztkBqR2)
+[![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://fosstodon.org/@bookstack)
 [![Twitter](https://img.shields.io/static/v1?label=Twitter&message=@bookstack_app&color=1d9bf0&logo=twitter)](https://twitter.com/bookstack_app)
 [![YouTube](https://img.shields.io/static/v1?label=YouTube&message=bookstackapp&color=ff0000&logo=youtube)](https://www.youtube.com/bookstackapp)
 
@@ -133,8 +134,6 @@ Note: This is not an exhaustive list of all libraries and projects that would be
 * [CodeMirror](https://codemirror.net) - _[MIT](https://github.com/codemirror/CodeMirror/blob/master/LICENSE)_
 * [Sortable](https://github.com/SortableJS/Sortable) - _[MIT](https://github.com/SortableJS/Sortable/blob/master/LICENSE)_
 * [Google Material Icons](https://github.com/google/material-design-icons) - _[Apache-2.0](https://github.com/google/material-design-icons/blob/master/LICENSE)_
-* [Dropzone.js](http://www.dropzonejs.com/) - _[MIT](https://github.com/dropzone/dropzone/blob/main/LICENSE)_
-* [clipboard.js](https://clipboardjs.com/) - _[MIT](https://github.com/zenorocha/clipboard.js/blob/master/LICENSE)_
 * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) - _[MIT](https://github.com/markdown-it/markdown-it/blob/master/LICENSE) and [ISC](https://github.com/revin/markdown-it-task-lists/blob/master/LICENSE)_
 * [Dompdf](https://github.com/dompdf/dompdf) - _[LGPL v2.1](https://github.com/dompdf/dompdf/blob/master/LICENSE.LGPL)_
 * [BarryVD/Dompdf](https://github.com/barryvdh/laravel-dompdf) - _[MIT](https://github.com/barryvdh/laravel-dompdf/blob/master/LICENSE)_
index b6acb0988f87fc74e1dd9181a8e22ae25795ebed..2e87f2ca635fc7d8d760b5fb80cf5c79da7ab5fb 100644 (file)
@@ -1,4 +1 @@
-<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
-    <path d="M0 0h24v24H0z" fill="none"/>
-</svg>
\ No newline at end of file
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6 2c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/></svg>
\ No newline at end of file
diff --git a/resources/icons/upload.svg b/resources/icons/upload.svg
new file mode 100644 (file)
index 0000000..c9b0346
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16.59 14H15v5c0 .55-.45 1-1 1h-4c-.55 0-1-.45-1-1v-5H7.41c-.89 0-1.34-1.08-.71-1.71l4.59-4.59a.996.996 0 0 1 1.41 0l4.59 4.59c.63.63.19 1.71-.7 1.71ZM5 4c0-.55.45-1 1-1h12c.55 0 1 .45 1 1s-.45 1-1 1H6c-.55 0-1-.45-1-1Z"/></svg>
\ No newline at end of file
index e49bf5e955bd040d1b68114fb17e8b307a3a024e..5b822e9009e26f58f4695215c4570c9a86bd59de 100644 (file)
@@ -1,34 +1,37 @@
+import * as events from './services/events';
+import * as httpInstance from './services/http';
+import Translations from './services/translations';
+
+import * as components from './services/components';
+import * as componentMap from './components';
+
 // Url retrieval function
-window.baseUrl = function(path) {
+window.baseUrl = function baseUrl(path) {
+    let targetPath = path;
     let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
-    if (basePath[basePath.length-1] === '/') basePath = basePath.slice(0, basePath.length-1);
-    if (path[0] === '/') path = path.slice(1);
-    return basePath + '/' + path;
+    if (basePath[basePath.length - 1] === '/') basePath = basePath.slice(0, basePath.length - 1);
+    if (targetPath[0] === '/') targetPath = targetPath.slice(1);
+    return `${basePath}/${targetPath}`;
 };
 
-window.importVersioned = function(moduleName) {
+window.importVersioned = function importVersioned(moduleName) {
     const version = document.querySelector('link[href*="/dist/styles.css?version="]').href.split('?version=').pop();
     const importPath = window.baseUrl(`dist/${moduleName}.js?version=${version}`);
     return import(importPath);
 };
 
 // Set events and http services on window
-import events from "./services/events"
-import httpInstance from "./services/http"
 window.$http = httpInstance;
 window.$events = events;
 
 // Translation setup
-// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
-import Translations from "./services/translations"
+// Creates a global function with name 'trans' to be used in the same way as the Laravel translation system
 const translator = new Translations();
 window.trans = translator.get.bind(translator);
 window.trans_choice = translator.getPlural.bind(translator);
 window.trans_plural = translator.parsePlural.bind(translator);
 
-// Load Components
-import * as components from "./services/components"
-import * as componentMap from "./components";
+// Load & initialise components
 components.register(componentMap);
 window.$components = components;
 components.init();
diff --git a/resources/js/code.mjs b/resources/js/code.mjs
deleted file mode 100644 (file)
index 9adc23b..0000000
+++ /dev/null
@@ -1,346 +0,0 @@
-import CodeMirror from "codemirror";
-import Clipboard from "clipboard/dist/clipboard.min";
-
-// Modes
-import 'codemirror/mode/css/css';
-import 'codemirror/mode/clike/clike';
-import 'codemirror/mode/dart/dart';
-import 'codemirror/mode/diff/diff';
-import 'codemirror/mode/fortran/fortran';
-import 'codemirror/mode/go/go';
-import 'codemirror/mode/haskell/haskell';
-import 'codemirror/mode/htmlmixed/htmlmixed';
-import 'codemirror/mode/javascript/javascript';
-import 'codemirror/mode/julia/julia';
-import 'codemirror/mode/lua/lua';
-import 'codemirror/mode/markdown/markdown';
-import 'codemirror/mode/mllike/mllike';
-import 'codemirror/mode/nginx/nginx';
-import 'codemirror/mode/octave/octave';
-import 'codemirror/mode/perl/perl';
-import 'codemirror/mode/pascal/pascal';
-import 'codemirror/mode/php/php';
-import 'codemirror/mode/powershell/powershell';
-import 'codemirror/mode/properties/properties';
-import 'codemirror/mode/python/python';
-import 'codemirror/mode/ruby/ruby';
-import 'codemirror/mode/rust/rust';
-import 'codemirror/mode/scheme/scheme';
-import 'codemirror/mode/shell/shell';
-import 'codemirror/mode/smarty/smarty';
-import 'codemirror/mode/sql/sql';
-import 'codemirror/mode/stex/stex';
-import 'codemirror/mode/swift/swift';
-import 'codemirror/mode/toml/toml';
-import 'codemirror/mode/twig/twig';
-import 'codemirror/mode/vb/vb';
-import 'codemirror/mode/vbscript/vbscript';
-import 'codemirror/mode/xml/xml';
-import 'codemirror/mode/yaml/yaml';
-
-// Addons
-import 'codemirror/addon/scroll/scrollpastend';
-
-// Mapping of possible languages or formats from user input to their codemirror modes.
-// Value can be a mode string or a function that will receive the code content & return the mode string.
-// The function option is used in the event the exact mode could be dynamic depending on the code.
-const modeMap = {
-    bash: 'shell',
-    css: 'css',
-    c: 'text/x-csrc',
-    java: 'text/x-java',
-    scala: 'text/x-scala',
-    kotlin: 'text/x-kotlin',
-    'c++': 'text/x-c++src',
-    'c#': 'text/x-csharp',
-    csharp: 'text/x-csharp',
-    dart: 'application/dart',
-    diff: 'diff',
-    for: 'fortran',
-    fortran: 'fortran',
-    'f#': 'text/x-fsharp',
-    fsharp: 'text/x-fsharp',
-    go: 'go',
-    haskell: 'haskell',
-    hs: 'haskell',
-    html: 'htmlmixed',
-    ini: 'properties',
-    javascript: 'text/javascript',
-    json: 'application/json',
-    js: 'text/javascript',
-    jl: 'text/x-julia',
-    julia: 'text/x-julia',
-    latex: 'text/x-stex',
-    lua: 'lua',
-    matlab: 'text/x-octave',
-    md: 'markdown',
-    mdown: 'markdown',
-    markdown: 'markdown',
-    ml: 'mllike',
-    mssql: 'text/x-mssql',
-    mysql: 'text/x-mysql',
-    nginx: 'nginx',
-    octave: 'text/x-octave',
-    perl: 'perl',
-    pl: 'perl',
-    powershell: 'powershell',
-    properties: 'properties',
-    ocaml: 'text/x-ocaml',
-    pascal: 'text/x-pascal',
-    pas: 'text/x-pascal',
-    php: (content) => {
-        return content.includes('<?php') ? 'php' : 'text/x-php';
-    },
-    pgsql: 'text/x-pgsql',
-    'pl/sql': 'text/x-plsql',
-    postgresql: 'text/x-pgsql',
-    py: 'python',
-    python: 'python',
-    ruby: 'ruby',
-    rust: 'rust',
-    rb: 'ruby',
-    rs: 'rust',
-    scheme: 'scheme',
-    shell: 'shell',
-    sh: 'shell',
-    smarty: 'smarty',
-    sql: 'text/x-sql',
-    sqlite: 'text/x-sqlite',
-    stext: 'text/x-stex',
-    swift: 'text/x-swift',
-    toml: 'toml',
-    ts: 'text/typescript',
-    twig: 'twig',
-    typescript: 'text/typescript',
-    vbs: 'vbscript',
-    vbscript: 'vbscript',
-    'vb.net': 'text/x-vb',
-    vbnet: 'text/x-vb',
-    xml: 'xml',
-    yaml: 'yaml',
-    yml: 'yaml',
-};
-
-/**
- * Highlight pre elements on a page
- */
-export function highlight() {
-    const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
-    for (const codeBlock of codeBlocks) {
-        highlightElem(codeBlock);
-    }
-}
-
-/**
- * Highlight all code blocks within the given parent element
- * @param {HTMLElement} parent
- */
-export function highlightWithin(parent) {
-    const codeBlocks = parent.querySelectorAll('pre');
-    for (const codeBlock of codeBlocks) {
-        highlightElem(codeBlock);
-    }
-}
-
-/**
- * Add code highlighting to a single element.
- * @param {HTMLElement} elem
- */
-function highlightElem(elem) {
-    const innerCodeElem = elem.querySelector('code[class^=language-]');
-    elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
-    const content = elem.textContent.trimEnd();
-
-    let mode = '';
-    if (innerCodeElem !== null) {
-        const langName = innerCodeElem.className.replace('language-', '');
-        mode = getMode(langName, content);
-    }
-
-    const cm = CodeMirror(function(elt) {
-        elem.parentNode.replaceChild(elt, elem);
-    }, {
-        value: content,
-        mode:  mode,
-        lineNumbers: true,
-        lineWrapping: false,
-        theme: getTheme(),
-        readOnly: true
-    });
-
-    addCopyIcon(cm);
-}
-
-/**
- * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
- * @param cmInstance
- */
-function addCopyIcon(cmInstance) {
-    const copyIcon = `<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
-    const copyButton = document.createElement('div');
-    copyButton.classList.add('CodeMirror-copy');
-    copyButton.innerHTML = copyIcon;
-    cmInstance.display.wrapper.appendChild(copyButton);
-
-    const clipboard = new Clipboard(copyButton, {
-        text: function(trigger) {
-            return cmInstance.getValue()
-        }
-    });
-
-    clipboard.on('success', event => {
-        copyButton.classList.add('success');
-        setTimeout(() => {
-            copyButton.classList.remove('success');
-        }, 240);
-    });
-}
-
-/**
- * Search for a codemirror code based off a user suggestion
- * @param {String} suggestion
- * @param {String} content
- * @returns {string}
- */
-function getMode(suggestion, content) {
-    suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase();
-
-    const modeMapType = typeof modeMap[suggestion];
-
-    if (modeMapType === 'undefined') {
-        return '';
-    }
-
-    if (modeMapType === 'function') {
-        return modeMap[suggestion](content);
-    }
-
-    return modeMap[suggestion];
-}
-
-/**
- * Ge the theme to use for CodeMirror instances.
- * @returns {*|string}
- */
-function getTheme() {
-    const darkMode = document.documentElement.classList.contains('dark-mode');
-    return window.codeTheme || (darkMode ? 'darcula' : 'default');
-}
-
-/**
- * Create a CodeMirror instance for showing inside the WYSIWYG editor.
- *  Manages a textarea element to hold code content.
- * @param {HTMLElement} cmContainer
- * @param {String} content
- * @param {String} language
- * @returns {{wrap: Element, editor: *}}
- */
-export function wysiwygView(cmContainer, content, language) {
-    return CodeMirror(cmContainer, {
-        value: content,
-        mode: getMode(language, content),
-        lineNumbers: true,
-        lineWrapping: false,
-        theme: getTheme(),
-        readOnly: true
-    });
-}
-
-
-/**
- * Create a CodeMirror instance to show in the WYSIWYG pop-up editor
- * @param {HTMLElement} elem
- * @param {String} modeSuggestion
- * @returns {*}
- */
-export function popupEditor(elem, modeSuggestion) {
-    const content = elem.textContent;
-
-    return CodeMirror(function(elt) {
-        elem.parentNode.insertBefore(elt, elem);
-        elem.style.display = 'none';
-    }, {
-        value: content,
-        mode:  getMode(modeSuggestion, content),
-        lineNumbers: true,
-        lineWrapping: false,
-        theme: getTheme()
-    });
-}
-
-/**
- * Create an inline editor to replace the given textarea.
- * @param {HTMLTextAreaElement} textArea
- * @param {String} mode
- * @returns {CodeMirror3}
- */
-export function inlineEditor(textArea, mode) {
-    return CodeMirror.fromTextArea(textArea, {
-        mode: getMode(mode, textArea.value),
-        lineNumbers: true,
-        lineWrapping: false,
-        theme: getTheme(),
-    });
-}
-
-/**
- * Set the mode of a codemirror instance.
- * @param cmInstance
- * @param modeSuggestion
- */
-export function setMode(cmInstance, modeSuggestion, content) {
-      cmInstance.setOption('mode', getMode(modeSuggestion, content));
-}
-
-/**
- * Set the content of a cm instance.
- * @param cmInstance
- * @param codeContent
- */
-export function setContent(cmInstance, codeContent) {
-    cmInstance.setValue(codeContent);
-    setTimeout(() => {
-        updateLayout(cmInstance);
-    }, 10);
-}
-
-/**
- * Update the layout (codemirror refresh) of a cm instance.
- * @param cmInstance
- */
-export function updateLayout(cmInstance) {
-    cmInstance.refresh();
-}
-
-/**
- * Get a CodeMirror instance to use for the markdown editor.
- * @param {HTMLElement} elem
- * @returns {*}
- */
-export function markdownEditor(elem) {
-    const content = elem.textContent;
-    const config = {
-        value: content,
-        mode: "markdown",
-        lineNumbers: true,
-        lineWrapping: true,
-        theme: getTheme(),
-        scrollPastEnd: true,
-    };
-
-    window.$events.emitPublic(elem, 'editor-markdown-cm::pre-init', {config});
-
-    return CodeMirror(function (elt) {
-        elem.parentNode.insertBefore(elt, elem);
-        elem.style.display = 'none';
-    }, config);
-}
-
-/**
- * Get the 'meta' key dependent on the user's system.
- * @returns {string}
- */
-export function getMetaKey() {
-    let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
-    return mac ? "Cmd" : "Ctrl";
-}
\ No newline at end of file
diff --git a/resources/js/code/index.mjs b/resources/js/code/index.mjs
new file mode 100644 (file)
index 0000000..e51472d
--- /dev/null
@@ -0,0 +1,206 @@
+import {EditorView, keymap} from '@codemirror/view';
+
+import {copyTextToClipboard} from '../services/clipboard';
+import {viewerExtensions, editorExtensions} from './setups';
+import {createView} from './views';
+import {SimpleEditorInterface} from './simple-editor-interface';
+
+/**
+ * Add a button to a CodeMirror instance which copies the contents to the clipboard upon click.
+ * @param {EditorView} editorView
+ */
+function addCopyIcon(editorView) {
+    const copyIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>';
+    const checkIcon = '<svg viewBox="0 0 24 24" width="16" height="16" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
+    const copyButton = document.createElement('button');
+    copyButton.setAttribute('type', 'button');
+    copyButton.classList.add('cm-copy-button');
+    copyButton.innerHTML = copyIcon;
+    editorView.dom.appendChild(copyButton);
+
+    const notifyTime = 620;
+    const transitionTime = 60;
+    copyButton.addEventListener('click', () => {
+        copyTextToClipboard(editorView.state.doc.toString());
+        copyButton.classList.add('success');
+
+        setTimeout(() => {
+            copyButton.innerHTML = checkIcon;
+        }, transitionTime / 2);
+
+        setTimeout(() => {
+            copyButton.classList.remove('success');
+        }, notifyTime);
+
+        setTimeout(() => {
+            copyButton.innerHTML = copyIcon;
+        }, notifyTime + (transitionTime / 2));
+    });
+}
+
+/**
+ * Add code highlighting to a single element.
+ * @param {HTMLElement} elem
+ */
+function highlightElem(elem) {
+    const innerCodeElem = elem.querySelector('code[class^=language-]');
+    elem.innerHTML = elem.innerHTML.replace(/<br\s*\/?>/gi, '\n');
+    const content = elem.textContent.trimEnd();
+
+    let langName = '';
+    if (innerCodeElem !== null) {
+        langName = innerCodeElem.className.replace('language-', '');
+    }
+
+    const wrapper = document.createElement('div');
+    elem.parentNode.insertBefore(wrapper, elem);
+
+    const ev = createView({
+        parent: wrapper,
+        doc: content,
+        extensions: viewerExtensions(wrapper),
+    });
+
+    const editor = new SimpleEditorInterface(ev);
+    editor.setMode(langName, content);
+
+    elem.remove();
+    addCopyIcon(ev);
+}
+
+/**
+ * Highlight all code blocks within the given parent element
+ * @param {HTMLElement} parent
+ */
+export function highlightWithin(parent) {
+    const codeBlocks = parent.querySelectorAll('pre');
+    for (const codeBlock of codeBlocks) {
+        highlightElem(codeBlock);
+    }
+}
+
+/**
+ * Highlight pre elements on a page
+ */
+export function highlight() {
+    const codeBlocks = document.querySelectorAll('.page-content pre, .comment-box .content pre');
+    for (const codeBlock of codeBlocks) {
+        highlightElem(codeBlock);
+    }
+}
+
+/**
+ * Create a CodeMirror instance for showing inside the WYSIWYG editor.
+ * Manages a textarea element to hold code content.
+ * @param {HTMLElement} cmContainer
+ * @param {ShadowRoot} shadowRoot
+ * @param {String} content
+ * @param {String} language
+ * @returns {SimpleEditorInterface}
+ */
+export function wysiwygView(cmContainer, shadowRoot, content, language) {
+    const ev = createView({
+        parent: cmContainer,
+        doc: content,
+        extensions: viewerExtensions(cmContainer),
+        root: shadowRoot,
+    });
+
+    const editor = new SimpleEditorInterface(ev);
+    editor.setMode(language, content);
+
+    return editor;
+}
+
+/**
+ * Create a CodeMirror instance to show in the WYSIWYG pop-up editor
+ * @param {HTMLElement} elem
+ * @param {String} modeSuggestion
+ * @returns {SimpleEditorInterface}
+ */
+export function popupEditor(elem, modeSuggestion) {
+    const content = elem.textContent;
+    const config = {
+        parent: elem.parentElement,
+        doc: content,
+        extensions: [
+            ...editorExtensions(elem.parentElement),
+            EditorView.updateListener.of(v => {
+                if (v.docChanged) {
+                    // textArea.value = v.state.doc.toString();
+                }
+            }),
+        ],
+    };
+
+    // Create editor, hide original input
+    const editor = new SimpleEditorInterface(createView(config));
+    editor.setMode(modeSuggestion, content);
+    elem.style.display = 'none';
+
+    return editor;
+}
+
+/**
+ * Create an inline editor to replace the given textarea.
+ * @param {HTMLTextAreaElement} textArea
+ * @param {String} mode
+ * @returns {SimpleEditorInterface}
+ */
+export function inlineEditor(textArea, mode) {
+    const content = textArea.value;
+    const config = {
+        parent: textArea.parentElement,
+        doc: content,
+        extensions: [
+            ...editorExtensions(textArea.parentElement),
+            EditorView.updateListener.of(v => {
+                if (v.docChanged) {
+                    textArea.value = v.state.doc.toString();
+                }
+            }),
+        ],
+    };
+
+    // Create editor view, hide original input
+    const ev = createView(config);
+    const editor = new SimpleEditorInterface(ev);
+    editor.setMode(mode, content);
+    textArea.style.display = 'none';
+
+    return editor;
+}
+
+/**
+ * Get a CodeMirror instance to use for the markdown editor.
+ * @param {HTMLElement} elem
+ * @param {function} onChange
+ * @param {object} domEventHandlers
+ * @param {Array} keyBindings
+ * @returns {EditorView}
+ */
+export function markdownEditor(elem, onChange, domEventHandlers, keyBindings) {
+    const content = elem.textContent;
+    const config = {
+        parent: elem.parentElement,
+        doc: content,
+        extensions: [
+            keymap.of(keyBindings),
+            ...editorExtensions(elem.parentElement),
+            EditorView.updateListener.of(v => {
+                onChange(v);
+            }),
+            EditorView.domEventHandlers(domEventHandlers),
+        ],
+    };
+
+    // Emit a pre-event public event to allow tweaking of the configure before view creation.
+    window.$events.emitPublic(elem, 'editor-markdown-cm6::pre-init', {editorViewConfig: config});
+
+    // Create editor view, hide original input
+    const ev = createView(config);
+    (new SimpleEditorInterface(ev)).setMode('markdown', '');
+    elem.style.display = 'none';
+
+    return ev;
+}
diff --git a/resources/js/code/languages.js b/resources/js/code/languages.js
new file mode 100644 (file)
index 0000000..c0f9e6d
--- /dev/null
@@ -0,0 +1,117 @@
+import {StreamLanguage} from '@codemirror/language';
+
+import {css} from '@codemirror/lang-css';
+import {json} from '@codemirror/lang-json';
+import {javascript} from '@codemirror/lang-javascript';
+import {html} from '@codemirror/lang-html';
+import {markdown} from '@codemirror/lang-markdown';
+import {php} from '@codemirror/lang-php';
+import {twig} from '@ssddanbrown/codemirror-lang-twig';
+import {xml} from '@codemirror/lang-xml';
+
+const legacyLoad = async mode => {
+    const modes = await window.importVersioned('legacy-modes');
+    return StreamLanguage.define(modes[mode]);
+};
+
+// Mapping of possible languages or formats from user input to their codemirror modes.
+// Value can be a mode string or a function that will receive the code content & return the mode string.
+// The function option is used in the event the exact mode could be dynamic depending on the code.
+const modeMap = {
+    bash: () => legacyLoad('shell'),
+    c: () => legacyLoad('c'),
+    css: async () => css(),
+    'c++': () => legacyLoad('cpp'),
+    'c#': () => legacyLoad('csharp'),
+    clj: () => legacyLoad('clojure'),
+    clojure: () => legacyLoad('clojure'),
+    csharp: () => legacyLoad('csharp'),
+    dart: () => legacyLoad('dart'),
+    diff: () => legacyLoad('diff'),
+    for: () => legacyLoad('fortran'),
+    fortran: () => legacyLoad('fortran'),
+    'f#': () => legacyLoad('fSharp'),
+    fsharp: () => legacyLoad('fSharp'),
+    go: () => legacyLoad('go'),
+    haskell: () => legacyLoad('haskell'),
+    hs: () => legacyLoad('haskell'),
+    html: async () => html(),
+    ini: () => legacyLoad('properties'),
+    java: () => legacyLoad('java'),
+    javascript: async () => javascript(),
+    json: async () => json(),
+    js: async () => javascript(),
+    jl: () => legacyLoad('julia'),
+    julia: () => legacyLoad('julia'),
+    kotlin: () => legacyLoad('kotlin'),
+    latex: () => legacyLoad('stex'),
+    lua: () => legacyLoad('lua'),
+    markdown: async () => markdown(),
+    matlab: () => legacyLoad('octave'),
+    md: async () => markdown(),
+    mdown: async () => markdown(),
+    ml: () => legacyLoad('sml'),
+    mssql: () => legacyLoad('msSQL'),
+    mysql: () => legacyLoad('mySQL'),
+    nginx: () => legacyLoad('nginx'),
+    octave: () => legacyLoad('octave'),
+    pas: () => legacyLoad('pascal'),
+    pascal: () => legacyLoad('pascal'),
+    perl: () => legacyLoad('perl'),
+    pgsql: () => legacyLoad('pgSQL'),
+    php: async code => {
+        const hasTags = code.includes('<?php');
+        return php({plain: !hasTags});
+    },
+    pl: () => legacyLoad('perl'),
+    'pl/sql': () => legacyLoad('plSQL'),
+    postgresql: () => legacyLoad('pgSQL'),
+    powershell: () => legacyLoad('powerShell'),
+    properties: () => legacyLoad('properties'),
+    ocaml: () => legacyLoad('oCaml'),
+    py: () => legacyLoad('python'),
+    python: () => legacyLoad('python'),
+    rb: () => legacyLoad('ruby'),
+    rs: () => legacyLoad('rust'),
+    ruby: () => legacyLoad('ruby'),
+    rust: () => legacyLoad('rust'),
+    scala: () => legacyLoad('scala'),
+    scheme: () => legacyLoad('scheme'),
+    shell: () => legacyLoad('shell'),
+    sh: () => legacyLoad('shell'),
+    smarty: () => legacyLoad('smarty'),
+    stext: () => legacyLoad('stex'),
+    swift: () => legacyLoad('swift'),
+    toml: () => legacyLoad('toml'),
+    ts: async () => javascript({typescript: true}),
+    twig: async () => twig(),
+    typescript: async () => javascript({typescript: true}),
+    sql: () => legacyLoad('standardSQL'),
+    sqlite: () => legacyLoad('sqlite'),
+    vbs: () => legacyLoad('vbScript'),
+    vbscript: () => legacyLoad('vbScript'),
+    'vb.net': () => legacyLoad('vb'),
+    vbnet: () => legacyLoad('vb'),
+    xml: async () => xml(),
+    yaml: () => legacyLoad('yaml'),
+    yml: () => legacyLoad('yaml'),
+};
+
+/**
+ * Get the relevant codemirror language extension based upon the given language
+ * suggestion and content.
+ * @param {String} langSuggestion
+ * @param {String} content
+ * @returns {Promise<StreamLanguage|LanguageSupport>}
+ */
+export function getLanguageExtension(langSuggestion, content) {
+    const suggestion = langSuggestion.trim().replace(/^\./g, '').toLowerCase();
+
+    const language = modeMap[suggestion];
+
+    if (typeof language === 'undefined') {
+        return undefined;
+    }
+
+    return language(content);
+}
diff --git a/resources/js/code/legacy-modes.mjs b/resources/js/code/legacy-modes.mjs
new file mode 100644 (file)
index 0000000..55a804b
--- /dev/null
@@ -0,0 +1,32 @@
+export {
+    c, cpp, csharp, java, kotlin, scala, dart,
+} from '@codemirror/legacy-modes/mode/clike';
+export {clojure} from '@codemirror/legacy-modes/mode/clojure';
+export {diff} from '@codemirror/legacy-modes/mode/diff';
+export {fortran} from '@codemirror/legacy-modes/mode/fortran';
+export {go} from '@codemirror/legacy-modes/mode/go';
+export {haskell} from '@codemirror/legacy-modes/mode/haskell';
+export {julia} from '@codemirror/legacy-modes/mode/julia';
+export {lua} from '@codemirror/legacy-modes/mode/lua';
+export {oCaml, fSharp, sml} from '@codemirror/legacy-modes/mode/mllike';
+export {nginx} from '@codemirror/legacy-modes/mode/nginx';
+export {octave} from '@codemirror/legacy-modes/mode/octave';
+export {perl} from '@codemirror/legacy-modes/mode/perl';
+export {pascal} from '@codemirror/legacy-modes/mode/pascal';
+export {powerShell} from '@codemirror/legacy-modes/mode/powershell';
+export {properties} from '@codemirror/legacy-modes/mode/properties';
+export {python} from '@codemirror/legacy-modes/mode/python';
+export {ruby} from '@codemirror/legacy-modes/mode/ruby';
+export {rust} from '@codemirror/legacy-modes/mode/rust';
+export {scheme} from '@codemirror/legacy-modes/mode/scheme';
+export {shell} from '@codemirror/legacy-modes/mode/shell';
+export {
+    standardSQL, pgSQL, msSQL, mySQL, sqlite, plSQL,
+} from '@codemirror/legacy-modes/mode/sql';
+export {stex} from '@codemirror/legacy-modes/mode/stex';
+export {swift} from '@codemirror/legacy-modes/mode/swift';
+export {toml} from '@codemirror/legacy-modes/mode/toml';
+export {vb} from '@codemirror/legacy-modes/mode/vb';
+export {vbScript} from '@codemirror/legacy-modes/mode/vbscript';
+export {yaml} from '@codemirror/legacy-modes/mode/yaml';
+export {smarty} from '@ssddanbrown/codemirror-lang-smarty';
diff --git a/resources/js/code/setups.js b/resources/js/code/setups.js
new file mode 100644 (file)
index 0000000..52b9cc1
--- /dev/null
@@ -0,0 +1,58 @@
+import {
+    EditorView, keymap, drawSelection, highlightActiveLine, dropCursor,
+    rectangularSelection, lineNumbers, highlightActiveLineGutter,
+} from '@codemirror/view';
+import {bracketMatching} from '@codemirror/language';
+import {
+    defaultKeymap, history, historyKeymap, indentWithTab,
+} from '@codemirror/commands';
+import {EditorState} from '@codemirror/state';
+import {getTheme} from './themes';
+
+/**
+ * @param {Element} parentEl
+ * @return {(Extension[]|{extension: Extension}|readonly Extension[])[]}
+ */
+function common(parentEl) {
+    return [
+        getTheme(parentEl),
+        lineNumbers(),
+        highlightActiveLineGutter(),
+        drawSelection(),
+        dropCursor(),
+        bracketMatching(),
+        rectangularSelection(),
+        highlightActiveLine(),
+    ];
+}
+
+/**
+ * @param {Element} parentEl
+ * @return {*[]}
+ */
+export function viewerExtensions(parentEl) {
+    return [
+        ...common(parentEl),
+        keymap.of([
+            ...defaultKeymap,
+        ]),
+        EditorState.readOnly.of(true),
+    ];
+}
+
+/**
+ * @param {Element} parentEl
+ * @return {*[]}
+ */
+export function editorExtensions(parentEl) {
+    return [
+        ...common(parentEl),
+        history(),
+        keymap.of([
+            ...defaultKeymap,
+            ...historyKeymap,
+            indentWithTab,
+        ]),
+        EditorView.lineWrapping,
+    ];
+}
diff --git a/resources/js/code/simple-editor-interface.js b/resources/js/code/simple-editor-interface.js
new file mode 100644 (file)
index 0000000..63456cd
--- /dev/null
@@ -0,0 +1,47 @@
+import {updateViewLanguage} from './views';
+
+export class SimpleEditorInterface {
+
+    /**
+     * @param {EditorView} editorView
+     */
+    constructor(editorView) {
+        this.ev = editorView;
+    }
+
+    /**
+     * Get the contents of an editor instance.
+     * @return {string}
+     */
+    getContent() {
+        return this.ev.state.doc.toString();
+    }
+
+    /**
+     * Set the contents of an editor instance.
+     * @param content
+     */
+    setContent(content) {
+        const {doc} = this.ev.state;
+        this.ev.dispatch({
+            changes: {from: 0, to: doc.length, insert: content},
+        });
+    }
+
+    /**
+     * Return focus to the editor instance.
+     */
+    focus() {
+        this.ev.focus();
+    }
+
+    /**
+     * Set the language mode of the editor instance.
+     * @param {String} mode
+     * @param {String} content
+     */
+    setMode(mode, content = '') {
+        updateViewLanguage(this.ev, mode, content);
+    }
+
+}
diff --git a/resources/js/code/themes.js b/resources/js/code/themes.js
new file mode 100644 (file)
index 0000000..b3635bd
--- /dev/null
@@ -0,0 +1,131 @@
+import {tags} from '@lezer/highlight';
+import {HighlightStyle, syntaxHighlighting} from '@codemirror/language';
+import {EditorView} from '@codemirror/view';
+import {oneDarkHighlightStyle, oneDarkTheme} from '@codemirror/theme-one-dark';
+
+const defaultLightHighlightStyle = HighlightStyle.define([
+    {
+        tag: tags.meta,
+        color: '#388938',
+    },
+    {
+        tag: tags.link,
+        textDecoration: 'underline',
+    },
+    {
+        tag: tags.heading,
+        textDecoration: 'underline',
+        fontWeight: 'bold',
+    },
+    {
+        tag: tags.emphasis,
+        fontStyle: 'italic',
+    },
+    {
+        tag: tags.strong,
+        fontWeight: 'bold',
+    },
+    {
+        tag: tags.strikethrough,
+        textDecoration: 'line-through',
+    },
+    {
+        tag: tags.keyword,
+        color: '#708',
+    },
+    {
+        tag: [tags.atom, tags.bool, tags.url, tags.contentSeparator, tags.labelName],
+        color: '#219',
+    },
+    {
+        tag: [tags.literal, tags.inserted],
+        color: '#164',
+    },
+    {
+        tag: [tags.string, tags.deleted],
+        color: '#a11',
+    },
+    {
+        tag: [tags.regexp, tags.escape, tags.special(tags.string)],
+        color: '#e40',
+    },
+    {
+        tag: tags.definition(tags.variableName),
+        color: '#00f',
+    },
+    {
+        tag: tags.local(tags.variableName),
+        color: '#30a',
+    },
+    {
+        tag: [tags.typeName, tags.namespace],
+        color: '#085',
+    },
+    {
+        tag: tags.className,
+        color: '#167',
+    },
+    {
+        tag: [tags.special(tags.variableName), tags.macroName],
+        color: '#256',
+    },
+    {
+        tag: tags.definition(tags.propertyName),
+        color: '#00c',
+    },
+    {
+        tag: tags.compareOperator,
+        color: '#708',
+    },
+    {
+        tag: tags.comment,
+        color: '#940',
+    },
+    {
+        tag: tags.invalid,
+        color: '#f00',
+    },
+]);
+
+const defaultThemeSpec = {
+    '&': {
+        backgroundColor: '#FFF',
+        color: '#000',
+    },
+    '&.cm-focused': {
+        outline: 'none',
+    },
+    '.cm-line': {
+        lineHeight: '1.6',
+    },
+};
+
+/**
+ * Get the theme extension to use for editor view instance.
+ * @returns {Extension[]}
+ */
+export function getTheme(viewParentEl) {
+    const darkMode = document.documentElement.classList.contains('dark-mode');
+    let viewTheme = darkMode ? oneDarkTheme : EditorView.theme(defaultThemeSpec);
+    let highlightStyle = darkMode ? oneDarkHighlightStyle : defaultLightHighlightStyle;
+
+    const eventData = {
+        darkModeActive: darkMode,
+        registerViewTheme(builder) {
+            const spec = builder();
+            if (spec) {
+                viewTheme = EditorView.theme(spec);
+            }
+        },
+        registerHighlightStyle(builder) {
+            const tagStyles = builder(tags) || [];
+            if (tagStyles.length) {
+                highlightStyle = HighlightStyle.define(tagStyles);
+            }
+        },
+    };
+
+    window.$events.emitPublic(viewParentEl, 'library-cm6::configure-theme', eventData);
+
+    return [viewTheme, syntaxHighlighting(highlightStyle)];
+}
diff --git a/resources/js/code/views.js b/resources/js/code/views.js
new file mode 100644 (file)
index 0000000..12148ca
--- /dev/null
@@ -0,0 +1,38 @@
+import {Compartment} from '@codemirror/state';
+import {EditorView} from '@codemirror/view';
+import {getLanguageExtension} from './languages';
+
+const viewLangCompartments = new WeakMap();
+
+/**
+ * Create a new editor view.
+ *
+ * @param {{parent: Element, doc: String, extensions: Array}} config
+ * @returns {EditorView}
+ */
+export function createView(config) {
+    const langCompartment = new Compartment();
+    config.extensions.push(langCompartment.of([]));
+
+    const ev = new EditorView(config);
+
+    viewLangCompartments.set(ev, langCompartment);
+
+    return ev;
+}
+
+/**
+ * Set the language mode of an EditorView.
+ *
+ * @param {EditorView} ev
+ * @param {string} modeSuggestion
+ * @param {string} content
+ */
+export async function updateViewLanguage(ev, modeSuggestion, content) {
+    const compartment = viewLangCompartments.get(ev);
+    const language = await getLanguageExtension(modeSuggestion, content);
+
+    ev.dispatch({
+        effects: compartment.reconfigure(language || []),
+    });
+}
index 19d2249fb28ece1607f00367e7e443b75ea8f8d5..3213c4835aa45fd5bad209e34b4b5a9e6b44967c 100644 (file)
@@ -1,6 +1,6 @@
-import {onChildEvent} from "../services/dom";
-import {uniqueId} from "../services/util";
-import {Component} from "./component";
+import {onChildEvent} from '../services/dom';
+import {uniqueId} from '../services/util';
+import {Component} from './component';
 
 /**
  * AddRemoveRows
@@ -8,6 +8,7 @@ import {Component} from "./component";
  * Needs a model row to use when adding a new row.
  */
 export class AddRemoveRows extends Component {
+
     setup() {
         this.modelRow = this.$refs.model;
         this.addButton = this.$refs.add;
@@ -19,7 +20,7 @@ export class AddRemoveRows extends Component {
     setupListeners() {
         this.addButton.addEventListener('click', this.add.bind(this));
 
-        onChildEvent(this.$el, this.removeSelector, 'click', (e) => {
+        onChildEvent(this.$el, this.removeSelector, 'click', e => {
             const row = e.target.closest(this.rowSelector);
             row.remove();
         });
@@ -44,9 +45,10 @@ export class AddRemoveRows extends Component {
      */
     setClonedInputNames(clone) {
         const rowId = uniqueId();
-        const randRowIdElems = clone.querySelectorAll(`[name*="randrowid"]`);
+        const randRowIdElems = clone.querySelectorAll('[name*="randrowid"]');
         for (const elem of randRowIdElems) {
             elem.name = elem.name.split('randrowid').join(rowId);
         }
     }
-}
\ No newline at end of file
+
+}
index f1af7f6cb10e97d2e65cfb1c4e49d1d5a4e7580e..aa2801f19e666c52ee9f1d738b71478ec105552a 100644 (file)
@@ -1,7 +1,8 @@
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 export class AjaxDeleteRow extends Component {
+
     setup() {
         this.row = this.$el;
         this.url = this.$opts.url;
@@ -19,9 +20,10 @@ export class AjaxDeleteRow extends Component {
                 window.$events.emit('success', resp.data.message);
             }
             this.row.remove();
-        }).catch(err => {
+        }).catch(() => {
             this.row.style.opacity = null;
             this.row.style.pointerEvents = null;
         });
     }
-}
\ No newline at end of file
+
+}
index 6f4e5af08c8bfbe20483ea0805cf4a44261c3861..583dde5724424defb44906504ab4973a2fb88af5 100644 (file)
@@ -1,5 +1,5 @@
-import {onEnterPress, onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onEnterPress, onSelect} from '../services/dom';
+import {Component} from './component';
 
 /**
  * Ajax Form
@@ -11,6 +11,7 @@ import {Component} from "./component";
  * otherwise will act as a fake form element.
  */
 export class AjaxForm extends Component {
+
     setup() {
         this.container = this.$el;
         this.responseContainer = this.container;
@@ -27,7 +28,6 @@ export class AjaxForm extends Component {
     }
 
     setupListeners() {
-
         if (this.container.tagName === 'FORM') {
             this.container.addEventListener('submit', this.submitRealForm.bind(this));
             return;
@@ -43,7 +43,7 @@ export class AjaxForm extends Component {
 
     submitFakeForm() {
         const fd = new FormData();
-        const inputs = this.container.querySelectorAll(`[name]`);
+        const inputs = this.container.querySelectorAll('[name]');
         for (const input of inputs) {
             fd.append(input.getAttribute('name'), input.value);
         }
@@ -76,4 +76,4 @@ export class AjaxForm extends Component {
         this.responseContainer.style.pointerEvents = null;
     }
 
-}
\ No newline at end of file
+}
index dfefd9b7f84afb24c1cdb77dd8dbe22281dc1533..4db09977fec7ff0d20bf10192b4b59ca3caad1aa 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 /**
  * Attachments List
@@ -13,11 +13,11 @@ export class AttachmentsList extends Component {
     }
 
     setupListeners() {
-        const isExpectedKey = (event) => event.key === 'Control' || event.key === 'Meta';
+        const isExpectedKey = event => event.key === 'Control' || event.key === 'Meta';
         window.addEventListener('keydown', event => {
-             if (isExpectedKey(event)) {
+            if (isExpectedKey(event)) {
                 this.addOpenQueryToLinks();
-             }
+            }
         }, {passive: true});
         window.addEventListener('keyup', event => {
             if (isExpectedKey(event)) {
@@ -30,7 +30,7 @@ export class AttachmentsList extends Component {
         const links = this.container.querySelectorAll('a.attachment-file');
         for (const link of links) {
             if (link.href.split('?')[1] !== 'open=true') {
-                link.href = link.href + '?open=true';
+                link.href += '?open=true';
                 link.setAttribute('target', '_blank');
             }
         }
@@ -43,4 +43,5 @@ export class AttachmentsList extends Component {
             link.removeAttribute('target');
         }
     }
-}
\ No newline at end of file
+
+}
index d8a506270dfcb3c8764364db961d921a9300c24a..f45b25e36d05f92d96d175e8b5207020bdcd4d14 100644 (file)
@@ -1,5 +1,5 @@
-import {showLoading} from "../services/dom";
-import {Component} from "./component";
+import {showLoading} from '../services/dom';
+import {Component} from './component';
 
 export class Attachments extends Component {
 
@@ -8,15 +8,16 @@ export class Attachments extends Component {
         this.pageId = this.$opts.pageId;
         this.editContainer = this.$refs.editContainer;
         this.listContainer = this.$refs.listContainer;
-        this.mainTabs = this.$refs.mainTabs;
-        this.list = this.$refs.list;
+        this.linksContainer = this.$refs.linksContainer;
+        this.listPanel = this.$refs.listPanel;
+        this.attachLinkButton = this.$refs.attachLinkButton;
 
         this.setupListeners();
     }
 
     setupListeners() {
         const reloadListBound = this.reloadList.bind(this);
-        this.container.addEventListener('dropzone-success', reloadListBound);
+        this.container.addEventListener('dropzone-upload-success', reloadListBound);
         this.container.addEventListener('ajax-form-success', reloadListBound);
 
         this.container.addEventListener('sortable-list-sort', event => {
@@ -27,7 +28,7 @@ export class Attachments extends Component {
             this.startEdit(event.detail.id);
         });
 
-        this.container.addEventListener('event-emit-select-edit-back', event => {
+        this.container.addEventListener('event-emit-select-edit-back', () => {
             this.stopEdit();
         });
 
@@ -39,16 +40,29 @@ export class Attachments extends Component {
                 markdown: contentTypes['text/plain'],
             });
         });
+
+        this.attachLinkButton.addEventListener('click', () => {
+            this.showSection('links');
+        });
+    }
+
+    showSection(section) {
+        const sectionMap = {
+            links: this.linksContainer,
+            edit: this.editContainer,
+            list: this.listContainer,
+        };
+
+        for (const [name, elem] of Object.entries(sectionMap)) {
+            elem.toggleAttribute('hidden', name !== section);
+        }
     }
 
     reloadList() {
         this.stopEdit();
-        /** @var {Tabs} */
-        const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
-        tabs.show('attachment-panel-items');
         window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
-            this.list.innerHTML = resp.data;
-            window.$components.init(this.list);
+            this.listPanel.innerHTML = resp.data;
+            window.$components.init(this.listPanel);
         });
     }
 
@@ -59,8 +73,7 @@ export class Attachments extends Component {
     }
 
     async startEdit(id) {
-        this.editContainer.classList.remove('hidden');
-        this.listContainer.classList.add('hidden');
+        this.showSection('edit');
 
         showLoading(this.editContainer);
         const resp = await window.$http.get(`/attachments/edit/${id}`);
@@ -69,8 +82,7 @@ export class Attachments extends Component {
     }
 
     stopEdit() {
-        this.editContainer.classList.add('hidden');
-        this.listContainer.classList.remove('hidden');
+        this.showSection('list');
     }
 
-}
\ No newline at end of file
+}
index c8726ca7e2914d5099b38f538664da7f11c44592..c78ef55492a9ae60138b65f809875f57cadc2a05 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class AutoSubmit extends Component {
 
@@ -8,4 +8,4 @@ export class AutoSubmit extends Component {
         this.form.submit();
     }
 
-}
\ No newline at end of file
+}
index 2ebf59f5dbf5ae08112d27384f96728b44fb3250..92a6c6af3f43fde8ff5288c460f92c76eab31650 100644 (file)
@@ -1,7 +1,7 @@
-import {escapeHtml} from "../services/util";
-import {onChildEvent} from "../services/dom";
-import {Component} from "./component";
-import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
+import {escapeHtml} from '../services/util';
+import {onChildEvent} from '../services/dom';
+import {Component} from './component';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
 
 const ajaxCache = {};
 
@@ -9,6 +9,7 @@ const ajaxCache = {};
  * AutoSuggest
  */
 export class AutoSuggest extends Component {
+
     setup() {
         this.parent = this.$el.parentElement;
         this.container = this.$el;
@@ -24,7 +25,7 @@ export class AutoSuggest extends Component {
     setupListeners() {
         const navHandler = new KeyboardNavigationHandler(
             this.list,
-            event => {
+            () => {
                 this.input.focus();
                 setTimeout(() => this.hideSuggestions(), 1);
             },
@@ -67,9 +68,7 @@ export class AutoSuggest extends Component {
         const search = this.input.value.toLowerCase();
         const suggestions = await this.loadSuggestions(search, nameFilter);
 
-        const toShow = suggestions.filter(val => {
-            return search === '' || val.toLowerCase().startsWith(search);
-        }).slice(0, 10);
+        const toShow = suggestions.filter(val => search === '' || val.toLowerCase().startsWith(search)).slice(0, 10);
 
         this.displaySuggestions(toShow);
     }
@@ -105,7 +104,8 @@ export class AutoSuggest extends Component {
      */
     displaySuggestions(suggestions) {
         if (suggestions.length === 0) {
-            return this.hideSuggestions();
+            this.hideSuggestions();
+            return;
         }
 
         // This used to use <button>s but was changed to div elements since Safari would not focus on buttons
@@ -126,4 +126,5 @@ export class AutoSuggest extends Component {
             this.hideSuggestions();
         }
     }
-}
\ No newline at end of file
+
+}
index 4f0a46f009b19822332bd680f766bc205acc43b9..046e640d10a06175d9bcb064574d038785991282 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class BackToTop extends Component {
 
@@ -18,7 +18,7 @@ export class BackToTop extends Component {
     }
 
     onPageScroll() {
-        let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
+        const scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
         if (!this.showing && scrollTopPos > this.breakPoint) {
             this.button.style.display = 'block';
             this.showing = true;
@@ -35,15 +35,15 @@ export class BackToTop extends Component {
     }
 
     scrollToTop() {
-        let targetTop = this.targetElem.getBoundingClientRect().top;
-        let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
-        let duration = 300;
-        let start = Date.now();
-        let scrollStart = this.targetElem.getBoundingClientRect().top;
+        const targetTop = this.targetElem.getBoundingClientRect().top;
+        const scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
+        const duration = 300;
+        const start = Date.now();
+        const scrollStart = this.targetElem.getBoundingClientRect().top;
 
         function setPos() {
-            let percentComplete = (1-((Date.now() - start) / duration));
-            let target = Math.abs(percentComplete * scrollStart);
+            const percentComplete = (1 - ((Date.now() - start) / duration));
+            const target = Math.abs(percentComplete * scrollStart);
             if (percentComplete > 0) {
                 scrollElem.scrollTop = target;
                 requestAnimationFrame(setPos.bind(this));
@@ -55,4 +55,4 @@ export class BackToTop extends Component {
         requestAnimationFrame(setPos.bind(this));
     }
 
-}
\ No newline at end of file
+}
index 5ae283fd017520b02adbb10fc0c56980eda2bd8f..2ba7d5d36b00dc912be8c3d1f25f1b5e464d2ffe 100644 (file)
@@ -1,25 +1,25 @@
-import Sortable, {MultiDrag} from "sortablejs";
-import {Component} from "./component";
-import {htmlToDom} from "../services/dom";
+import Sortable, {MultiDrag} from 'sortablejs';
+import {Component} from './component';
+import {htmlToDom} from '../services/dom';
 
 // Auto sort control
 const sortOperations = {
-    name: function(a, b) {
+    name(a, b) {
         const aName = a.getAttribute('data-name').trim().toLowerCase();
         const bName = b.getAttribute('data-name').trim().toLowerCase();
         return aName.localeCompare(bName);
     },
-    created: function(a, b) {
+    created(a, b) {
         const aTime = Number(a.getAttribute('data-created'));
         const bTime = Number(b.getAttribute('data-created'));
         return bTime - aTime;
     },
-    updated: function(a, b) {
+    updated(a, b) {
         const aTime = Number(a.getAttribute('data-updated'));
         const bTime = Number(b.getAttribute('data-updated'));
         return bTime - aTime;
     },
-    chaptersFirst: function(a, b) {
+    chaptersFirst(a, b) {
         const aType = a.getAttribute('data-type');
         const bType = b.getAttribute('data-type');
         if (aType === bType) {
@@ -27,7 +27,7 @@ const sortOperations = {
         }
         return (aType === 'chapter' ? -1 : 1);
     },
-    chaptersLast: function(a, b) {
+    chaptersLast(a, b) {
         const aType = a.getAttribute('data-type');
         const bType = b.getAttribute('data-type');
         if (aType === bType) {
@@ -45,22 +45,22 @@ const sortOperations = {
  */
 const moveActions = {
     up: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return !(elem.previousElementSibling === null && !parent);
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             const newSibling = elem.previousElementSibling || parent;
             newSibling.insertAdjacentElement('beforebegin', elem);
-        }
+        },
     },
     down: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return !(elem.nextElementSibling === null && !parent);
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             const newSibling = elem.nextElementSibling || parent;
             newSibling.insertAdjacentElement('afterend', elem);
-        }
+        },
     },
     next_book: {
         active(elem, parent, book) {
@@ -69,7 +69,7 @@ const moveActions = {
         run(elem, parent, book) {
             const newList = book.nextElementSibling.querySelector('ul');
             newList.prepend(elem);
-        }
+        },
     },
     prev_book: {
         active(elem, parent, book) {
@@ -78,13 +78,13 @@ const moveActions = {
         run(elem, parent, book) {
             const newList = book.previousElementSibling.querySelector('ul');
             newList.appendChild(elem);
-        }
+        },
     },
     next_chapter: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             const nextChapter = this.getNextChapter(elem, parent);
             nextChapter.querySelector('ul').prepend(elem);
         },
@@ -92,14 +92,14 @@ const moveActions = {
             const topLevel = (parent || elem);
             const topItems = Array.from(topLevel.parentElement.children);
             const index = topItems.indexOf(topLevel);
-            return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
-        }
+            return topItems.slice(index + 1).find(item => item.dataset.type === 'chapter');
+        },
     },
     prev_chapter: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             const prevChapter = this.getPrevChapter(elem, parent);
             prevChapter.querySelector('ul').append(elem);
         },
@@ -107,40 +107,40 @@ const moveActions = {
             const topLevel = (parent || elem);
             const topItems = Array.from(topLevel.parentElement.children);
             const index = topItems.indexOf(topLevel);
-            return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
-        }
+            return topItems.slice(0, index).reverse().find(item => item.dataset.type === 'chapter');
+        },
     },
     book_end: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return parent || (parent === null && elem.nextElementSibling);
         },
         run(elem, parent, book) {
             book.querySelector('ul').append(elem);
-        }
+        },
     },
     book_start: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return parent || (parent === null && elem.previousElementSibling);
         },
         run(elem, parent, book) {
             book.querySelector('ul').prepend(elem);
-        }
+        },
     },
     before_chapter: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return parent;
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             parent.insertAdjacentElement('beforebegin', elem);
-        }
+        },
     },
     after_chapter: {
-        active(elem, parent, book) {
+        active(elem, parent) {
             return parent;
         },
-        run(elem, parent, book) {
+        run(elem, parent) {
             parent.insertAdjacentElement('afterend', elem);
-        }
+        },
     },
 };
 
@@ -196,12 +196,12 @@ export class BookSort extends Component {
             reverse = (lastSort === sort) ? !reverse : false;
             let sortFunction = sortOperations[sort];
             if (reverse && reversibleTypes.includes(sort)) {
-                sortFunction = function(a, b) {
-                    return 0 - sortOperations[sort](a, b)
+                sortFunction = function reverseSortOperation(a, b) {
+                    return 0 - sortOperations[sort](a, b);
                 };
             }
 
-            for (let list of sortLists) {
+            for (const list of sortLists) {
                 const directItems = Array.from(list.children).filter(child => child.matches('li'));
                 directItems.sort(sortFunction).forEach(sortedItem => {
                     list.appendChild(sortedItem);
@@ -221,7 +221,7 @@ export class BookSort extends Component {
         const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
         if (alreadyAdded) return;
 
-        const entitySortItemUrl = entityInfo.link + '/sort-item';
+        const entitySortItemUrl = `${entityInfo.link}/sort-item`;
         window.$http.get(entitySortItemUrl).then(resp => {
             const newBookContainer = htmlToDom(resp.data);
             this.sortContainer.append(newBookContainer);
@@ -249,9 +249,9 @@ export class BookSort extends Component {
         const chapterGroupConfig = {
             name: 'chapter',
             pull: ['book', 'chapter'],
-            put: function(toList, fromList, draggedElem) {
+            put(toList, fromList, draggedElem) {
                 return draggedElem.getAttribute('data-type') === 'page';
-            }
+            },
         };
 
         for (const sortElem of sortElems) {
@@ -260,8 +260,8 @@ export class BookSort extends Component {
                 animation: 150,
                 fallbackOnBody: true,
                 swapThreshold: 0.65,
-                onSort: (event) => {
-                    this.ensureNoNestedChapters()
+                onSort: () => {
+                    this.ensureNoNestedChapters();
                     this.updateMapInput();
                     this.updateMoveActionStateForAll();
                 },
@@ -304,7 +304,7 @@ export class BookSort extends Component {
         const entityMap = [];
         const lists = this.container.querySelectorAll('.sort-list');
 
-        for (let list of lists) {
+        for (const list of lists) {
             const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
             const directChildren = Array.from(list.children)
                 .filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
@@ -332,9 +332,9 @@ export class BookSort extends Component {
         entityMap.push({
             id: childId,
             sort: index,
-            parentChapter: parentChapter,
-            type: type,
-            book: bookId
+            parentChapter,
+            type,
+            book: bookId,
         });
 
         const subPages = childElem.querySelectorAll('[data-type="page"]');
@@ -344,7 +344,7 @@ export class BookSort extends Component {
                 sort: i,
                 parentChapter: childId,
                 type: 'page',
-                book: bookId
+                book: bookId,
             });
         }
     }
@@ -383,4 +383,5 @@ export class BookSort extends Component {
             this.updateMoveActionState(item);
         }
     }
-}
\ No newline at end of file
+
+}
index 37df213e3c98e1ce12fa2de347e9397c3213238f..7c6480a1af0b8a6c176e1f6dd8beca9ada8a5033 100644 (file)
@@ -1,5 +1,5 @@
-import {slideUp, slideDown} from "../services/animations";
-import {Component} from "./component";
+import {slideUp, slideDown} from '../services/animations';
+import {Component} from './component';
 
 export class ChapterContents extends Component {
 
@@ -27,6 +27,11 @@ export class ChapterContents extends Component {
 
     click(event) {
         event.preventDefault();
-        this.isOpen ?  this.close() : this.open();
+        if (this.isOpen) {
+            this.close();
+        } else {
+            this.open();
+        }
     }
+
 }
index 205cbd8fdbc21efec49301430a7eee97fa67a991..f9dc2b69ff9f8480b34f541a961b27bd17dbf786 100644 (file)
@@ -1,9 +1,19 @@
-import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
-import {Component} from "./component";
-
+import {onChildEvent, onEnterPress, onSelect} from '../services/dom';
+import {Component} from './component';
 
 export class CodeEditor extends Component {
 
+    /**
+     * @type {null|SimpleEditorInterface}
+     */
+    editor = null;
+
+    callback = null;
+
+    history = {};
+
+    historyKey = 'code_history';
+
     setup() {
         this.container = this.$refs.container;
         this.popup = this.$el;
@@ -16,10 +26,6 @@ export class CodeEditor extends Component {
         this.historyList = this.$refs.historyList;
         this.favourites = new Set(this.$opts.favourites.split(','));
 
-        this.callback = null;
-        this.editor = null;
-        this.history = {};
-        this.historyKey = 'code_history';
         this.setupListeners();
         this.setupFavourites();
     }
@@ -37,15 +43,15 @@ export class CodeEditor extends Component {
             this.languageInputChange(language);
         });
 
-        onEnterPress(this.languageInput, e => this.save());
-        this.languageInput.addEventListener('input', e => this.languageInputChange(this.languageInput.value));
-        onSelect(this.saveButton, e => this.save());
+        onEnterPress(this.languageInput, () => this.save());
+        this.languageInput.addEventListener('input', () => this.languageInputChange(this.languageInput.value));
+        onSelect(this.saveButton, () => this.save());
 
         onChildEvent(this.historyList, 'button', 'click', (event, elem) => {
             event.preventDefault();
             const historyTime = elem.dataset.time;
             if (this.editor) {
-                this.editor.setValue(this.history[historyTime]);
+                this.editor.setContent(this.history[historyTime]);
             }
         });
     }
@@ -68,17 +74,23 @@ export class CodeEditor extends Component {
 
         onChildEvent(button.parentElement, '.lang-option-favorite-toggle', 'click', () => {
             isFavorite = !isFavorite;
-            isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
+
+            if (isFavorite) {
+                this.favourites.add(language);
+            } else {
+                this.favourites.delete(language);
+            }
+
             button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
 
             window.$http.patch('/preferences/update-code-language-favourite', {
-                language: language,
-                active: isFavorite
+                language,
+                active: isFavorite,
             });
 
             this.sortLanguageList();
             if (isFavorite) {
-                button.scrollIntoView({block: "center", behavior: "smooth"});
+                button.scrollIntoView({block: 'center', behavior: 'smooth'});
             }
         });
     }
@@ -90,7 +102,7 @@ export class CodeEditor extends Component {
 
             if (aFav && !bFav) {
                 return -1;
-            } else if (bFav && !aFav) {
+            } if (bFav && !aFav) {
                 return 1;
             }
 
@@ -104,19 +116,18 @@ export class CodeEditor extends Component {
 
     save() {
         if (this.callback) {
-            this.callback(this.editor.getValue(), this.languageInput.value);
+            this.callback(this.editor.getContent(), this.languageInput.value);
         }
         this.hide();
     }
 
-    open(code, language, callback) {
+    async open(code, language, callback) {
         this.languageInput.value = language;
         this.callback = callback;
 
-        this.show()
-            .then(() => this.languageInputChange(language))
-            .then(() => window.importVersioned('code'))
-            .then(Code => Code.setContent(this.editor, code));
+        await this.show();
+        this.languageInputChange(language);
+        this.editor.setContent(code);
     }
 
     async show() {
@@ -127,10 +138,9 @@ export class CodeEditor extends Component {
 
         this.loadHistory();
         this.getPopup().show(() => {
-            Code.updateLayout(this.editor);
             this.editor.focus();
         }, () => {
-            this.addHistory()
+            this.addHistory();
         });
     }
 
@@ -147,8 +157,7 @@ export class CodeEditor extends Component {
     }
 
     async updateEditorMode(language) {
-        const Code = await window.importVersioned('code');
-        Code.setMode(this.editor, language, this.editor.getValue());
+        this.editor.setMode(language, this.editor.getContent());
     }
 
     languageInputChange(language) {
@@ -160,7 +169,7 @@ export class CodeEditor extends Component {
             const isMatch = inputLang === lang;
             link.classList.toggle('active', isMatch);
             if (isMatch) {
-                link.scrollIntoView({block: "center", behavior: "smooth"});
+                link.scrollIntoView({block: 'center', behavior: 'smooth'});
             }
         }
     }
@@ -170,14 +179,14 @@ export class CodeEditor extends Component {
         const historyKeys = Object.keys(this.history).reverse();
         this.historyDropDown.classList.toggle('hidden', historyKeys.length === 0);
         this.historyList.innerHTML = historyKeys.map(key => {
-             const localTime = (new Date(parseInt(key))).toLocaleTimeString();
-             return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
+            const localTime = (new Date(parseInt(key, 10))).toLocaleTimeString();
+            return `<li><button type="button" data-time="${key}" class="text-item">${localTime}</button></li>`;
         }).join('');
     }
 
     addHistory() {
         if (!this.editor) return;
-        const code = this.editor.getValue();
+        const code = this.editor.getContent();
         if (!code) return;
 
         // Stop if we'd be storing the same as the last item
@@ -189,4 +198,4 @@ export class CodeEditor extends Component {
         window.sessionStorage.setItem(this.historyKey, historyString);
     }
 
-}
\ No newline at end of file
+}
index 14bfc97f04ed5358b2758112e9cf3f06d47ae423..e12d770447fb1584243e0d3d9820ef21302d96a6 100644 (file)
@@ -1,6 +1,6 @@
-import {Component} from "./component";
+import {Component} from './component';
 
-export class CodeHighlighter extends Component{
+export class CodeHighlighter extends Component {
 
     setup() {
         const container = this.$el;
@@ -8,9 +8,9 @@ export class CodeHighlighter extends Component{
         const codeBlocks = container.querySelectorAll('pre');
         if (codeBlocks.length > 0) {
             window.importVersioned('code').then(Code => {
-               Code.highlightWithin(container);
+                Code.highlightWithin(container);
             });
         }
     }
 
-}
\ No newline at end of file
+}
index 0e49aec1755693c8615fa1447e37b1fe549b3545..2f536da0b7af692048f1bd9d3c89d6f4be693a7b 100644 (file)
@@ -2,14 +2,14 @@
  * A simple component to render a code editor within the textarea
  * this exists upon.
  */
-import {Component} from "./component";
+import {Component} from './component';
 
 export class CodeTextarea extends Component {
 
     async setup() {
-        const mode = this.$opts.mode;
+        const {mode} = this.$opts;
         const Code = await window.importVersioned('code');
         Code.inlineEditor(this.$el, mode);
     }
 
-}
\ No newline at end of file
+}
index bb8ed477ffe9f600769dace8ba6d7f2af32d5fb7..6f740ed7163204020fcbd6569393b14729d787c4 100644 (file)
@@ -1,5 +1,5 @@
-import {slideDown, slideUp} from "../services/animations";
-import {Component} from "./component";
+import {slideDown, slideUp} from '../services/animations';
+import {Component} from './component';
 
 /**
  * Collapsible
@@ -45,4 +45,4 @@ export class Collapsible extends Component {
         }
     }
 
-}
\ No newline at end of file
+}
index 292bbb62414581184b3eb7aaf2e28a98eaa0d627..654f41a96643ef750d0c63bf6dac3d32a3203a08 100644 (file)
@@ -51,8 +51,9 @@ export class Component {
         const componentName = this.$name;
         const event = new CustomEvent(`${componentName}-${eventName}`, {
             bubbles: true,
-            detail: data
+            detail: data,
         });
         this.$el.dispatchEvent(event);
     }
-}
\ No newline at end of file
+
+}
index 572945d5aba4ae7099a30dc29a28fe58f62d4542..184618fccfad9928cca02f81f3450b61fbe21ebe 100644 (file)
@@ -1,5 +1,5 @@
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 /**
  * Custom equivalent of window.confirm() using our popup component.
@@ -25,8 +25,8 @@ export class ConfirmDialog extends Component {
             this.sendResult(false);
         });
 
-        return new Promise((res, rej) => {
-           this.res = res;
+        return new Promise(res => {
+            this.res = res;
         });
     }
 
@@ -42,9 +42,9 @@ export class ConfirmDialog extends Component {
      */
     sendResult(result) {
         if (this.res) {
-            this.res(result)
+            this.res(result);
             this.res = null;
         }
     }
 
-}
\ No newline at end of file
+}
index 99804c4bcea010165ad4298f405860e904b9689d..a5f1d5664247c9763a520cbec034b20fc7ca666e 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class CustomCheckbox extends Component {
 
@@ -30,4 +30,4 @@ export class CustomCheckbox extends Component {
         this.display.setAttribute('aria-checked', checked);
     }
 
-}
\ No newline at end of file
+}
index 6466fb584882b0af36280f06dc2420f7599305e8..71c2026294917abd63dc8b58ab54e3527eb99469 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class DetailsHighlighter extends Component {
 
@@ -19,4 +19,5 @@ export class DetailsHighlighter extends Component {
         }
         this.dealtWith = true;
     }
-}
\ No newline at end of file
+
+}
index 30a2aadc1467ddd97a1a3ccb30efea591383f07b..2344619f5e9b794de3177c6c4acf8a5c7550b351 100644 (file)
@@ -1,6 +1,6 @@
-import {debounce} from "../services/util";
-import {transitionHeight} from "../services/animations";
-import {Component} from "./component";
+import {debounce} from '../services/util';
+import {transitionHeight} from '../services/animations';
+import {Component} from './component';
 
 export class DropdownSearch extends Component {
 
@@ -40,7 +40,7 @@ export class DropdownSearch extends Component {
 
     runLocalSearch(searchTerm) {
         const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
-        for (let listItem of listItems) {
+        for (const listItem of listItems) {
             const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
             listItem.style.display = match ? 'flex' : 'none';
             listItem.classList.toggle('hidden', !match);
@@ -79,4 +79,4 @@ export class DropdownSearch extends Component {
         this.loadingElem.style.display = show ? 'block' : 'none';
     }
 
-}
\ No newline at end of file
+}
index ed69088b29acae227a9f57557406299a74e71459..b68f332b6f13a04a2d2db7fd706cfae0d42dc8bd 100644 (file)
@@ -1,6 +1,6 @@
-import {onSelect} from "../services/dom";
-import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
+import {Component} from './component';
 
 /**
  * Dropdown
@@ -41,7 +41,11 @@ export class Dropdown extends Component {
             this.menu.style.position = 'fixed';
             this.menu.style.width = `${menuOriginalRect.width}px`;
             this.menu.style.left = `${menuOriginalRect.left}px`;
-            heightOffset = dropUpwards ? (window.innerHeight - menuOriginalRect.top  - toggleHeight / 2) : menuOriginalRect.top;
+            if (dropUpwards) {
+                heightOffset = (window.innerHeight - menuOriginalRect.top - toggleHeight / 2);
+            } else {
+                heightOffset = menuOriginalRect.top;
+            }
         }
 
         // Adjust menu to display upwards if near the bottom of the screen
@@ -55,8 +59,8 @@ export class Dropdown extends Component {
 
         // Set listener to hide on mouse leave or window click
         this.menu.addEventListener('mouseleave', this.hide);
-        window.addEventListener('click', event => {
-            if (!this.menu.contains(event.target)) {
+        window.addEventListener('click', clickEvent => {
+            if (!this.menu.contains(clickEvent.target)) {
                 this.hide();
             }
         });
@@ -76,7 +80,7 @@ export class Dropdown extends Component {
     }
 
     hideAll() {
-        for (let dropdown of window.$components.get('dropdown')) {
+        for (const dropdown of window.$components.get('dropdown')) {
             dropdown.hide();
         }
     }
@@ -100,13 +104,13 @@ export class Dropdown extends Component {
     }
 
     setupListeners() {
-        const keyboardNavHandler = new KeyboardNavigationHandler(this.container, (event) => {
+        const keyboardNavHandler = new KeyboardNavigationHandler(this.container, event => {
             this.hide();
             this.toggle.focus();
             if (!this.bubbleEscapes) {
                 event.stopPropagation();
             }
-        }, (event) => {
+        }, event => {
             if (event.target.nodeName === 'INPUT') {
                 event.preventDefault();
                 event.stopPropagation();
@@ -120,10 +124,10 @@ export class Dropdown extends Component {
 
         // Hide menu on option click
         this.container.addEventListener('click', event => {
-             const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
-             if (possibleChildren.includes(event.target)) {
-                 this.hide();
-             }
+            const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
+            if (possibleChildren.includes(event.target)) {
+                this.hide();
+            }
         });
 
         onSelect(this.toggle, event => {
index 911a033c776b81ca757db2ad1e85875d599c0877..1fdf824ae72a38e520aaf170c465e114e96d7960 100644 (file)
-import DropZoneLib from "dropzone";
-import {fadeOut} from "../services/animations";
-import {Component} from "./component";
+import {Component} from './component';
+import {Clipboard} from '../services/clipboard';
+import {
+    elem, getLoading, onSelect, removeLoading,
+} from '../services/dom';
 
 export class Dropzone extends Component {
+
     setup() {
         this.container = this.$el;
+        this.statusArea = this.$refs.statusArea;
+        this.dropTarget = this.$refs.dropTarget;
+        this.selectButtons = this.$manyRefs.selectButton || [];
+
+        this.isActive = true;
+
         this.url = this.$opts.url;
         this.successMessage = this.$opts.successMessage;
-        this.removeMessage = this.$opts.removeMessage;
-        this.uploadLimit = Number(this.$opts.uploadLimit);
+        this.errorMessage = this.$opts.errorMessage;
+        this.uploadLimitMb = Number(this.$opts.uploadLimit);
         this.uploadLimitMessage = this.$opts.uploadLimitMessage;
-        this.timeoutMessage = this.$opts.timeoutMessage;
-
-        const _this = this;
-        this.dz = new DropZoneLib(this.container, {
-            addRemoveLinks: true,
-            dictRemoveFile: this.removeMessage,
-            timeout: Number(window.uploadTimeout) || 60000,
-            maxFilesize: this.uploadLimit,
-            url: this.url,
-            withCredentials: true,
-            init() {
-                this.dz = this;
-                this.dz.on('sending', _this.onSending.bind(_this));
-                this.dz.on('success', _this.onSuccess.bind(_this));
-                this.dz.on('error', _this.onError.bind(_this));
+        this.zoneText = this.$opts.zoneText;
+        this.fileAcceptTypes = this.$opts.fileAccept;
+
+        this.setupListeners();
+    }
+
+    /**
+     * Public method to allow external disabling/enabling of this drag+drop dropzone.
+     * @param {Boolean} active
+     */
+    toggleActive(active) {
+        this.isActive = active;
+    }
+
+    setupListeners() {
+        onSelect(this.selectButtons, this.manualSelectHandler.bind(this));
+        this.setupDropTargetHandlers();
+    }
+
+    setupDropTargetHandlers() {
+        let depth = 0;
+
+        const reset = () => {
+            this.hideOverlay();
+            depth = 0;
+        };
+
+        this.dropTarget.addEventListener('dragenter', event => {
+            event.preventDefault();
+            depth += 1;
+
+            if (depth === 1 && this.isActive) {
+                this.showOverlay();
+            }
+        });
+
+        this.dropTarget.addEventListener('dragover', event => {
+            event.preventDefault();
+        });
+
+        this.dropTarget.addEventListener('dragend', reset);
+        this.dropTarget.addEventListener('dragleave', () => {
+            depth -= 1;
+            if (depth === 0) {
+                reset();
+            }
+        });
+        this.dropTarget.addEventListener('drop', event => {
+            event.preventDefault();
+            reset();
+
+            if (!this.isActive) {
+                return;
+            }
+
+            const clipboard = new Clipboard(event.dataTransfer);
+            const files = clipboard.getFiles();
+            for (const file of files) {
+                this.createUploadFromFile(file);
             }
         });
     }
 
-    onSending(file, xhr, data) {
+    manualSelectHandler() {
+        const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;', accept: this.fileAcceptTypes});
+        this.container.append(input);
+        input.click();
+        input.addEventListener('change', () => {
+            for (const file of input.files) {
+                this.createUploadFromFile(file);
+            }
+            input.remove();
+        });
+    }
 
-        const token = window.document.querySelector('meta[name=token]').getAttribute('content');
-        data.append('_token', token);
+    showOverlay() {
+        const overlay = this.dropTarget.querySelector('.dropzone-overlay');
+        if (!overlay) {
+            const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
+            this.dropTarget.append(zoneElem);
+        }
+    }
 
-        xhr.ontimeout = (e) => {
-            this.dz.emit('complete', file);
-            this.dz.emit('error', file, this.timeoutMessage);
+    hideOverlay() {
+        const overlay = this.dropTarget.querySelector('.dropzone-overlay');
+        if (overlay) {
+            overlay.remove();
         }
     }
 
-    onSuccess(file, data) {
-        this.$emit('success', {file, data});
+    /**
+     * @param {File} file
+     * @return {Upload}
+     */
+    createUploadFromFile(file) {
+        const {
+            dom, status, progress, dismiss,
+        } = this.createDomForFile(file);
+        this.statusArea.append(dom);
+        const component = this;
+
+        const upload = {
+            file,
+            dom,
+            updateProgress(percentComplete) {
+                progress.textContent = `${percentComplete}%`;
+                progress.style.width = `${percentComplete}%`;
+            },
+            markError(message) {
+                status.setAttribute('data-status', 'error');
+                status.textContent = message;
+                removeLoading(dom);
+                this.updateProgress(100);
+            },
+            markSuccess(message) {
+                status.setAttribute('data-status', 'success');
+                status.textContent = message;
+                removeLoading(dom);
+                setTimeout(dismiss, 2400);
+                component.$emit('upload-success', {
+                    name: file.name,
+                });
+            },
+        };
 
-        if (this.successMessage) {
-            window.$events.emit('success', this.successMessage);
+        // Enforce early upload filesize limit
+        if (file.size > (this.uploadLimitMb * 1000000)) {
+            upload.markError(this.uploadLimitMessage);
+            return upload;
         }
 
-        fadeOut(file.previewElement, 800, () => {
-            this.dz.removeFile(file);
+        this.startXhrForUpload(upload);
+
+        return upload;
+    }
+
+    /**
+     * @param {Upload} upload
+     */
+    startXhrForUpload(upload) {
+        const formData = new FormData();
+        formData.append('file', upload.file, upload.file.name);
+        const component = this;
+
+        const req = window.$http.createXMLHttpRequest('POST', this.url, {
+            error() {
+                upload.markError(component.errorMessage);
+            },
+            readystatechange() {
+                if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
+                    upload.markSuccess(component.successMessage);
+                } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
+                    const content = this.responseText;
+                    const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
+                    const message = data?.message || data?.error || content;
+                    upload.markError(message);
+                }
+            },
+        });
+
+        req.upload.addEventListener('progress', evt => {
+            const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
+            upload.updateProgress(percent);
         });
+
+        req.setRequestHeader('Accept', 'application/json');
+        req.send(formData);
     }
 
-    onError(file, errorMessage, xhr) {
-        this.$emit('error', {file, errorMessage, xhr});
+    /**
+     * @param {File} file
+     * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}
+     */
+    createDomForFile(file) {
+        const image = elem('img', {src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.224 7.373a.924.924 0 0 0-.92.925l-.006 7.404c0 .509.412.925.921.925h5.557a.928.928 0 0 0 .926-.925v-5.553l-2.777-2.776Zm3.239 3.239V8.067l2.545 2.545z' style='fill:%23000;fill-opacity:.75'/%3E%3C/svg%3E"});
+        const status = elem('div', {class: 'dropzone-file-item-status'}, []);
+        const progress = elem('div', {class: 'dropzone-file-item-progress'});
+        const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);
 
-        const setMessage = (message) => {
-            const messsageEl = file.previewElement.querySelector('[data-dz-errormessage]');
-            messsageEl.textContent = message;
-        }
+        const dom = elem('div', {class: 'dropzone-file-item'}, [
+            imageWrap,
+            elem('div', {class: 'dropzone-file-item-text-wrap'}, [
+                elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
+                getLoading(),
+                status,
+            ]),
+            progress,
+        ]);
 
-        if (xhr && xhr.status === 413) {
-            setMessage(this.uploadLimitMessage);
-        } else if (errorMessage.file) {
-            setMessage(errorMessage.file);
+        if (file.type.startsWith('image/')) {
+            image.src = URL.createObjectURL(file);
         }
-    }
 
-    removeAll() {
-        this.dz.removeAllFiles(true);
+        const dismiss = () => {
+            dom.classList.add('dismiss');
+            dom.addEventListener('animationend', () => {
+                dom.remove();
+            });
+        };
+
+        dom.addEventListener('click', dismiss);
+
+        return {
+            dom, progress, status, dismiss,
+        };
     }
-}
\ No newline at end of file
+
+}
+
+/**
+ * @typedef Upload
+ * @property {File} file
+ * @property {Element} dom
+ * @property {function(Number)} updateProgress
+ * @property {function(String)} markError
+ * @property {function(String)} markSuccess
+ */
index a581ae7b4609727200ad3251b6f0e5e59dd1213e..4d3c0ae75d22177e0362c21b2b2b7f971e1af2ac 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class EditorToolbox extends Component {
 
@@ -35,11 +35,10 @@ export class EditorToolbox extends Component {
     }
 
     setActiveTab(tabName, openToolbox = false) {
-
         // Set button visibility
         for (const button of this.buttons) {
             button.classList.remove('active');
-            const bName =  button.dataset.tab;
+            const bName = button.dataset.tab;
             if (bName === tabName) button.classList.add('active');
         }
 
@@ -55,4 +54,4 @@ export class EditorToolbox extends Component {
         }
     }
 
-}
\ No newline at end of file
+}
index d4a616ff1d5765e2f278028016e8fa95bf1d8cbc..7ab99a2a70bb2f45445d485b6aff49e018b25d7b 100644 (file)
@@ -1,5 +1,5 @@
-import {htmlToDom} from "../services/dom";
-import {Component} from "./component";
+import {htmlToDom} from '../services/dom';
+import {Component} from './component';
 
 export class EntityPermissions extends Component {
 
@@ -29,12 +29,12 @@ export class EntityPermissions extends Component {
         this.container.addEventListener('click', event => {
             const button = event.target.closest('button');
             if (button && button.dataset.roleId) {
-                this.removeRowOnButtonClick(button)
+                this.removeRowOnButtonClick(button);
             }
         });
 
         // Role select change
-        this.roleSelect.addEventListener('change', event => {
+        this.roleSelect.addEventListener('change', () => {
             const roleId = this.roleSelect.value;
             if (roleId) {
                 this.addRoleRow(roleId);
@@ -61,8 +61,8 @@ export class EntityPermissions extends Component {
 
     removeRowOnButtonClick(button) {
         const row = button.closest('.item-list-row');
-        const roleId = button.dataset.roleId;
-        const roleName = button.dataset.roleName;
+        const {roleId} = button.dataset;
+        const {roleName} = button.dataset;
 
         const option = document.createElement('option');
         option.value = roleId;
@@ -72,4 +72,4 @@ export class EntityPermissions extends Component {
         row.remove();
     }
 
-}
\ No newline at end of file
+}
index b0e42401d51b1520807cfb97d295afaf27ecd826..7a50444708dee6313ab5b2caee5a2dd21def7cdb 100644 (file)
@@ -1,7 +1,8 @@
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 export class EntitySearch extends Component {
+
     setup() {
         this.entityId = this.$opts.entityId;
         this.entityType = this.$opts.entityType;
@@ -30,7 +31,8 @@ export class EntitySearch extends Component {
     runSearch() {
         const term = this.searchInput.value.trim();
         if (term.length === 0) {
-            return this.clearSearch();
+            this.clearSearch();
+            return;
         }
 
         this.searchView.classList.remove('hidden');
@@ -51,4 +53,5 @@ export class EntitySearch extends Component {
         this.loadingBlock.classList.add('hidden');
         this.searchInput.value = '';
     }
-}
\ No newline at end of file
+
+}
index d455f7ee7d5286f3bbfb9979ec0651ea6dfac98c..e21e67fb33ebb2b6e1ec914d30d425114857bce4 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class EntitySelectorPopup extends Component {
 
@@ -57,4 +57,5 @@ export class EntitySelectorPopup extends Component {
         this.getSelector().reset();
         if (this.callback && entity) this.callback(entity);
     }
-}
\ No newline at end of file
+
+}
index 09d14b23383075d015a9c846679de9b6641bf188..f12108fbb497877d9d96cd345e66cdf6a24f51bf 100644 (file)
@@ -1,5 +1,5 @@
-import {onChildEvent} from "../services/dom";
-import {Component} from "./component";
+import {onChildEvent} from '../services/dom';
+import {Component} from './component';
 
 /**
  * Entity Selector
@@ -29,7 +29,7 @@ export class EntitySelector extends Component {
         this.elem.addEventListener('click', this.onClick.bind(this));
 
         let lastSearch = 0;
-        this.searchInput.addEventListener('input', event => {
+        this.searchInput.addEventListener('input', () => {
             lastSearch = Date.now();
             this.showLoading();
             setTimeout(() => {
@@ -43,35 +43,35 @@ export class EntitySelector extends Component {
         });
 
         // Keyboard navigation
-        onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
-            if (e.ctrlKey && e.code === 'Enter') {
+        onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
+            if (event.ctrlKey && event.code === 'Enter') {
                 const form = this.$el.closest('form');
                 if (form) {
                     form.submit();
-                    e.preventDefault();
+                    event.preventDefault();
                     return;
                 }
             }
 
-            if (e.code === 'ArrowDown') {
+            if (event.code === 'ArrowDown') {
                 this.focusAdjacent(true);
             }
-            if (e.code === 'ArrowUp') {
+            if (event.code === 'ArrowUp') {
                 this.focusAdjacent(false);
             }
         });
 
-        this.searchInput.addEventListener('keydown', e => {
-            if (e.code === 'ArrowDown') {
+        this.searchInput.addEventListener('keydown', event => {
+            if (event.code === 'ArrowDown') {
                 this.focusAdjacent(true);
             }
-        })
+        });
     }
 
     focusAdjacent(forward = true) {
         const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
         const selectedIndex = items.indexOf(document.activeElement);
-        const newItem = items[selectedIndex+ (forward ? 1 : -1)] || items[0];
+        const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
         if (newItem) {
             newItem.focus();
         }
@@ -101,7 +101,7 @@ export class EntitySelector extends Component {
         window.$http.get(this.searchUrl()).then(resp => {
             this.resultsContainer.innerHTML = resp.data;
             this.hideLoading();
-        })
+        });
     }
 
     searchUrl() {
@@ -144,13 +144,13 @@ export class EntitySelector extends Component {
 
         const link = item.getAttribute('href');
         const name = item.querySelector('.entity-list-item-name').textContent;
-        const data = {id: Number(id), name: name, link: link};
+        const data = {id: Number(id), name, link};
 
         if (isSelected) {
             item.classList.add('selected');
             this.selectedItemData = data;
         } else {
-            window.$events.emit('entity-select-change', null)
+            window.$events.emit('entity-select-change', null);
         }
 
         if (!isDblClick && !isSelected) return;
@@ -159,7 +159,7 @@ export class EntitySelector extends Component {
             this.confirmSelection(data);
         }
         if (isSelected) {
-            window.$events.emit('entity-select-change', data)
+            window.$events.emit('entity-select-change', data);
         }
     }
 
@@ -175,4 +175,4 @@ export class EntitySelector extends Component {
         this.selectedItemData = null;
     }
 
-}
\ No newline at end of file
+}
index 2e6fd5fdbac008566f7fc88701000327853e7f0d..2097c0528868181cdc9e94c67cc630fdfca18652 100644 (file)
@@ -1,5 +1,5 @@
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 /**
  * EventEmitSelect
@@ -12,15 +12,15 @@ import {Component} from "./component";
  * All options will be set as the "detail" of the event with
  * their values included.
  */
-export class EventEmitSelect extends Component{
+export class EventEmitSelect extends Component {
+
     setup() {
         this.container = this.$el;
         this.name = this.$opts.name;
 
-
         onSelect(this.$el, () => {
             this.$emit(this.name, this.$opts);
         });
     }
 
-}
\ No newline at end of file
+}
index ab4d38ab1df2224270a27aab2f1fca6ae3786e10..0d2018b9da23531b27d98b18db682335ab47615b 100644 (file)
@@ -1,9 +1,9 @@
-import {slideUp, slideDown} from "../services/animations";
-import {Component} from "./component";
+import {slideUp, slideDown} from '../services/animations';
+import {Component} from './component';
 
 export class ExpandToggle extends Component {
 
-    setup(elem) {
+    setup() {
         this.targetSelector = this.$opts.targetSelector;
         this.isOpen = this.$opts.isOpen === 'true';
         this.updateEndpoint = this.$opts.updateEndpoint;
@@ -24,8 +24,9 @@ export class ExpandToggle extends Component {
         event.preventDefault();
 
         const matchingElems = document.querySelectorAll(this.targetSelector);
-        for (let match of matchingElems) {
-            this.isOpen ?  this.close(match) : this.open(match);
+        for (const match of matchingElems) {
+            const action = this.isOpen ? this.close : this.open;
+            action(match);
         }
 
         this.isOpen = !this.isOpen;
@@ -34,8 +35,8 @@ export class ExpandToggle extends Component {
 
     updateSystemAjax(isOpen) {
         window.$http.patch(this.updateEndpoint, {
-            expand: isOpen ? 'true' : 'false'
+            expand: isOpen ? 'true' : 'false',
         });
     }
 
-}
\ No newline at end of file
+}
index 7bc8a1d45187f8f711f0362c15bcab42119996db..798bd7aacb0d5c00fb5d0ece92c6ae604678a235 100644 (file)
@@ -1,7 +1,7 @@
-import {htmlToDom} from "../services/dom";
-import {debounce} from "../services/util";
-import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
-import {Component} from "./component";
+import {htmlToDom} from '../services/dom';
+import {debounce} from '../services/util';
+import {KeyboardNavigationHandler} from '../services/keyboard-navigation';
+import {Component} from './component';
 
 /**
  * Global (header) search box handling.
@@ -25,12 +25,12 @@ export class GlobalSearch extends Component {
 
         // Handle search input changes
         this.input.addEventListener('input', () => {
-            const value = this.input.value;
+            const {value} = this.input;
             if (value.length > 0) {
                 this.loadingWrap.style.display = 'block';
                 this.suggestionResultsWrap.style.opacity = '0.5';
                 updateSuggestionsDebounced(value);
-            }  else {
+            } else {
                 this.hideSuggestions();
             }
         });
@@ -55,7 +55,7 @@ export class GlobalSearch extends Component {
         if (!this.input.value) {
             return;
         }
-        
+
         const resultDom = htmlToDom(results);
 
         this.suggestionResultsWrap.innerHTML = '';
@@ -71,7 +71,7 @@ export class GlobalSearch extends Component {
         this.container.classList.add('search-active');
         window.requestAnimationFrame(() => {
             this.suggestions.classList.add('search-suggestions-animation');
-        })
+        });
     }
 
     hideSuggestions() {
@@ -79,4 +79,5 @@ export class GlobalSearch extends Component {
         this.suggestions.classList.remove('search-suggestions-animation');
         this.suggestionResultsWrap.innerHTML = '';
     }
-}
\ No newline at end of file
+
+}
index 11b23cca6cc06f833d8d3146ceb6c0bb0ddf6bdf..f94f897f617e5cfae3b39ddb3aaf7db073426d56 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class HeaderMobileToggle extends Component {
 
@@ -19,10 +19,10 @@ export class HeaderMobileToggle extends Component {
         this.toggleButton.setAttribute('aria-expanded', this.open ? 'true' : 'false');
         if (this.open) {
             this.elem.addEventListener('keydown', this.onKeyDown);
-            window.addEventListener('click', this.onWindowClick)
+            window.addEventListener('click', this.onWindowClick);
         } else {
             this.elem.removeEventListener('keydown', this.onKeyDown);
-            window.removeEventListener('click', this.onWindowClick)
+            window.removeEventListener('click', this.onWindowClick);
         }
         event.stopPropagation();
     }
@@ -37,4 +37,4 @@ export class HeaderMobileToggle extends Component {
         this.onToggle(event);
     }
 
-}
\ No newline at end of file
+}
index 418b7c98a2be0f16801f7b0ac7e45536c6ff8faa..b817823643506b4c34a8a253ee6ffae9ec9e0749 100644 (file)
@@ -1,5 +1,7 @@
-import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
-import {Component} from "./component";
+import {
+    onChildEvent, onSelect, removeLoading, showLoading,
+} from '../services/dom';
+import {Component} from './component';
 
 export class ImageManager extends Component {
 
@@ -16,7 +18,10 @@ export class ImageManager extends Component {
         this.listContainer = this.$refs.listContainer;
         this.filterTabs = this.$manyRefs.filterTabs;
         this.selectButton = this.$refs.selectButton;
+        this.uploadButton = this.$refs.uploadButton;
+        this.uploadHint = this.$refs.uploadHint;
         this.formContainer = this.$refs.formContainer;
+        this.formContainerPlaceholder = this.$refs.formContainerPlaceholder;
         this.dropzoneContainer = this.$refs.dropzoneContainer;
 
         // Instance data
@@ -48,28 +53,24 @@ export class ImageManager extends Component {
             event.preventDefault();
         });
 
-        onSelect(this.cancelSearch, event => {
+        onSelect(this.cancelSearch, () => {
             this.resetListView();
             this.resetSearchView();
             this.loadGallery();
-            this.cancelSearch.classList.remove('active');
         });
 
-        this.searchInput.addEventListener('input', event => {
-            this.cancelSearch.classList.toggle('active', this.searchInput.value.trim());
-        });
-
-        onChildEvent(this.listContainer, '.load-more', 'click', async event => {
-            showLoading(event.target);
-            this.page++;
+        onChildEvent(this.listContainer, '.load-more button', 'click', async event => {
+            const wrapper = event.target.closest('.load-more');
+            showLoading(wrapper);
+            this.page += 1;
             await this.loadGallery();
-            event.target.remove();
+            wrapper.remove();
         });
 
         this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
 
         this.listContainer.addEventListener('error', event => {
-            event.target.src = baseUrl('loading_error.png');
+            event.target.src = window.baseUrl('loading_error.png');
         }, true);
 
         onSelect(this.selectButton, () => {
@@ -79,14 +80,17 @@ export class ImageManager extends Component {
             this.hide();
         });
 
-        onChildEvent(this.formContainer, '#image-manager-delete', 'click', event => {
+        onChildEvent(this.formContainer, '#image-manager-delete', 'click', () => {
             if (this.lastSelected) {
                 this.loadImageEditForm(this.lastSelected.id, true);
             }
         });
 
-        this.formContainer.addEventListener('ajax-form-success', this.refreshGallery.bind(this));
-        this.container.addEventListener('dropzone-success', this.refreshGallery.bind(this));
+        this.formContainer.addEventListener('ajax-form-success', () => {
+            this.refreshGallery();
+            this.resetEditForm();
+        });
+        this.container.addEventListener('dropzone-upload-success', this.refreshGallery.bind(this));
     }
 
     show(callback, type = 'gallery') {
@@ -95,7 +99,15 @@ export class ImageManager extends Component {
         this.callback = callback;
         this.type = type;
         this.getPopup().show();
-        this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
+
+        const hideUploads = type !== 'gallery';
+        this.dropzoneContainer.classList.toggle('hidden', hideUploads);
+        this.uploadButton.classList.toggle('hidden', hideUploads);
+        this.uploadHint.classList.toggle('hidden', hideUploads);
+
+        /** @var {Dropzone} * */
+        const dropzone = window.$components.firstOnElement(this.container, 'dropzone');
+        dropzone.toggleActive(!hideUploads);
 
         if (!this.hasData) {
             this.loadGallery();
@@ -161,6 +173,7 @@ export class ImageManager extends Component {
 
     resetEditForm() {
         this.formContainer.innerHTML = '';
+        this.formContainerPlaceholder.removeAttribute('hidden');
     }
 
     resetListView() {
@@ -207,7 +220,8 @@ export class ImageManager extends Component {
         const params = requestDelete ? {delete: true} : {};
         const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
         this.formContainer.innerHTML = formHtml;
+        this.formContainerPlaceholder.setAttribute('hidden', '');
         window.$components.init(this.formContainer);
     }
 
-}
\ No newline at end of file
+}
index 03d9567d22e331c82a04c75bc1840e207c5d1eec..d25e01dd7b722835718ea54b310ba219c272f5ce 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class ImagePicker extends Component {
 
@@ -31,7 +31,7 @@ export class ImagePicker extends Component {
             this.removeInput.setAttribute('disabled', 'disabled');
         }
 
-        for (let file of this.imageInput.files) {
+        for (const file of this.imageInput.files) {
             this.imageElem.src = window.URL.createObjectURL(file);
         }
         this.imageElem.classList.remove('none');
@@ -54,4 +54,4 @@ export class ImagePicker extends Component {
         this.resetInput.setAttribute('disabled', 'disabled');
     }
 
-}
\ No newline at end of file
+}
index 82136184b4eaa84f5095c3fe85fd1b457bb5d592..803714e62a35658d3b134831eb896740d2687bfb 100644 (file)
@@ -1,59 +1,59 @@
-export {AddRemoveRows} from "./add-remove-rows.js"
-export {AjaxDeleteRow} from "./ajax-delete-row.js"
-export {AjaxForm} from "./ajax-form.js"
-export {Attachments} from "./attachments.js"
-export {AttachmentsList} from "./attachments-list.js"
-export {AutoSuggest} from "./auto-suggest.js"
-export {AutoSubmit} from "./auto-submit.js"
-export {BackToTop} from "./back-to-top.js"
-export {BookSort} from "./book-sort.js"
-export {ChapterContents} from "./chapter-contents.js"
-export {CodeEditor} from "./code-editor.js"
-export {CodeHighlighter} from "./code-highlighter.js"
-export {CodeTextarea} from "./code-textarea.js"
-export {Collapsible} from "./collapsible.js"
-export {ConfirmDialog} from "./confirm-dialog"
-export {CustomCheckbox} from "./custom-checkbox.js"
-export {DetailsHighlighter} from "./details-highlighter.js"
-export {Dropdown} from "./dropdown.js"
-export {DropdownSearch} from "./dropdown-search.js"
-export {Dropzone} from "./dropzone.js"
-export {EditorToolbox} from "./editor-toolbox.js"
-export {EntityPermissions} from "./entity-permissions"
-export {EntitySearch} from "./entity-search.js"
-export {EntitySelector} from "./entity-selector.js"
-export {EntitySelectorPopup} from "./entity-selector-popup.js"
-export {EventEmitSelect} from "./event-emit-select.js"
-export {ExpandToggle} from "./expand-toggle.js"
-export {GlobalSearch} from "./global-search.js"
-export {HeaderMobileToggle} from "./header-mobile-toggle.js"
-export {ImageManager} from "./image-manager.js"
-export {ImagePicker} from "./image-picker.js"
-export {ListSortControl} from "./list-sort-control.js"
-export {MarkdownEditor} from "./markdown-editor.js"
-export {NewUserPassword} from "./new-user-password.js"
-export {Notification} from "./notification.js"
-export {OptionalInput} from "./optional-input.js"
-export {PageComments} from "./page-comments.js"
-export {PageDisplay} from "./page-display.js"
-export {PageEditor} from "./page-editor.js"
-export {PagePicker} from "./page-picker.js"
-export {PermissionsTable} from "./permissions-table.js"
-export {Pointer} from "./pointer.js"
-export {Popup} from "./popup.js"
-export {SettingAppColorScheme} from "./setting-app-color-scheme.js"
-export {SettingColorPicker} from "./setting-color-picker.js"
-export {SettingHomepageControl} from "./setting-homepage-control.js"
-export {ShelfSort} from "./shelf-sort.js"
-export {Shortcuts} from "./shortcuts"
-export {ShortcutInput} from "./shortcut-input"
-export {SortableList} from "./sortable-list.js"
-export {SubmitOnChange} from "./submit-on-change.js"
-export {Tabs} from "./tabs.js"
-export {TagManager} from "./tag-manager.js"
-export {TemplateManager} from "./template-manager.js"
-export {ToggleSwitch} from "./toggle-switch.js"
-export {TriLayout} from "./tri-layout.js"
-export {UserSelect} from "./user-select.js"
-export {WebhookEvents} from "./webhook-events"
-export {WysiwygEditor} from "./wysiwyg-editor.js"
+export {AddRemoveRows} from './add-remove-rows';
+export {AjaxDeleteRow} from './ajax-delete-row';
+export {AjaxForm} from './ajax-form';
+export {Attachments} from './attachments';
+export {AttachmentsList} from './attachments-list';
+export {AutoSuggest} from './auto-suggest';
+export {AutoSubmit} from './auto-submit';
+export {BackToTop} from './back-to-top';
+export {BookSort} from './book-sort';
+export {ChapterContents} from './chapter-contents';
+export {CodeEditor} from './code-editor';
+export {CodeHighlighter} from './code-highlighter';
+export {CodeTextarea} from './code-textarea';
+export {Collapsible} from './collapsible';
+export {ConfirmDialog} from './confirm-dialog';
+export {CustomCheckbox} from './custom-checkbox';
+export {DetailsHighlighter} from './details-highlighter';
+export {Dropdown} from './dropdown';
+export {DropdownSearch} from './dropdown-search';
+export {Dropzone} from './dropzone';
+export {EditorToolbox} from './editor-toolbox';
+export {EntityPermissions} from './entity-permissions';
+export {EntitySearch} from './entity-search';
+export {EntitySelector} from './entity-selector';
+export {EntitySelectorPopup} from './entity-selector-popup';
+export {EventEmitSelect} from './event-emit-select';
+export {ExpandToggle} from './expand-toggle';
+export {GlobalSearch} from './global-search';
+export {HeaderMobileToggle} from './header-mobile-toggle';
+export {ImageManager} from './image-manager';
+export {ImagePicker} from './image-picker';
+export {ListSortControl} from './list-sort-control';
+export {MarkdownEditor} from './markdown-editor';
+export {NewUserPassword} from './new-user-password';
+export {Notification} from './notification';
+export {OptionalInput} from './optional-input';
+export {PageComments} from './page-comments';
+export {PageDisplay} from './page-display';
+export {PageEditor} from './page-editor';
+export {PagePicker} from './page-picker';
+export {PermissionsTable} from './permissions-table';
+export {Pointer} from './pointer';
+export {Popup} from './popup';
+export {SettingAppColorScheme} from './setting-app-color-scheme';
+export {SettingColorPicker} from './setting-color-picker';
+export {SettingHomepageControl} from './setting-homepage-control';
+export {ShelfSort} from './shelf-sort';
+export {Shortcuts} from './shortcuts';
+export {ShortcutInput} from './shortcut-input';
+export {SortableList} from './sortable-list';
+export {SubmitOnChange} from './submit-on-change';
+export {Tabs} from './tabs';
+export {TagManager} from './tag-manager';
+export {TemplateManager} from './template-manager';
+export {ToggleSwitch} from './toggle-switch';
+export {TriLayout} from './tri-layout';
+export {UserSelect} from './user-select';
+export {WebhookEvents} from './webhook-events';
+export {WysiwygEditor} from './wysiwyg-editor';
index b8d4de73a0e7b8cd3326ca9e30679366043af7be..4b38fc1e50a5ca29528a1465ff75c16fb9d2aa1f 100644 (file)
@@ -2,7 +2,7 @@
  * ListSortControl
  * Manages the logic for the control which provides list sorting options.
  */
-import {Component} from "./component";
+import {Component} from './component';
 
 export class ListSortControl extends Component {
 
@@ -45,4 +45,4 @@ export class ListSortControl extends Component {
         this.form.submit();
     }
 
-}
\ No newline at end of file
+}
index 5cd92cae2bf1b7c556b2f947d5b0f2f360be3ba6..fa06807a5c815c79ebc117a3c9436940f913ee0d 100644 (file)
@@ -1,6 +1,5 @@
-import {debounce} from "../services/util";
-import {Component} from "./component";
-import {init as initEditor} from "../markdown/editor";
+import {Component} from './component';
+import {init as initEditor} from '../markdown/editor';
 
 export class MarkdownEditor extends Component {
 
@@ -17,7 +16,7 @@ export class MarkdownEditor extends Component {
         this.divider = this.$refs.divider;
         this.displayWrap = this.$refs.displayWrap;
 
-        const settingContainer = this.$refs.settingContainer;
+        const {settingContainer} = this.$refs;
         const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
 
         this.editor = null;
@@ -45,19 +44,18 @@ export class MarkdownEditor extends Component {
         window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
             markdownIt: this.editor.markdown.getRenderer(),
             displayEl: this.display,
-            codeMirrorInstance: this.editor.cm,
+            cmEditorView: this.editor.cm,
         });
     }
 
     setupListeners() {
-
         // Button actions
         this.elem.addEventListener('click', event => {
-            let button = event.target.closest('button[data-action]');
+            const button = event.target.closest('button[data-action]');
             if (button === null) return;
 
             const action = button.getAttribute('data-action');
-            if (action === 'insertImage') this.editor.actions.insertImage();
+            if (action === 'insertImage') this.editor.actions.showImageInsert();
             if (action === 'insertLink') this.editor.actions.showLinkSelector();
             if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
                 this.editor.actions.showImageManager();
@@ -80,29 +78,23 @@ export class MarkdownEditor extends Component {
             toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
         });
 
-        // Refresh CodeMirror on container resize
-        const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false);
-        const observer = new ResizeObserver(resizeDebounced);
-        observer.observe(this.elem);
-
         this.handleDividerDrag();
     }
 
     handleDividerDrag() {
-        this.divider.addEventListener('pointerdown', event => {
+        this.divider.addEventListener('pointerdown', () => {
             const wrapRect = this.elem.getBoundingClientRect();
-            const moveListener = (event) => {
+            const moveListener = event => {
                 const xRel = event.pageX - wrapRect.left;
                 const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
-                this.displayWrap.style.flexBasis = `${100-xPct}%`;
+                this.displayWrap.style.flexBasis = `${100 - xPct}%`;
                 this.editor.settings.set('editorWidth', xPct);
             };
-            const upListener = (event) => {
+            const upListener = () => {
                 window.removeEventListener('pointermove', moveListener);
                 window.removeEventListener('pointerup', upListener);
                 this.display.style.pointerEvents = null;
                 document.body.style.userSelect = null;
-                this.editor.cm.refresh();
             };
 
             this.display.style.pointerEvents = 'none';
@@ -112,7 +104,7 @@ export class MarkdownEditor extends Component {
         });
         const widthSetting = this.editor.settings.get('editorWidth');
         if (widthSetting) {
-            this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
+            this.displayWrap.style.flexBasis = `${100 - widthSetting}%`;
         }
     }
 
index a4ed4d15b300c5e255e86ef43eee9f8c7b31d014..e294f8e97f3228ed5a111614bd0c697ff7323b81 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class NewUserPassword extends Component {
 
@@ -23,4 +23,4 @@ export class NewUserPassword extends Component {
         this.inputContainer.style.display = inviting ? 'none' : 'block';
     }
 
-}
\ No newline at end of file
+}
index 8a0876241fe15232b82264e93556357691816614..8a1ef922a97a5870d2cf0683204a2e46e3769631 100644 (file)
@@ -1,13 +1,13 @@
-import {Component} from "./component";
+import {Component} from './component';
 
-export class Notification  extends Component {
+export class Notification extends Component {
 
     setup() {
         this.container = this.$el;
         this.type = this.$opts.type;
         this.textElem = this.container.querySelector('span');
         this.autoHide = this.$opts.autoHide === 'true';
-        this.initialShow = this.$opts.show === 'true'
+        this.initialShow = this.$opts.show === 'true';
         this.container.style.display = 'grid';
 
         window.$events.listen(this.type, text => {
@@ -47,4 +47,4 @@ export class Notification  extends Component {
         this.container.removeEventListener('transitionend', this.hideCleanup);
     }
 
-}
\ No newline at end of file
+}
index cc429c991522bcd9b51a5fc811127e7775a4a066..64cee12cd29b6a212a2fb882be6a0be193598a46 100644 (file)
@@ -1,7 +1,8 @@
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 export class OptionalInput extends Component {
+
     setup() {
         this.removeButton = this.$refs.remove;
         this.showButton = this.$refs.show;
@@ -24,4 +25,4 @@ export class OptionalInput extends Component {
         });
     }
 
-}
\ No newline at end of file
+}
index 726531e951f86f4a8447b41844803eb1d01dd083..0ac9d05720de80cdf9ec1b71e31d4ed02c77d1e0 100644 (file)
@@ -1,6 +1,6 @@
-import {scrollAndHighlightElement} from "../services/util";
-import {Component} from "./component";
-import {htmlToDom} from "../services/dom";
+import {scrollAndHighlightElement} from '../services/util';
+import {Component} from './component';
+import {htmlToDom} from '../services/dom';
 
 export class PageComments extends Component {
 
@@ -36,11 +36,11 @@ export class PageComments extends Component {
     }
 
     handleAction(event) {
-        let actionElem = event.target.closest('[action]');
+        const actionElem = event.target.closest('[action]');
 
         if (event.target.matches('a[href^="#"]')) {
             const id = event.target.href.split('#')[1];
-            scrollAndHighlightElement(document.querySelector('#' + id));
+            scrollAndHighlightElement(document.querySelector(`#${id}`));
         }
 
         if (actionElem === null) return;
@@ -68,24 +68,24 @@ export class PageComments extends Component {
         if (this.editingComment) this.closeUpdateForm();
         commentElem.querySelector('[comment-content]').style.display = 'none';
         commentElem.querySelector('[comment-edit-container]').style.display = 'block';
-        let textArea = commentElem.querySelector('[comment-edit-container] textarea');
-        let lineCount = textArea.value.split('\n').length;
-        textArea.style.height = ((lineCount * 20) + 40) + 'px';
+        const textArea = commentElem.querySelector('[comment-edit-container] textarea');
+        const lineCount = textArea.value.split('\n').length;
+        textArea.style.height = `${(lineCount * 20) + 40}px`;
         this.editingComment = commentElem;
     }
 
     updateComment(event) {
-        let form = event.target;
+        const form = event.target;
         event.preventDefault();
-        let text = form.querySelector('textarea').value;
-        let reqData = {
-            text: text,
+        const text = form.querySelector('textarea').value;
+        const reqData = {
+            text,
             parent_id: this.parentId || null,
         };
         this.showLoading(form);
-        let commentId = this.editingComment.getAttribute('comment');
+        const commentId = this.editingComment.getAttribute('comment');
         window.$http.put(`/comment/${commentId}`, reqData).then(resp => {
-            let newComment = document.createElement('div');
+            const newComment = document.createElement('div');
             newComment.innerHTML = resp.data;
             this.editingComment.innerHTML = newComment.children[0].innerHTML;
             window.$events.success(this.updatedText);
@@ -98,9 +98,9 @@ export class PageComments extends Component {
     }
 
     deleteComment(commentElem) {
-        let id = commentElem.getAttribute('comment');
+        const id = commentElem.getAttribute('comment');
         this.showLoading(commentElem.querySelector('[comment-content]'));
-        window.$http.delete(`/comment/${id}`).then(resp => {
+        window.$http.delete(`/comment/${id}`).then(() => {
             commentElem.parentNode.removeChild(commentElem);
             window.$events.success(this.deletedText);
             this.updateCount();
@@ -111,9 +111,9 @@ export class PageComments extends Component {
     saveComment(event) {
         event.preventDefault();
         event.stopPropagation();
-        let text = this.formInput.value;
-        let reqData = {
-            text: text,
+        const text = this.formInput.value;
+        const reqData = {
+            text,
             parent_id: this.parentId || null,
         };
         this.showLoading(this.form);
@@ -131,7 +131,7 @@ export class PageComments extends Component {
     }
 
     updateCount() {
-        let count = this.container.children.length;
+        const count = this.container.children.length;
         this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count});
     }
 
@@ -148,14 +148,14 @@ export class PageComments extends Component {
         this.formContainer.parentNode.style.display = 'block';
         this.addButtonContainer.style.display = 'none';
         this.formInput.focus();
-        this.formInput.scrollIntoView({behavior: "smooth"});
+        this.formInput.scrollIntoView({behavior: 'smooth'});
     }
 
     hideForm() {
         this.formContainer.style.display = 'none';
         this.formContainer.parentNode.style.display = 'none';
         if (this.getCommentCount() > 0) {
-            this.elem.appendChild(this.addButtonContainer)
+            this.elem.appendChild(this.addButtonContainer);
         } else {
             this.commentCountBar.appendChild(this.addButtonContainer);
         }
@@ -182,7 +182,7 @@ export class PageComments extends Component {
 
     showLoading(formElem) {
         const groups = formElem.querySelectorAll('.form-group');
-        for (let group of groups) {
+        for (const group of groups) {
             group.style.display = 'none';
         }
         formElem.querySelector('.form-group.loading').style.display = 'block';
@@ -190,10 +190,10 @@ export class PageComments extends Component {
 
     hideLoading(formElem) {
         const groups = formElem.querySelectorAll('.form-group');
-        for (let group of groups) {
+        for (const group of groups) {
             group.style.display = 'block';
         }
         formElem.querySelector('.form-group.loading').style.display = 'none';
     }
 
-}
\ No newline at end of file
+}
index c06c3310dee2c44c221b331c8a78b8d3eee9d3f3..eb7df5fb6460b37b52835a719d78df7f11d28aaa 100644 (file)
@@ -1,6 +1,33 @@
-import * as DOM from "../services/dom";
-import {scrollAndHighlightElement} from "../services/util";
-import {Component} from "./component";
+import * as DOM from '../services/dom';
+import {scrollAndHighlightElement} from '../services/util';
+import {Component} from './component';
+
+function toggleAnchorHighlighting(elementId, shouldHighlight) {
+    DOM.forEach(`a[href="#${elementId}"]`, anchor => {
+        anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
+    });
+}
+
+function headingVisibilityChange(entries) {
+    for (const entry of entries) {
+        const isVisible = (entry.intersectionRatio === 1);
+        toggleAnchorHighlighting(entry.target.id, isVisible);
+    }
+}
+
+function addNavObserver(headings) {
+    // Setup the intersection observer.
+    const intersectOpts = {
+        rootMargin: '0px 0px 0px 0px',
+        threshold: 1.0,
+    };
+    const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
+
+    // observe each heading
+    for (const heading of headings) {
+        pageNavObserver.observe(heading);
+    }
+}
 
 export class PageDisplay extends Component {
 
@@ -26,7 +53,7 @@ export class PageDisplay extends Component {
                 window.$components.first('tri-layout').showContent();
                 const contentId = child.getAttribute('href').substr(1);
                 this.goToText(contentId);
-                window.history.pushState(null, null, '#' + contentId);
+                window.history.pushState(null, null, `#${contentId}`);
             });
         }
     }
@@ -58,33 +85,6 @@ export class PageDisplay extends Component {
         if (headings.length > 0 && pageNav !== null) {
             addNavObserver(headings);
         }
-
-        function addNavObserver(headings) {
-            // Setup the intersection observer.
-            const intersectOpts = {
-                rootMargin: '0px 0px 0px 0px',
-                threshold: 1.0
-            };
-            const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
-
-            // observe each heading
-            for (const heading of headings) {
-                pageNavObserver.observe(heading);
-            }
-        }
-
-        function headingVisibilityChange(entries, observer) {
-            for (const entry of entries) {
-                const isVisible = (entry.intersectionRatio === 1);
-                toggleAnchorHighlighting(entry.target.id, isVisible);
-            }
-        }
-
-        function toggleAnchorHighlighting(elementId, shouldHighlight) {
-            DOM.forEach('a[href="#' + elementId + '"]', anchor => {
-                anchor.closest('li').classList.toggle('current-heading', shouldHighlight);
-            });
-        }
     }
 
     setupDetailsCodeBlockRefresh() {
@@ -96,4 +96,5 @@ export class PageDisplay extends Component {
         const details = [...this.container.querySelectorAll('details')];
         details.forEach(detail => detail.addEventListener('toggle', onToggle));
     }
-}
\ No newline at end of file
+
+}
index c58f45b66198544c4ffd0f8d21052d95a09ea1ae..e7f4c0ba95907345e7ad25b6b0fc2fa3284c8853 100644 (file)
@@ -1,9 +1,10 @@
-import * as Dates from "../services/dates";
-import {onSelect} from "../services/dom";
-import {debounce} from "../services/util";
-import {Component} from "./component";
+import * as Dates from '../services/dates';
+import {onSelect} from '../services/dom';
+import {debounce} from '../services/util';
+import {Component} from './component';
 
 export class PageEditor extends Component {
+
     setup() {
         // Options
         this.draftsEnabled = this.$opts.draftsEnabled === 'true';
@@ -22,7 +23,7 @@ export class PageEditor extends Component {
         this.draftDisplayIcon = this.$refs.draftDisplayIcon;
         this.changelogInput = this.$refs.changelogInput;
         this.changelogDisplay = this.$refs.changelogDisplay;
-        this.changeEditorButtons = this.$manyRefs.changeEditor;
+        this.changeEditorButtons = this.$manyRefs.changeEditor || [];
         this.switchDialogContainer = this.$refs.switchDialog;
 
         // Translations
@@ -58,7 +59,9 @@ export class PageEditor extends Component {
         window.$events.listen('editor-save-page', this.savePage.bind(this));
 
         // Listen to content changes from the editor
-        const onContentChange = () => this.autoSave.pendingChange = true;
+        const onContentChange = () => {
+            this.autoSave.pendingChange = true;
+        };
         window.$events.listen('editor-html-change', onContentChange);
         window.$events.listen('editor-markdown-change', onContentChange);
 
@@ -79,7 +82,8 @@ export class PageEditor extends Component {
 
     setInitialFocus() {
         if (this.hasDefaultTitle) {
-            return this.titleElem.select();
+            this.titleElem.select();
+            return;
         }
 
         window.setTimeout(() => {
@@ -93,12 +97,12 @@ export class PageEditor extends Component {
 
     runAutoSave() {
         // Stop if manually saved recently to prevent bombarding the server
-        const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency)/2);
+        const savedRecently = (Date.now() - this.autoSave.last < (this.autoSave.frequency) / 2);
         if (savedRecently || !this.autoSave.pendingChange) {
             return;
         }
 
-        this.saveDraft()
+        this.saveDraft();
     }
 
     savePage() {
@@ -132,7 +136,9 @@ export class PageEditor extends Component {
             try {
                 const saveKey = `draft-save-fail-${(new Date()).toISOString()}`;
                 window.localStorage.setItem(saveKey, JSON.stringify(data));
-            } catch (err) {}
+            } catch (lsErr) {
+                console.error(lsErr);
+            }
 
             window.$events.emit('error', this.autosaveFailText);
         }
@@ -153,7 +159,8 @@ export class PageEditor extends Component {
         try {
             response = await window.$http.get(`/ajax/page/${this.pageId}`);
         } catch (e) {
-            return console.error(e);
+            console.error(e);
+            return;
         }
 
         if (this.autoSave.interval) {
@@ -172,7 +179,6 @@ export class PageEditor extends Component {
             this.startAutoSave();
         }, 1000);
         window.$events.emit('success', this.draftDiscardedText);
-
     }
 
     updateChangelogDisplay() {
@@ -180,7 +186,7 @@ export class PageEditor extends Component {
         if (summary.length === 0) {
             summary = this.setChangelogText;
         } else if (summary.length > 16) {
-            summary = summary.slice(0, 16) + '...';
+            summary = `${summary.slice(0, 16)}...`;
         }
         this.changelogDisplay.innerText = summary;
     }
@@ -193,7 +199,7 @@ export class PageEditor extends Component {
         event.preventDefault();
 
         const link = event.target.closest('a').href;
-        /** @var {ConfirmDialog} **/
+        /** @var {ConfirmDialog} * */
         const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
         const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
 
index fba0a0a43f779a8802c0a7130789fb948f2e0226..130972fdd63ebc8aaed1cf4572a728d25a505904 100644 (file)
@@ -1,4 +1,8 @@
-import {Component} from "./component";
+import {Component} from './component';
+
+function toggleElem(elem, show) {
+    elem.style.display = show ? null : 'none';
+}
 
 export class PagePicker extends Component {
 
@@ -18,13 +22,13 @@ export class PagePicker extends Component {
         this.selectButton.addEventListener('click', this.showPopup.bind(this));
         this.display.parentElement.addEventListener('click', this.showPopup.bind(this));
 
-        this.resetButton.addEventListener('click', event => {
+        this.resetButton.addEventListener('click', () => {
             this.setValue('', '');
         });
     }
 
     showPopup() {
-        /** @type {EntitySelectorPopup} **/
+        /** @type {EntitySelectorPopup} * */
         const selectorPopup = window.$components.first('entity-selector-popup');
         selectorPopup.show(entity => {
             this.setValue(entity.id, entity.name);
@@ -44,7 +48,7 @@ export class PagePicker extends Component {
         toggleElem(this.defaultDisplay, !hasValue);
         toggleElem(this.display, hasValue);
         if (hasValue) {
-            let id = this.getAssetIdFromVal();
+            const id = this.getAssetIdFromVal();
             this.display.textContent = `#${id}, ${name}`;
             this.display.href = window.baseUrl(`/link/${id}`);
         }
@@ -55,7 +59,3 @@ export class PagePicker extends Component {
     }
 
 }
-
-function toggleElem(elem, show) {
-    elem.style.display = show ? null : 'none';
-}
\ No newline at end of file
index 58ead1d57620b58a798ccdc0dfc3708121c8a1f9..800403c61a76e3b36bc46da922c15f264be65d3d 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class PermissionsTable extends Component {
 
@@ -41,7 +41,7 @@ export class PermissionsTable extends Component {
         const tableRows = this.container.querySelectorAll(this.rowSelector);
         const inputsToToggle = [];
 
-        for (let row of tableRows) {
+        for (const row of tableRows) {
             const targetCell = row.children[colIndex];
             if (targetCell) {
                 inputsToToggle.push(...targetCell.querySelectorAll('input[type=checkbox]'));
@@ -57,10 +57,10 @@ export class PermissionsTable extends Component {
 
     toggleAllInputs(inputsToToggle) {
         const currentState = inputsToToggle.length > 0 ? inputsToToggle[0].checked : false;
-        for (let checkbox of inputsToToggle) {
+        for (const checkbox of inputsToToggle) {
             checkbox.checked = !currentState;
             checkbox.dispatchEvent(new Event('change'));
         }
     }
 
-}
\ No newline at end of file
+}
index d884dc7214ce3ac71673ac0bddf71f39c2755fee..e2e2ceca729fd0d5952ece4c3b6eb09bd1657267 100644 (file)
@@ -1,12 +1,13 @@
-import * as DOM from "../services/dom";
-import Clipboard from "clipboard/dist/clipboard.min";
-import {Component} from "./component";
-
+import * as DOM from '../services/dom';
+import {Component} from './component';
+import {copyTextToClipboard} from '../services/clipboard';
 
 export class Pointer extends Component {
 
     setup() {
         this.container = this.$el;
+        this.input = this.$refs.input;
+        this.button = this.$refs.button;
         this.pageId = this.$opts.pageId;
 
         // Instance variables
@@ -16,15 +17,17 @@ export class Pointer extends Component {
         this.pointerSectionId = '';
 
         this.setupListeners();
-
-        // Set up clipboard
-        new Clipboard(this.container.querySelector('button'));
     }
 
     setupListeners() {
+        // Copy on copy button click
+        this.button.addEventListener('click', () => {
+            copyTextToClipboard(this.input.value);
+        });
+
         // Select all contents on input click
-        DOM.onChildEvent(this.container, 'input', 'click', (event, input) => {
-            input.select();
+        this.input.addEventListener('click', event => {
+            this.input.select();
             event.stopPropagation();
         });
 
@@ -43,7 +46,7 @@ export class Pointer extends Component {
         });
 
         // Hide pointer when clicking away
-        DOM.onEvents(document.body, ['click', 'focus'], event => {
+        DOM.onEvents(document.body, ['click', 'focus'], () => {
             if (!this.showing || this.isSelection) return;
             this.hidePointer();
         });
@@ -109,15 +112,15 @@ export class Pointer extends Component {
     updateForTarget(element) {
         let inputText = this.pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${this.pointerSectionId}`) : `{{@${this.pageId}#${this.pointerSectionId}}}`;
         if (this.pointerModeLink && !inputText.startsWith('http')) {
-            inputText = window.location.protocol + "//" + window.location.host + inputText;
+            inputText = `${window.location.protocol}//${window.location.host}${inputText}`;
         }
 
-        this.container.querySelector('input').value = inputText;
+        this.input.value = inputText;
 
         // Update anchor if present
         const editAnchor = this.container.querySelector('#pointer-edit');
         if (editAnchor && element) {
-            const editHref = editAnchor.dataset.editHref;
+            const {editHref} = editAnchor.dataset;
             const elementId = element.id;
 
             // get the first 50 characters.
@@ -125,4 +128,5 @@ export class Pointer extends Component {
             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
         }
     }
-}
\ No newline at end of file
+
+}
index 4c20876f854dbfd8441b7cd4d3d8ffb38bc6c28d..6627365483e69f6c90b37d5eac7528f1f0c71be3 100644 (file)
@@ -1,6 +1,6 @@
-import {fadeIn, fadeOut} from "../services/animations";
-import {onSelect} from "../services/dom";
-import {Component} from "./component";
+import {fadeIn, fadeOut} from '../services/animations';
+import {onSelect} from '../services/dom';
+import {Component} from './component';
 
 /**
  * Popup window that will contain other content.
@@ -26,11 +26,11 @@ export class Popup extends Component {
 
         this.container.addEventListener('click', event => {
             if (event.target === this.container && lastMouseDownTarget === this.container) {
-                return this.hide();
+                this.hide();
             }
         });
 
-        onSelect(this.hideButtons, e => this.hide());
+        onSelect(this.hideButtons, () => this.hide());
     }
 
     hide(onComplete = null) {
@@ -47,7 +47,7 @@ export class Popup extends Component {
     show(onComplete = null, onHide = null) {
         fadeIn(this.container, 120, onComplete);
 
-        this.onkeyup = (event) => {
+        this.onkeyup = event => {
             if (event.key === 'Escape') {
                 this.hide();
             }
@@ -56,4 +56,4 @@ export class Popup extends Component {
         this.onHide = onHide;
     }
 
-}
\ No newline at end of file
+}
index 71b14badc137c3c81a1cc00b1faf1d6038f6b718..add2d0cdac33c433570eab64da7efceff52a53c4 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class SettingAppColorScheme extends Component {
 
@@ -14,7 +14,7 @@ export class SettingAppColorScheme extends Component {
             this.handleModeChange(newMode);
         });
 
-        const onInputChange = (event) => {
+        const onInputChange = event => {
             this.updateAppColorsFromInputs();
 
             if (event.target.name.startsWith('setting-app-color')) {
@@ -44,7 +44,7 @@ export class SettingAppColorScheme extends Component {
                 cssId = 'primary';
             }
 
-            const varName = '--color-' + cssId;
+            const varName = `--color-${cssId}`;
             document.body.style.setProperty(varName, input.value);
         }
     }
@@ -57,9 +57,8 @@ export class SettingAppColorScheme extends Component {
         const lightName = input.name.replace('-color', '-color-light');
         const hexVal = input.value;
         const rgb = this.hexToRgb(hexVal);
-        const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
+        const rgbLightVal = `rgba(${[rgb.r, rgb.g, rgb.b, '0.15'].join(',')})`;
 
-        console.log(input.name, lightName, hexVal, rgbLightVal)
         const lightColorInput = this.container.querySelector(`input[name="${lightName}"][type="hidden"]`);
         lightColorInput.value = rgbLightVal;
     }
@@ -75,7 +74,7 @@ export class SettingAppColorScheme extends Component {
         return {
             r: result ? parseInt(result[1], 16) : 0,
             g: result ? parseInt(result[2], 16) : 0,
-            b: result ? parseInt(result[3], 16) : 0
+            b: result ? parseInt(result[3], 16) : 0,
         };
     }
 
index bfb2c93cee4810d290ed2dfdb16303148082b048..bc47b96c0f9ff469962f7a65c2ea8f34060adc3b 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class SettingColorPicker extends Component {
 
@@ -17,4 +17,5 @@ export class SettingColorPicker extends Component {
         this.colorInput.value = value;
         this.colorInput.dispatchEvent(new Event('change', {bubbles: true}));
     }
-}
\ No newline at end of file
+
+}
index 992be9f826dad07c36b74abcdbc647ff53dc0ce2..6563ced0d725a47591fb55e1e1288e5d358383f4 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class SettingHomepageControl extends Component {
 
@@ -14,4 +14,5 @@ export class SettingHomepageControl extends Component {
         const showPagePicker = this.typeControl.value === 'page';
         this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
     }
-}
\ No newline at end of file
+
+}
index e4aefc5918bb41d0bcfb6be918263692afa62b19..01ca11a333f10289f1fd185dbce66bc3a704316b 100644 (file)
@@ -1,17 +1,17 @@
-import Sortable from "sortablejs";
-import {Component} from "./component";
+import Sortable from 'sortablejs';
+import {Component} from './component';
 
 /**
  * @type {Object<string, function(HTMLElement, HTMLElement, HTMLElement)>}
  */
 const itemActions = {
-    move_up(item, shelfBooksList, allBooksList) {
+    move_up(item) {
         const list = item.parentNode;
         const index = Array.from(list.children).indexOf(item);
         const newIndex = Math.max(index - 1, 0);
         list.insertBefore(item, list.children[newIndex] || null);
     },
-    move_down(item, shelfBooksList, allBooksList) {
+    move_down(item) {
         const list = item.parentNode;
         const index = Array.from(list.children).indexOf(item);
         const newIndex = Math.min(index + 2, list.children.length);
@@ -20,7 +20,7 @@ const itemActions = {
     remove(item, shelfBooksList, allBooksList) {
         allBooksList.appendChild(item);
     },
-    add(item, shelfBooksList, allBooksList) {
+    add(item, shelfBooksList) {
         shelfBooksList.appendChild(item);
     },
 };
@@ -62,11 +62,11 @@ export class ShelfSort extends Component {
             }
         });
 
-        this.bookSearchInput.addEventListener('input', event => {
+        this.bookSearchInput.addEventListener('input', () => {
             this.filterBooksByName(this.bookSearchInput.value);
         });
 
-        this.sortButtonContainer.addEventListener('click' , event => {
+        this.sortButtonContainer.addEventListener('click', event => {
             const button = event.target.closest('button[data-sort]');
             if (button) {
                 this.sortShelfBooks(button.dataset.sort);
@@ -78,11 +78,10 @@ export class ShelfSort extends Component {
      * @param {String} filterVal
      */
     filterBooksByName(filterVal) {
-
         // Set height on first search, if not already set, to prevent the distraction
         // of the list height jumping around
         if (!this.allBookList.style.height) {
-            this.allBookList.style.height = this.allBookList.getBoundingClientRect().height + 'px';
+            this.allBookList.style.height = `${this.allBookList.getBoundingClientRect().height}px`;
         }
 
         const books = this.allBookList.children;
@@ -100,7 +99,7 @@ export class ShelfSort extends Component {
      */
     sortItemActionClick(sortItemAction) {
         const sortItem = sortItemAction.closest('.scroll-box-item');
-        const action = sortItemAction.dataset.action;
+        const {action} = sortItemAction.dataset;
 
         const actionFunction = itemActions[action];
         actionFunction(sortItem, this.shelfBookList, this.allBookList);
@@ -122,10 +121,10 @@ export class ShelfSort extends Component {
             const bProp = bookB.dataset[sortProperty].toLowerCase();
 
             if (reverse) {
-                return aProp < bProp ? (aProp === bProp ? 0 : 1) : -1;
+                return bProp.localeCompare(aProp);
             }
 
-            return aProp < bProp ? (aProp === bProp ? 0 : -1) : 1;
+            return aProp.localeCompare(bProp);
         });
 
         for (const book of books) {
@@ -136,4 +135,4 @@ export class ShelfSort extends Component {
         this.onChange();
     }
 
-}
\ No newline at end of file
+}
index 2a2aaa225a518c34413df94de2851745129dfa9e..17e05fc8d140ae7fcd9e22b1310a80459fdfff1c 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 /**
  * Keys to ignore when recording shortcuts.
@@ -18,16 +18,16 @@ export class ShortcutInput extends Component {
         this.listenerRecordKey = this.listenerRecordKey.bind(this);
 
         this.input.addEventListener('focus', () => {
-             this.startListeningForInput();
+            this.startListeningForInput();
         });
 
         this.input.addEventListener('blur', () => {
             this.stopListeningForInput();
-        })
+        });
     }
 
     startListeningForInput() {
-        this.input.addEventListener('keydown', this.listenerRecordKey)
+        this.input.addEventListener('keydown', this.listenerRecordKey);
     }
 
     /**
@@ -51,4 +51,4 @@ export class ShortcutInput extends Component {
         this.input.removeEventListener('keydown', this.listenerRecordKey);
     }
 
-}
\ No newline at end of file
+}
index a87213b2e8968070b5a0934cc3db0e6a1ef75fa0..1d5bd51d7849a1b57c107b45802d90daf2fcbc38 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 function reverseMap(map) {
     const reversed = {};
@@ -8,7 +8,6 @@ function reverseMap(map) {
     return reversed;
 }
 
-
 export class Shortcuts extends Component {
 
     setup() {
@@ -19,13 +18,13 @@ export class Shortcuts extends Component {
         this.hintsShowing = false;
 
         this.hideHints = this.hideHints.bind(this);
+        this.hintAbortController = null;
 
         this.setupListeners();
     }
 
     setupListeners() {
         window.addEventListener('keydown', event => {
-
             if (event.target.closest('input, select, textarea')) {
                 return;
             }
@@ -35,7 +34,11 @@ export class Shortcuts extends Component {
 
         window.addEventListener('keydown', event => {
             if (event.key === '?') {
-                this.hintsShowing ? this.hideHints() : this.showHints();
+                if (this.hintsShowing) {
+                    this.hideHints();
+                } else {
+                    this.showHints();
+                }
             }
         });
     }
@@ -44,7 +47,6 @@ export class Shortcuts extends Component {
      * @param {KeyboardEvent} event
      */
     handleShortcutPress(event) {
-
         const keys = [
             event.ctrlKey ? 'Ctrl' : '',
             event.metaKey ? 'Cmd' : '',
@@ -90,7 +92,7 @@ export class Shortcuts extends Component {
             return true;
         }
 
-        console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
+        console.error('Shortcut attempted to be ran for element type that does not have handling setup', el);
 
         return false;
     }
@@ -113,10 +115,12 @@ export class Shortcuts extends Component {
             displayedIds.add(id);
         }
 
-        window.addEventListener('scroll', this.hideHints);
-        window.addEventListener('focus', this.hideHints);
-        window.addEventListener('blur', this.hideHints);
-        window.addEventListener('click', this.hideHints);
+        this.hintAbortController = new AbortController();
+        const signal = this.hintAbortController.signal;
+        window.addEventListener('scroll', this.hideHints, {signal});
+        window.addEventListener('focus', this.hideHints, {signal});
+        window.addEventListener('blur', this.hideHints, {signal});
+        window.addEventListener('click', this.hideHints, {signal});
 
         this.hintsShowing = true;
     }
@@ -135,10 +139,10 @@ export class Shortcuts extends Component {
 
         const linkage = document.createElement('div');
         linkage.classList.add('shortcut-linkage');
-        linkage.style.left = targetBounds.x + 'px';
-        linkage.style.top = targetBounds.y + 'px';
-        linkage.style.width = targetBounds.width + 'px';
-        linkage.style.height = targetBounds.height + 'px';
+        linkage.style.left = `${targetBounds.x}px`;
+        linkage.style.top = `${targetBounds.y}px`;
+        linkage.style.width = `${targetBounds.width}px`;
+        linkage.style.height = `${targetBounds.height}px`;
 
         wrapper.append(label, linkage);
 
@@ -151,12 +155,8 @@ export class Shortcuts extends Component {
     hideHints() {
         const wrapper = this.container.querySelector('.shortcut-container');
         wrapper.remove();
-
-        window.removeEventListener('scroll', this.hideHints);
-        window.removeEventListener('focus', this.hideHints);
-        window.removeEventListener('blur', this.hideHints);
-        window.removeEventListener('click', this.hideHints);
-
+        this.hintAbortController?.abort();
         this.hintsShowing = false;
     }
-}
\ No newline at end of file
+
+}
index bbbd92088ab9f3191e516cecae9833c2058b05a7..7b0c4f243b487ff36853f0cb73985d1be78e74ed 100644 (file)
@@ -1,5 +1,5 @@
-import Sortable from "sortablejs";
-import {Component} from "./component";
+import Sortable from 'sortablejs';
+import {Component} from './component';
 
 /**
  * SortableList
@@ -9,6 +9,7 @@ import {Component} from "./component";
  * the data to set on the data-transfer.
  */
 export class SortableList extends Component {
+
     setup() {
         this.container = this.$el;
         this.handleSelector = this.$opts.handleSelector;
@@ -33,4 +34,5 @@ export class SortableList extends Component {
             dragoverBubble: false,
         });
     }
-}
\ No newline at end of file
+
+}
index da4ac699608f21a11a5392dfd272a3492ae178a9..52faa1d10d46d2b1fe9701b68fd779b603ba9667 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 /**
  * Submit on change
@@ -9,8 +9,7 @@ export class SubmitOnChange extends Component {
     setup() {
         this.filter = this.$opts.filter;
 
-        this.$el.addEventListener('change', (event) => {
-
+        this.$el.addEventListener('change', event => {
             if (this.filter && !event.target.matches(this.filter)) {
                 return;
             }
@@ -22,4 +21,4 @@ export class SubmitOnChange extends Component {
         });
     }
 
-}
\ No newline at end of file
+}
index 8d22e3e9b57279648ce4c2bc45856ff351e84a79..560dc6273e3937eeb0744c3b8ae9210eb9bb6d92 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 /**
  * Tabs
@@ -46,4 +46,4 @@ export class Tabs extends Component {
         this.$emit('change', {showing: sectionId});
     }
 
-}
\ No newline at end of file
+}
index 24d34055327d506ce6728a28b384ab5f59ac2dce..1172e7775aae0e68d04625236d7254bbd941dda1 100644 (file)
@@ -1,6 +1,7 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class TagManager extends Component {
+
     setup() {
         this.addRemoveComponentEl = this.$refs.addRemove;
         this.container = this.$el;
@@ -11,8 +12,7 @@ export class TagManager extends Component {
 
     setupListeners() {
         this.container.addEventListener('input', event => {
-
-            /** @var {AddRemoveRows} **/
+            /** @var {AddRemoveRows} * */
             const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
             if (!this.hasEmptyRows() && event.target.value) {
                 addRemoveComponent.add();
@@ -22,9 +22,8 @@ export class TagManager extends Component {
 
     hasEmptyRows() {
         const rows = this.container.querySelectorAll(this.rowSelector);
-        const firstEmpty = [...rows].find(row => {
-            return [...row.querySelectorAll('input')].filter(input => input.value).length === 0;
-        });
+        const firstEmpty = [...rows].find(row => [...row.querySelectorAll('input')].filter(input => input.value).length === 0);
         return firstEmpty !== undefined;
     }
-}
\ No newline at end of file
+
+}
index 774336471f00c954ec74b6625566d886d2a1ac7b..56ec876d48bbb66a78e65038e4bfdd45a09759d5 100644 (file)
@@ -1,5 +1,5 @@
-import * as DOM from "../services/dom";
-import {Component} from "./component";
+import * as DOM from '../services/dom';
+import {Component} from './component';
 
 export class TemplateManager extends Component {
 
@@ -36,10 +36,10 @@ export class TemplateManager extends Component {
         });
 
         // Search submit button press
-        this.searchButton.addEventListener('click', event => this.performSearch());
+        this.searchButton.addEventListener('click', () => this.performSearch());
 
         // Search cancel button press
-        this.searchCancel.addEventListener('click', event => {
+        this.searchCancel.addEventListener('click', () => {
             this.searchInput.value = '';
             this.performSearch();
         });
@@ -66,7 +66,7 @@ export class TemplateManager extends Component {
 
     async insertTemplate(templateId, action = 'replace') {
         const resp = await window.$http.get(`/templates/${templateId}`);
-        const eventName = 'editor::' + action;
+        const eventName = `editor::${action}`;
         window.$events.emit(eventName, resp.data);
     }
 
@@ -79,10 +79,11 @@ export class TemplateManager extends Component {
 
     async performSearch() {
         const searchTerm = this.searchInput.value;
-        const resp = await window.$http.get(`/templates`, {
-            search: searchTerm
+        const resp = await window.$http.get('/templates', {
+            search: searchTerm,
         });
         this.searchCancel.style.display = searchTerm ? 'block' : 'none';
         this.list.innerHTML = resp.data;
     }
-}
\ No newline at end of file
+
+}
index b749eb54132492efa06b53a05a151fe35c282c3c..e8209eb279794d8a4138acbc68c3c5961ef6828e 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class ToggleSwitch extends Component {
 
@@ -18,4 +18,4 @@ export class ToggleSwitch extends Component {
         this.input.dispatchEvent(changeEvent);
     }
 
-}
\ No newline at end of file
+}
index ead2ac3d0dae85d4f865cd191d6ff768307eaa76..8ccefb06c482b09da5c7c0d4103f3d8307b8ee12 100644 (file)
@@ -1,4 +1,4 @@
-import {Component} from "./component";
+import {Component} from './component';
 
 export class TriLayout extends Component {
 
@@ -9,8 +9,8 @@ export class TriLayout extends Component {
         this.lastLayoutType = 'none';
         this.onDestroy = null;
         this.scrollCache = {
-            'content': 0,
-            'info': 0,
+            content: 0,
+            info: 0,
         };
         this.lastTabShown = 'content';
 
@@ -19,15 +19,15 @@ export class TriLayout extends Component {
 
         // Watch layout changes
         this.updateLayout();
-        window.addEventListener('resize', event => {
+        window.addEventListener('resize', () => {
             this.updateLayout();
         }, {passive: true});
     }
 
     updateLayout() {
         let newLayout = 'tablet';
-        if (window.innerWidth <= 1000) newLayout =  'mobile';
-        if (window.innerWidth >= 1400) newLayout =  'desktop';
+        if (window.innerWidth <= 1000) newLayout = 'mobile';
+        if (window.innerWidth >= 1400) newLayout = 'desktop';
         if (newLayout === this.lastLayoutType) return;
 
         if (this.onDestroy) {
@@ -53,20 +53,19 @@ export class TriLayout extends Component {
             for (const tab of this.tabs) {
                 tab.removeEventListener('click', this.mobileTabClick);
             }
-        }
+        };
     }
 
     setupDesktop() {
         //
     }
 
-
     /**
      * Action to run when the mobile info toggle bar is clicked/tapped
      * @param event
      */
     mobileTabClick(event) {
-        const tab = event.target.dataset.tab;
+        const {tab} = event.target.dataset;
         this.showTab(tab);
     }
 
@@ -109,4 +108,4 @@ export class TriLayout extends Component {
         this.lastTabShown = tabName;
     }
 
-}
\ No newline at end of file
+}
index d4d88a633c115ab06a9bd718e32ca41090efc109..e6adc3c23c82d4e163c12c4f4c42adfc1d80d249 100644 (file)
@@ -1,5 +1,5 @@
-import {onChildEvent} from "../services/dom";
-import {Component} from "./component";
+import {onChildEvent} from '../services/dom';
+import {Component} from './component';
 
 export class UserSelect extends Component {
 
@@ -20,9 +20,9 @@ export class UserSelect extends Component {
     }
 
     hide() {
-        /** @var {Dropdown} **/
+        /** @var {Dropdown} * */
         const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
         dropdown.hide();
     }
 
-}
\ No newline at end of file
+}
index ad8c59ac2abd7c73a1857602f1f465b754f016e9..68661972d3255a362de6796d48d11d14a6e8452b 100644 (file)
@@ -2,7 +2,7 @@
  * Webhook Events
  * Manages dynamic selection control in the webhook form interface.
  */
-import {Component} from "./component";
+import {Component} from './component';
 
 export class WebhookEvents extends Component {
 
@@ -27,4 +27,4 @@ export class WebhookEvents extends Component {
         }
     }
 
-}
\ No newline at end of file
+}
index 96731a0d9088d6b0009876140874a55768710ea5..21db207e6705451daf45d7a241210b63c1aa00be 100644 (file)
@@ -1,5 +1,5 @@
-import {build as buildEditorConfig} from "../wysiwyg/config";
-import {Component} from "./component";
+import {build as buildEditorConfig} from '../wysiwyg/config';
+import {Component} from './component';
 
 export class WysiwygEditor extends Component {
 
@@ -45,8 +45,8 @@ export class WysiwygEditor extends Component {
      */
     getContent() {
         return {
-            html: this.editor.getContent()
+            html: this.editor.getContent(),
         };
     }
 
-}
\ No newline at end of file
+}
index 9faf43de34143a69933e2aeeea5a2dbb333e2fb4..514bff87d86b87379a49a75035958f637394bd39 100644 (file)
@@ -1,6 +1,7 @@
-import DrawIO from "../services/drawio";
+import * as DrawIO from '../services/drawio';
 
 export class Actions {
+
     /**
      * @param {MarkdownEditor} editor
      */
@@ -13,7 +14,7 @@ export class Actions {
     }
 
     updateAndRender() {
-        const content = this.editor.cm.getValue();
+        const content = this.#getText();
         this.editor.config.inputEl.value = content;
 
         const html = this.editor.markdown.render(content);
@@ -28,50 +29,49 @@ export class Actions {
         return this.lastContent;
     }
 
-    insertImage() {
-        const cursorPos = this.editor.cm.getCursor('from');
-        /** @type {ImageManager} **/
+    showImageInsert() {
+        /** @type {ImageManager} * */
         const imageManager = window.$components.first('image-manager');
+
         imageManager.show(image => {
             const imageUrl = image.thumbs.display || image.url;
-            let selectedText = this.editor.cm.getSelection();
-            let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
-            this.editor.cm.focus();
-            this.editor.cm.replaceSelection(newText);
-            this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
+            const selectedText = this.#getSelectionText();
+            const newText = `[![${selectedText || image.name}](${imageUrl})](${image.url})`;
+            this.#replaceSelection(newText, newText.length);
         }, 'gallery');
     }
 
+    insertImage() {
+        const newText = `![${this.#getSelectionText()}](http://)`;
+        this.#replaceSelection(newText, newText.length - 1);
+    }
+
     insertLink() {
-        const cursorPos = this.editor.cm.getCursor('from');
-        const selectedText = this.editor.cm.getSelection() || '';
+        const selectedText = this.#getSelectionText();
         const newText = `[${selectedText}]()`;
-        this.editor.cm.focus();
-        this.editor.cm.replaceSelection(newText);
         const cursorPosDiff = (selectedText === '') ? -3 : -1;
-        this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
+        this.#replaceSelection(newText, newText.length + cursorPosDiff);
     }
 
     showImageManager() {
-        const cursorPos = this.editor.cm.getCursor('from');
-        /** @type {ImageManager} **/
+        const selectionRange = this.#getSelectionRange();
+        /** @type {ImageManager} * */
         const imageManager = window.$components.first('image-manager');
         imageManager.show(image => {
-            this.insertDrawing(image, cursorPos);
+            this.#insertDrawing(image, selectionRange);
         }, 'drawio');
     }
 
     // Show the popup link selector and insert a link when finished
     showLinkSelector() {
-        const cursorPos = this.editor.cm.getCursor('from');
-        /** @type {EntitySelectorPopup} **/
+        const selectionRange = this.#getSelectionRange();
+
+        /** @type {EntitySelectorPopup} * */
         const selector = window.$components.first('entity-selector-popup');
         selector.show(entity => {
-            let selectedText = this.editor.cm.getSelection() || entity.name;
-            let newText = `[${selectedText}](${entity.link})`;
-            this.editor.cm.focus();
-            this.editor.cm.replaceSelection(newText);
-            this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
+            const selectedText = this.#getSelectionText(selectionRange) || entity.name;
+            const newText = `[${selectedText}](${entity.link})`;
+            this.#replaceSelection(newText, newText.length, selectionRange);
         });
     }
 
@@ -80,19 +80,16 @@ export class Actions {
         const url = this.editor.config.drawioUrl;
         if (!url) return;
 
-        const cursorPos = this.editor.cm.getCursor('from');
-
-        DrawIO.show(url,() => {
-            return Promise.resolve('');
-        }, (pngData) => {
+        const selectionRange = this.#getSelectionRange();
 
+        DrawIO.show(url, () => Promise.resolve(''), pngData => {
             const data = {
                 image: pngData,
                 uploaded_to: Number(this.editor.config.pageId),
             };
 
-            window.$http.post("/images/drawio", data).then(resp => {
-                this.insertDrawing(resp.data, cursorPos);
+            window.$http.post('/images/drawio', data).then(resp => {
+                this.#insertDrawing(resp.data, selectionRange);
                 DrawIO.close();
             }).catch(err => {
                 this.handleDrawingUploadError(err);
@@ -100,43 +97,36 @@ export class Actions {
         });
     }
 
-    insertDrawing(image, originalCursor) {
+    #insertDrawing(image, originalSelectionRange) {
         const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
-        this.editor.cm.focus();
-        this.editor.cm.replaceSelection(newText);
-        this.editor.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
+        this.#replaceSelection(newText, newText.length, originalSelectionRange);
     }
 
     // Show draw.io if enabled and handle save.
     editDrawing(imgContainer) {
-        const drawioUrl = this.editor.config.drawioUrl;
+        const {drawioUrl} = this.editor.config;
         if (!drawioUrl) {
             return;
         }
 
-        const cursorPos = this.editor.cm.getCursor('from');
+        const selectionRange = this.#getSelectionRange();
         const drawingId = imgContainer.getAttribute('drawio-diagram');
 
-        DrawIO.show(drawioUrl, () => {
-            return DrawIO.load(drawingId);
-        }, (pngData) => {
-
+        DrawIO.show(drawioUrl, () => DrawIO.load(drawingId), pngData => {
             const data = {
                 image: pngData,
                 uploaded_to: Number(this.editor.config.pageId),
             };
 
-            window.$http.post("/images/drawio", data).then(resp => {
+            window.$http.post('/images/drawio', data).then(resp => {
                 const newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
-                const newContent = this.editor.cm.getValue().split('\n').map(line => {
+                const newContent = this.#getText().split('\n').map(line => {
                     if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
                         return newText;
                     }
                     return line;
                 }).join('\n');
-                this.editor.cm.setValue(newContent);
-                this.editor.cm.setCursor(cursorPos);
-                this.editor.cm.focus();
+                this.#setText(newContent, selectionRange);
                 DrawIO.close();
             }).catch(err => {
                 this.handleDrawingUploadError(err);
@@ -150,12 +140,12 @@ export class Actions {
         } else {
             window.$events.emit('error', this.editor.config.text.imageUploadError);
         }
-        console.log(error);
+        console.error(error);
     }
 
     // Make the editor full screen
     fullScreen() {
-        const container = this.editor.config.container;
+        const {container} = this.editor.config;
         const alreadyFullscreen = container.classList.contains('fullscreen');
         container.classList.toggle('fullscreen', !alreadyFullscreen);
         document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
@@ -167,29 +157,30 @@ export class Actions {
             return;
         }
 
-        const content = this.editor.cm.getValue();
-        const lines = content.split(/\r?\n/);
-        let lineNumber = lines.findIndex(line => {
-            return line && line.indexOf(searchText) !== -1;
-        });
+        const text = this.editor.cm.state.doc;
+        let lineCount = 1;
+        let scrollToLine = -1;
+        for (const line of text.iterLines()) {
+            if (line.includes(searchText)) {
+                scrollToLine = lineCount;
+                break;
+            }
+            lineCount += 1;
+        }
 
-        if (lineNumber === -1) {
+        if (scrollToLine === -1) {
             return;
         }
 
-        this.editor.cm.scrollIntoView({
-            line: lineNumber,
-        }, 200);
-        this.editor.cm.focus();
-        // set the cursor location.
-        this.editor.cm.setCursor({
-            line: lineNumber,
-            char: lines[lineNumber].length
-        })
+        const line = text.line(scrollToLine);
+        this.#setSelection(line.from, line.to, true);
+        this.focus();
     }
 
     focus() {
-        this.editor.cm.focus();
+        if (!this.editor.cm.hasFocus) {
+            this.editor.cm.focus();
+        }
     }
 
     /**
@@ -197,7 +188,7 @@ export class Actions {
      * @param {String} content
      */
     insertContent(content) {
-        this.editor.cm.replaceSelection(content);
+        this.#replaceSelection(content, content.length);
     }
 
     /**
@@ -205,11 +196,11 @@ export class Actions {
      * @param {String} content
      */
     prependContent(content) {
-        const cursorPos = this.editor.cm.getCursor('from');
-        const newContent = content + '\n' + this.editor.cm.getValue();
-        this.editor.cm.setValue(newContent);
-        const prependLineCount = content.split('\n').length;
-        this.editor.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
+        content = this.#cleanTextForEditor(content);
+        const selectionRange = this.#getSelectionRange();
+        const selectFrom = selectionRange.from + content.length + 1;
+        this.#dispatchChange(0, 0, `${content}\n`, selectFrom);
+        this.focus();
     }
 
     /**
@@ -217,10 +208,9 @@ export class Actions {
      * @param {String} content
      */
     appendContent(content) {
-        const cursorPos = this.editor.cm.getCursor('from');
-        const newContent = this.editor.cm.getValue() + '\n' + content;
-        this.editor.cm.setValue(newContent);
-        this.editor.cm.setCursor(cursorPos.line, cursorPos.ch);
+        content = this.#cleanTextForEditor(content);
+        this.#dispatchChange(this.editor.cm.state.doc.length, `\n${content}`);
+        this.focus();
     }
 
     /**
@@ -228,18 +218,7 @@ export class Actions {
      * @param {String} content
      */
     replaceContent(content) {
-        this.editor.cm.setValue(content);
-    }
-
-    /**
-     * @param {String|RegExp} search
-     * @param {String} replace
-     */
-    findAndReplaceContent(search, replace) {
-        const text = this.editor.cm.getValue();
-        const cursor = this.editor.cm.listSelections();
-        this.editor.cm.setValue(text.replace(search, replace));
-        this.editor.cm.setSelections(cursor);
+        this.#setText(content);
     }
 
     /**
@@ -247,51 +226,30 @@ export class Actions {
      * @param {String} newStart
      */
     replaceLineStart(newStart) {
-        const cursor = this.editor.cm.getCursor();
-        let lineContent = this.editor.cm.getLine(cursor.line);
-        const lineLen = lineContent.length;
+        const selectionRange = this.#getSelectionRange();
+        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
+
+        const lineContent = line.text;
         const lineStart = lineContent.split(' ')[0];
 
         // Remove symbol if already set
         if (lineStart === newStart) {
-            lineContent = lineContent.replace(`${newStart} `, '');
-            this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-            this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
+            const newLineContent = lineContent.replace(`${newStart} `, '');
+            const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
+            this.#dispatchChange(line.from, line.to, newLineContent, selectFrom);
             return;
         }
 
+        let newLineContent = lineContent;
         const alreadySymbol = /^[#>`]/.test(lineStart);
-        let posDif = 0;
         if (alreadySymbol) {
-            posDif = newStart.length - lineStart.length;
-            lineContent = lineContent.replace(lineStart, newStart).trim();
+            newLineContent = lineContent.replace(lineStart, newStart).trim();
         } else if (newStart !== '') {
-            posDif = newStart.length + 1;
-            lineContent = newStart + ' ' + lineContent;
+            newLineContent = `${newStart} ${lineContent}`;
         }
-        this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
-    }
 
-    /**
-     * Wrap the line in the given start and end contents.
-     * @param {String} start
-     * @param {String} end
-     */
-    wrapLine(start, end) {
-        const cursor = this.editor.cm.getCursor();
-        const lineContent = this.editor.cm.getLine(cursor.line);
-        const lineLen = lineContent.length;
-        let newLineContent = lineContent;
-
-        if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
-            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
-        } else {
-            newLineContent = `${start}${lineContent}${end}`;
-        }
-
-        this.editor.cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
+        const selectFrom = selectionRange.from + (newLineContent.length - lineContent.length);
+        this.#dispatchChange(line.from, line.to, newLineContent, selectFrom);
     }
 
     /**
@@ -300,37 +258,43 @@ export class Actions {
      * @param {String} end
      */
     wrapSelection(start, end) {
-        const selection = this.editor.cm.getSelection();
-        if (selection === '') return this.wrapLine(start, end);
+        const selectRange = this.#getSelectionRange();
+        const selectionText = this.#getSelectionText(selectRange);
+        if (!selectionText) {
+            this.#wrapLine(start, end);
+            return;
+        }
 
-        let newSelection = selection;
-        const frontDiff = 0;
-        let endDiff;
+        let newSelectionText = selectionText;
+        let newRange;
 
-        if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
-            newSelection = selection.slice(start.length, selection.length - end.length);
-            endDiff = -(end.length + start.length);
+        if (selectionText.startsWith(start) && selectionText.endsWith(end)) {
+            newSelectionText = selectionText.slice(start.length, selectionText.length - end.length);
+            newRange = selectRange.extend(selectRange.from, selectRange.to - (start.length + end.length));
         } else {
-            newSelection = `${start}${selection}${end}`;
-            endDiff = start.length + end.length;
+            newSelectionText = `${start}${selectionText}${end}`;
+            newRange = selectRange.extend(selectRange.from, selectRange.to + (start.length + end.length));
         }
 
-        const selections = this.editor.cm.listSelections()[0];
-        this.editor.cm.replaceSelection(newSelection);
-        const headFirst = selections.head.ch <= selections.anchor.ch;
-        selections.head.ch += headFirst ? frontDiff : endDiff;
-        selections.anchor.ch += headFirst ? endDiff : frontDiff;
-        this.editor.cm.setSelections([selections]);
+        this.#dispatchChange(
+            selectRange.from,
+            selectRange.to,
+            newSelectionText,
+            newRange.anchor,
+            newRange.head,
+        );
     }
 
     replaceLineStartForOrderedList() {
-        const cursor = this.editor.cm.getCursor();
-        const prevLineContent = this.editor.cm.getLine(cursor.line - 1) || '';
-        const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || [];
+        const selectionRange = this.#getSelectionRange();
+        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
+        const prevLine = this.editor.cm.state.doc.line(line.number - 1);
+
+        const listMatch = prevLine.text.match(/^(\s*)(\d)([).])\s/) || [];
 
         const number = (Number(listMatch[2]) || 0) + 1;
         const whiteSpace = listMatch[1] || '';
-        const listMark = listMatch[3] || '.'
+        const listMark = listMatch[3] || '.';
 
         const prefix = `${whiteSpace}${number}${listMark}`;
         return this.replaceLineStart(prefix);
@@ -341,118 +305,247 @@ export class Actions {
      * Creates a callout block if none existing, and removes it if cycling past the danger type.
      */
     cycleCalloutTypeAtSelection() {
-        const selectionRange = this.editor.cm.listSelections()[0];
-        const lineContent = this.editor.cm.getLine(selectionRange.anchor.line);
-        const lineLength = lineContent.length;
-        const contentRange = {
-            anchor: {line: selectionRange.anchor.line, ch: 0},
-            head: {line: selectionRange.anchor.line, ch: lineLength},
-        };
+        const selectionRange = this.#getSelectionRange();
+        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
 
         const formats = ['info', 'success', 'warning', 'danger'];
         const joint = formats.join('|');
         const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i');
-        const matches = regex.exec(lineContent);
+        const matches = regex.exec(line.text);
         const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();
 
         if (format === formats[formats.length - 1]) {
-            this.wrapLine(`<p class="callout ${formats[formats.length - 1]}">`, '</p>');
+            this.#wrapLine(`<p class="callout ${formats[formats.length - 1]}">`, '</p>');
         } else if (format === '') {
-            this.wrapLine('<p class="callout info">', '</p>');
+            this.#wrapLine('<p class="callout info">', '</p>');
         } else {
             const newFormatIndex = formats.indexOf(format) + 1;
             const newFormat = formats[newFormatIndex];
-            const newContent = lineContent.replace(matches[0], matches[0].replace(format, newFormat));
-            this.editor.cm.replaceRange(newContent, contentRange.anchor, contentRange.head);
+            const newContent = line.text.replace(matches[0], matches[0].replace(format, newFormat));
+            const lineDiff = newContent.length - line.text.length;
+            this.#dispatchChange(
+                line.from,
+                line.to,
+                newContent,
+                selectionRange.anchor + lineDiff,
+                selectionRange.head + lineDiff,
+            );
+        }
+    }
 
-            const chDiff = newContent.length - lineContent.length;
-            selectionRange.anchor.ch += chDiff;
-            if (selectionRange.anchor !== selectionRange.head) {
-                selectionRange.head.ch += chDiff;
-            }
-            this.editor.cm.setSelection(selectionRange.anchor, selectionRange.head);
+    syncDisplayPosition(event) {
+        // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
+        const scrollEl = event.target;
+        const atEnd = Math.abs(scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop) < 1;
+        if (atEnd) {
+            this.editor.display.scrollToIndex(-1);
+            return;
+        }
+
+        const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop);
+        const range = this.editor.cm.state.sliceDoc(0, blockInfo.from);
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');
+        const totalLines = doc.documentElement.querySelectorAll('body > *');
+        this.editor.display.scrollToIndex(totalLines.length);
+    }
+
+    /**
+     * Fetch and insert the template of the given ID.
+     * The page-relative position provided can be used to determine insert location if possible.
+     * @param {String} templateId
+     * @param {Number} posX
+     * @param {Number} posY
+     */
+    async insertTemplate(templateId, posX, posY) {
+        const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
+        const {data} = await window.$http.get(`/templates/${templateId}`);
+        const content = data.markdown || data.html;
+        this.#dispatchChange(cursorPos, cursorPos, content, cursorPos);
+    }
+
+    /**
+     * Insert multiple images from the clipboard from an event at the provided
+     * screen coordinates (Typically form a paste event).
+     * @param {File[]} images
+     * @param {Number} posX
+     * @param {Number} posY
+     */
+    insertClipboardImages(images, posX, posY) {
+        const cursorPos = this.editor.cm.posAtCoords({x: posX, y: posY}, false);
+        for (const image of images) {
+            this.uploadImage(image, cursorPos);
         }
     }
 
     /**
      * Handle image upload and add image into markdown content
      * @param {File} file
+     * @param {?Number} position
      */
-    uploadImage(file) {
+    async uploadImage(file, position = null) {
         if (file === null || file.type.indexOf('image') !== 0) return;
         let ext = 'png';
 
+        if (position === null) {
+            position = this.#getSelectionRange().from;
+        }
+
         if (file.name) {
-            let fileNameMatches = file.name.match(/\.(.+)$/);
+            const fileNameMatches = file.name.match(/\.(.+)$/);
             if (fileNameMatches.length > 1) ext = fileNameMatches[1];
         }
 
         // Insert image into markdown
-        const id = "image-" + Math.random().toString(16).slice(2);
+        const id = `image-${Math.random().toString(16).slice(2)}`;
         const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
-        const selectedText = this.editor.cm.getSelection();
-        const placeHolderText = `![${selectedText}](${placeholderImage})`;
-        const cursor = this.editor.cm.getCursor();
-        this.editor.cm.replaceSelection(placeHolderText);
-        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
+        const placeHolderText = `![](${placeholderImage})`;
+        this.#dispatchChange(position, position, placeHolderText, position);
 
-        const remoteFilename = "image-" + Date.now() + "." + ext;
+        const remoteFilename = `image-${Date.now()}.${ext}`;
         const formData = new FormData();
         formData.append('file', file, remoteFilename);
         formData.append('uploaded_to', this.editor.config.pageId);
 
-        window.$http.post('/images/gallery', formData).then(resp => {
-            const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
-            this.findAndReplaceContent(placeHolderText, newContent);
-        }).catch(err => {
+        try {
+            const {data} = await window.$http.post('/images/gallery', formData);
+            const newContent = `[![](${data.thumbs.display})](${data.url})`;
+            this.#findAndReplaceContent(placeHolderText, newContent);
+        } catch (err) {
             window.$events.emit('error', this.editor.config.text.imageUploadError);
-            this.findAndReplaceContent(placeHolderText, selectedText);
-            console.log(err);
-        });
+            this.#findAndReplaceContent(placeHolderText, '');
+            console.error(err);
+        }
     }
 
-    syncDisplayPosition() {
-        // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
-        const scroll = this.editor.cm.getScrollInfo();
-        const atEnd = scroll.top + scroll.clientHeight === scroll.height;
-        if (atEnd) {
-            this.editor.display.scrollToIndex(-1);
-            return;
-        }
+    /**
+     * Get the current text of the editor instance.
+     * @return {string}
+     */
+    #getText() {
+        return this.editor.cm.state.doc.toString();
+    }
 
-        const lineNum = this.editor.cm.lineAtHeight(scroll.top, 'local');
-        const range = this.editor.cm.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
-        const parser = new DOMParser();
-        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');
-        const totalLines = doc.documentElement.querySelectorAll('body > *');
-        this.editor.display.scrollToIndex(totalLines.length);
+    /**
+     * Set the text of the current editor instance.
+     * @param {String} text
+     * @param {?SelectionRange} selectionRange
+     */
+    #setText(text, selectionRange = null) {
+        selectionRange = selectionRange || this.#getSelectionRange();
+        this.#dispatchChange(0, this.editor.cm.state.doc.length, text, selectionRange.from);
+        this.focus();
     }
 
     /**
-     * Fetch and insert the template of the given ID.
-     * The page-relative position provided can be used to determine insert location if possible.
-     * @param {String} templateId
-     * @param {Number} posX
-     * @param {Number} posY
+     * Replace the current selection and focus the editor.
+     * Takes an offset for the cursor, after the change, relative to the start of the provided string.
+     * Can be provided a selection range to use instead of the current selection range.
+     * @param {String} newContent
+     * @param {Number} cursorOffset
+     * @param {?SelectionRange} selectionRange
      */
-    insertTemplate(templateId, posX, posY) {
-        const cursorPos = this.editor.cm.coordsChar({left: posX, top: posY});
-        this.editor.cm.setCursor(cursorPos);
-        window.$http.get(`/templates/${templateId}`).then(resp => {
-            const content = resp.data.markdown || resp.data.html;
-            this.editor.cm.replaceSelection(content);
-        });
+    #replaceSelection(newContent, cursorOffset = 0, selectionRange = null) {
+        selectionRange = selectionRange || this.editor.cm.state.selection.main;
+        const selectFrom = selectionRange.from + cursorOffset;
+        this.#dispatchChange(selectionRange.from, selectionRange.to, newContent, selectFrom);
+        this.focus();
     }
 
     /**
-     * Insert multiple images from the clipboard.
-     * @param {File[]} images
+     * Get the text content of the main current selection.
+     * @param {SelectionRange} selectionRange
+     * @return {string}
      */
-    insertClipboardImages(images) {
-        const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY});
-        this.editor.cm.setCursor(cursorPos);
-        for (const image of images) {
-            this.uploadImage(image);
+    #getSelectionText(selectionRange = null) {
+        selectionRange = selectionRange || this.#getSelectionRange();
+        return this.editor.cm.state.sliceDoc(selectionRange.from, selectionRange.to);
+    }
+
+    /**
+     * Get the range of the current main selection.
+     * @return {SelectionRange}
+     */
+    #getSelectionRange() {
+        return this.editor.cm.state.selection.main;
+    }
+
+    /**
+     * Cleans the given text to work with the editor.
+     * Standardises line endings to what's expected.
+     * @param {String} text
+     * @return {String}
+     */
+    #cleanTextForEditor(text) {
+        return text.replace(/\r\n|\r/g, '\n');
+    }
+
+    /**
+     * Find and replace the first occurrence of [search] with [replace]
+     * @param {String} search
+     * @param {String} replace
+     */
+    #findAndReplaceContent(search, replace) {
+        const newText = this.#getText().replace(search, replace);
+        this.#setText(newText);
+    }
+
+    /**
+     * Wrap the line in the given start and end contents.
+     * @param {String} start
+     * @param {String} end
+     */
+    #wrapLine(start, end) {
+        const selectionRange = this.#getSelectionRange();
+        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
+        const lineContent = line.text;
+        let newLineContent;
+        let lineOffset = 0;
+
+        if (lineContent.startsWith(start) && lineContent.endsWith(end)) {
+            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
+            lineOffset = -(start.length);
+        } else {
+            newLineContent = `${start}${lineContent}${end}`;
+            lineOffset = start.length;
+        }
+
+        this.#dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset);
+    }
+
+    /**
+     * Dispatch changes to the editor.
+     * @param {Number} from
+     * @param {?Number} to
+     * @param {?String} text
+     * @param {?Number} selectFrom
+     * @param {?Number} selectTo
+     */
+    #dispatchChange(from, to = null, text = null, selectFrom = null, selectTo = null) {
+        const tr = {changes: {from, to, insert: text}};
+
+        if (selectFrom) {
+            tr.selection = {anchor: selectFrom};
+            if (selectTo) {
+                tr.selection.head = selectTo;
+            }
         }
+
+        this.editor.cm.dispatch(tr);
+    }
+
+    /**
+     * Set the current selection range.
+     * Optionally will scroll the new range into view.
+     * @param {Number} from
+     * @param {Number} to
+     * @param {Boolean} scrollIntoView
+     */
+    #setSelection(from, to, scrollIntoView = false) {
+        this.editor.cm.dispatch({
+            selection: {anchor: from, head: to},
+            scrollIntoView,
+        });
     }
-}
\ No newline at end of file
+
+}
index 8724a23c89e33a04b8e0b700dd6395eeb9beaad0..9d54c19d754b52b6489bc9aad5bdb7299f4d8542 100644 (file)
@@ -1,6 +1,6 @@
-import {provide as provideShortcuts} from "./shortcuts";
-import {debounce} from "../services/util";
-import Clipboard from "../services/clipboard";
+import {provideKeyBindings} from './shortcuts';
+import {debounce} from '../services/util';
+import {Clipboard} from '../services/clipboard';
 
 /**
  * Initiate the codemirror instance for the markdown editor.
@@ -9,62 +9,67 @@ import Clipboard from "../services/clipboard";
  */
 export async function init(editor) {
     const Code = await window.importVersioned('code');
-    const cm = Code.markdownEditor(editor.config.inputEl);
 
-    // Will force to remain as ltr for now due to issues when HTML is in editor.
-    cm.setOption('direction', 'ltr');
-    // Register shortcuts
-    cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey()));
-
-
-    // Register codemirror events
-
-    // Update data on content change
-    cm.on('change', (instance, changeObj) => editor.actions.updateAndRender());
+    /**
+     * @param {ViewUpdate} v
+     */
+    function onViewUpdate(v) {
+        if (v.docChanged) {
+            editor.actions.updateAndRender();
+        }
+    }
 
-    // Handle scroll to sync display view
     const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
     let syncActive = editor.settings.get('scrollSync');
-    editor.settings.onChange('scrollSync', val => syncActive = val);
-    cm.on('scroll', instance => {
-        if (syncActive) {
-            onScrollDebounced(instance);
-        }
+    editor.settings.onChange('scrollSync', val => {
+        syncActive = val;
     });
 
-    // Handle image paste
-    cm.on('paste', (cm, event) => {
-        const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
-
-        // Don't handle the event ourselves if no items exist of contains table-looking data
-        if (!clipboard.hasItems() || clipboard.containsTabularData()) {
-            return;
-        }
+    const domEventHandlers = {
+        // Handle scroll to sync display view
+        scroll: event => syncActive && onScrollDebounced(event),
+        // Handle image & content drag n drop
+        drop: event => {
+            const templateId = event.dataTransfer.getData('bookstack/template');
+            if (templateId) {
+                event.preventDefault();
+                editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
+            }
 
-        const images = clipboard.getImages();
-        for (const image of images) {
-            editor.actions.uploadImage(image);
-        }
-    });
+            const clipboard = new Clipboard(event.dataTransfer);
+            const clipboardImages = clipboard.getImages();
+            if (clipboardImages.length > 0) {
+                event.stopPropagation();
+                event.preventDefault();
+                editor.actions.insertClipboardImages(clipboardImages, event.pageX, event.pageY);
+            }
+        },
+        // Handle image paste
+        paste: event => {
+            const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
 
-    // Handle image & content drag n drop
-    cm.on('drop', (cm, event) => {
+            // Don't handle the event ourselves if no items exist of contains table-looking data
+            if (!clipboard.hasItems() || clipboard.containsTabularData()) {
+                return;
+            }
 
-        const templateId = event.dataTransfer.getData('bookstack/template');
-        if (templateId) {
-            event.preventDefault();
-            editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
-        }
+            const images = clipboard.getImages();
+            for (const image of images) {
+                editor.actions.uploadImage(image);
+            }
+        },
+    };
 
-        const clipboard = new Clipboard(event.dataTransfer);
-        const clipboardImages = clipboard.getImages();
-        if (clipboardImages.length > 0) {
-            event.stopPropagation();
-            event.preventDefault();
-            editor.actions.insertClipboardImages(clipboardImages);
-        }
+    const cm = Code.markdownEditor(
+        editor.config.inputEl,
+        onViewUpdate,
+        domEventHandlers,
+        provideKeyBindings(editor),
+    );
 
-    });
+    // Add editor view to window for easy access/debugging.
+    // Not part of official API/Docs
+    window.mdEditorView = cm;
 
     return cm;
-}
\ No newline at end of file
+}
index 3afd03dd5fd2dd6baa8ce9691421d59f4f00274a..c3d803f7048c579b8c86f1e371e9c2b483927be0 100644 (file)
@@ -6,23 +6,22 @@ function getContentToInsert({html, markdown}) {
  * @param {MarkdownEditor} editor
  */
 export function listen(editor) {
-
-    window.$events.listen('editor::replace', (eventContent) => {
+    window.$events.listen('editor::replace', eventContent => {
         const markdown = getContentToInsert(eventContent);
         editor.actions.replaceContent(markdown);
     });
 
-    window.$events.listen('editor::append', (eventContent) => {
+    window.$events.listen('editor::append', eventContent => {
         const markdown = getContentToInsert(eventContent);
         editor.actions.appendContent(markdown);
     });
 
-    window.$events.listen('editor::prepend', (eventContent) => {
+    window.$events.listen('editor::prepend', eventContent => {
         const markdown = getContentToInsert(eventContent);
         editor.actions.prependContent(markdown);
     });
 
-    window.$events.listen('editor::insert', (eventContent) => {
+    window.$events.listen('editor::insert', eventContent => {
         const markdown = getContentToInsert(eventContent);
         editor.actions.insertContent(markdown);
     });
@@ -30,4 +29,4 @@ export function listen(editor) {
     window.$events.listen('editor::focus', () => {
         editor.actions.focus();
     });
-}
\ No newline at end of file
+}
index 2c78da1899fe75504e4578093ebd684c93c479bd..487df1cabcec4d5a9cf9d0c48d5b412d78ff9ee5 100644 (file)
@@ -1,4 +1,4 @@
-import {patchDomFromHtmlString} from "../services/vdom";
+import {patchDomFromHtmlString} from '../services/vdom';
 
 export class Display {
 
@@ -81,7 +81,7 @@ export class Display {
      * @param {String} html
      */
     patchWithHtml(html) {
-        const body = this.doc.body;
+        const {body} = this.doc;
 
         if (body.children.length === 0) {
             const wrap = document.createElement('div');
@@ -102,8 +102,8 @@ export class Display {
         const elems = this.doc.body?.children[0]?.children;
         if (elems && elems.length <= index) return;
 
-        const topElem = (index === -1) ? elems[elems.length-1] : elems[index];
-        topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
+        const topElem = (index === -1) ? elems[elems.length - 1] : elems[index];
+        topElem.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'smooth'});
     }
 
-}
\ No newline at end of file
+}
index 1cf4cef2bcf04494f0f617a90a43069326fb5985..46c35c850ac77b5e19277c00df784f6a415546c4 100644 (file)
@@ -1,10 +1,9 @@
-import {Markdown} from "./markdown";
-import {Display} from "./display";
-import {Actions} from "./actions";
-import {Settings} from "./settings";
-import {listen} from "./common-events";
-import {init as initCodemirror} from "./codemirror";
-
+import {Markdown} from './markdown';
+import {Display} from './display';
+import {Actions} from './actions';
+import {Settings} from './settings';
+import {listen} from './common-events';
+import {init as initCodemirror} from './codemirror';
 
 /**
  * Initiate a new markdown editor instance.
@@ -12,7 +11,6 @@ import {init as initCodemirror} from "./codemirror";
  * @returns {Promise<MarkdownEditor>}
  */
 export async function init(config) {
-
     /**
      * @type {MarkdownEditor}
      */
@@ -31,7 +29,6 @@ export async function init(config) {
     return editor;
 }
 
-
 /**
  * @typedef MarkdownEditorConfig
  * @property {String} pageId
@@ -49,6 +46,6 @@ export async function init(config) {
  * @property {Display} display
  * @property {Markdown} markdown
  * @property {Actions} actions
- * @property {CodeMirror} cm
+ * @property {EditorView} cm
  * @property {Settings} settings
- */
\ No newline at end of file
+ */
index ef3a02872eabecc99b7161c6a218e4fcab545527..e63184accaf141a8caa78414f8bcb4cfafa2e7d2 100644 (file)
@@ -1,4 +1,4 @@
-import MarkdownIt from "markdown-it";
+import MarkdownIt from 'markdown-it';
 import mdTasksLists from 'markdown-it-task-lists';
 
 export class Markdown {
@@ -24,7 +24,5 @@ export class Markdown {
     render(markdown) {
         return this.renderer.render(markdown);
     }
-}
-
-
 
+}
index 62aab82e9d5dda088bbcfe53dd26cba8227b1c49..b843aaa8a2b2bed55133a17c075473285db5c595 100644 (file)
@@ -21,7 +21,7 @@ export class Settings {
 
     listenToInputChanges(inputs) {
         for (const input of inputs) {
-            input.addEventListener('change', event => {
+            input.addEventListener('change', () => {
                 const name = input.getAttribute('name').replace('md-', '');
                 this.set(name, input.checked);
             });
@@ -59,4 +59,5 @@ export class Settings {
         listeners.push(callback);
         this.changeListeners[key] = listeners;
     }
-}
\ No newline at end of file
+
+}
index 17ffe2fb3a6fc69d94654fdcc63cc631d0af21f6..543e6dcdde67dd26ab8beb8f2987a71932151fbc 100644 (file)
@@ -1,48 +1,62 @@
 /**
- * Provide shortcuts for the given codemirror instance.
+ * Provide shortcuts for the editor instance.
  * @param {MarkdownEditor} editor
- * @param {String} metaKey
  * @returns {Object<String, Function>}
  */
-export function provide(editor, metaKey) {
+function provide(editor) {
     const shortcuts = {};
 
     // Insert Image shortcut
-    shortcuts[`${metaKey}-Alt-I`] = function(cm) {
-        const selectedText = cm.getSelection();
-        const newText = `![${selectedText}](http://)`;
-        const cursorPos = cm.getCursor('from');
-        cm.replaceSelection(newText);
-        cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
-    };
+    shortcuts['Shift-Mod-i'] = () => editor.actions.insertImage();
 
     // Save draft
-    shortcuts[`${metaKey}-S`] = cm => window.$events.emit('editor-save-draft');
+    shortcuts['Mod-s'] = () => window.$events.emit('editor-save-draft');
 
     // Save page
-    shortcuts[`${metaKey}-Enter`] = cm => window.$events.emit('editor-save-page');
+    shortcuts['Mod-Enter'] = () => window.$events.emit('editor-save-page');
 
     // Show link selector
-    shortcuts[`Shift-${metaKey}-K`] = cm => editor.actions.showLinkSelector();
+    shortcuts['Shift-Mod-k'] = () => editor.actions.showLinkSelector();
 
     // Insert Link
-    shortcuts[`${metaKey}-K`] = cm => editor.actions.insertLink();
+    shortcuts['Mod-k'] = () => editor.actions.insertLink();
 
     // FormatShortcuts
-    shortcuts[`${metaKey}-1`] = cm => editor.actions.replaceLineStart('##');
-    shortcuts[`${metaKey}-2`] = cm => editor.actions.replaceLineStart('###');
-    shortcuts[`${metaKey}-3`] = cm => editor.actions.replaceLineStart('####');
-    shortcuts[`${metaKey}-4`] = cm => editor.actions.replaceLineStart('#####');
-    shortcuts[`${metaKey}-5`] = cm => editor.actions.replaceLineStart('');
-    shortcuts[`${metaKey}-D`] = cm => editor.actions.replaceLineStart('');
-    shortcuts[`${metaKey}-6`] = cm => editor.actions.replaceLineStart('>');
-    shortcuts[`${metaKey}-Q`] = cm => editor.actions.replaceLineStart('>');
-    shortcuts[`${metaKey}-7`] = cm => editor.actions.wrapSelection('\n```\n', '\n```');
-    shortcuts[`${metaKey}-8`] = cm => editor.actions.wrapSelection('`', '`');
-    shortcuts[`Shift-${metaKey}-E`] = cm => editor.actions.wrapSelection('`', '`');
-    shortcuts[`${metaKey}-9`] = cm => editor.actions.cycleCalloutTypeAtSelection();
-    shortcuts[`${metaKey}-P`] = cm => editor.actions.replaceLineStart('-')
-    shortcuts[`${metaKey}-O`] = cm => editor.actions.replaceLineStartForOrderedList()
+    shortcuts['Mod-1'] = () => editor.actions.replaceLineStart('##');
+    shortcuts['Mod-2'] = () => editor.actions.replaceLineStart('###');
+    shortcuts['Mod-3'] = () => editor.actions.replaceLineStart('####');
+    shortcuts['Mod-4'] = () => editor.actions.replaceLineStart('#####');
+    shortcuts['Mod-5'] = () => editor.actions.replaceLineStart('');
+    shortcuts['Mod-d'] = () => editor.actions.replaceLineStart('');
+    shortcuts['Mod-6'] = () => editor.actions.replaceLineStart('>');
+    shortcuts['Mod-q'] = () => editor.actions.replaceLineStart('>');
+    shortcuts['Mod-7'] = () => editor.actions.wrapSelection('\n```\n', '\n```');
+    shortcuts['Mod-8'] = () => editor.actions.wrapSelection('`', '`');
+    shortcuts['Shift-Mod-e'] = () => editor.actions.wrapSelection('`', '`');
+    shortcuts['Mod-9'] = () => editor.actions.cycleCalloutTypeAtSelection();
+    shortcuts['Mod-p'] = () => editor.actions.replaceLineStart('-');
+    shortcuts['Mod-o'] = () => editor.actions.replaceLineStartForOrderedList();
 
     return shortcuts;
-}
\ No newline at end of file
+}
+
+/**
+ * Get the editor shortcuts in CodeMirror keybinding format.
+ * @param {MarkdownEditor} editor
+ * @return {{key: String, run: function, preventDefault: boolean}[]}
+ */
+export function provideKeyBindings(editor) {
+    const shortcuts = provide(editor);
+    const keyBindings = [];
+
+    const wrapAction = action => () => {
+        action();
+        return true;
+    };
+
+    for (const [shortcut, action] of Object.entries(shortcuts)) {
+        keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true});
+    }
+
+    return keyBindings;
+}
index 12b8077cf7dccec37dc48e6f83a7c6aa1ce43c86..bc983c8072aa529d7693018716c7b0ed255db087 100644 (file)
@@ -5,6 +5,53 @@
  */
 const animateStylesCleanupMap = new WeakMap();
 
+/**
+ * Animate the css styles of an element using FLIP animation techniques.
+ * Styles must be an object where the keys are style properties, camelcase, and the values
+ * are an array of two items in the format [initialValue, finalValue]
+ * @param {Element} element
+ * @param {Object} styles
+ * @param {Number} animTime
+ * @param {Function} onComplete
+ */
+function animateStyles(element, styles, animTime = 400, onComplete = null) {
+    const styleNames = Object.keys(styles);
+    for (const style of styleNames) {
+        element.style[style] = styles[style][0];
+    }
+
+    const cleanup = () => {
+        for (const style of styleNames) {
+            element.style[style] = null;
+        }
+        element.style.transition = null;
+        element.removeEventListener('transitionend', cleanup);
+        animateStylesCleanupMap.delete(element);
+        if (onComplete) onComplete();
+    };
+
+    setTimeout(() => {
+        element.style.transition = `all ease-in-out ${animTime}ms`;
+        for (const style of styleNames) {
+            element.style[style] = styles[style][1];
+        }
+
+        element.addEventListener('transitionend', cleanup);
+        animateStylesCleanupMap.set(element, cleanup);
+    }, 15);
+}
+
+/**
+ * Run the active cleanup action for the given element.
+ * @param {Element} element
+ */
+function cleanupExistingElementAnimation(element) {
+    if (animateStylesCleanupMap.has(element)) {
+        const oldCleanup = animateStylesCleanupMap.get(element);
+        oldCleanup();
+    }
+}
+
 /**
  * Fade in the given element.
  * @param {Element} element
@@ -15,7 +62,7 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
     cleanupExistingElementAnimation(element);
     element.style.display = 'block';
     animateStyles(element, {
-        opacity: ['0', '1']
+        opacity: ['0', '1'],
     }, animTime, () => {
         if (onComplete) onComplete();
     });
@@ -30,7 +77,7 @@ export function fadeIn(element, animTime = 400, onComplete = null) {
 export function fadeOut(element, animTime = 400, onComplete = null) {
     cleanupExistingElementAnimation(element);
     animateStyles(element, {
-        opacity: ['1', '0']
+        opacity: ['1', '0'],
     }, animTime, () => {
         element.style.display = 'none';
         if (onComplete) onComplete();
@@ -113,50 +160,3 @@ export function transitionHeight(element, animTime = 400) {
         animateStyles(element, animStyles, animTime);
     };
 }
-
-/**
- * Animate the css styles of an element using FLIP animation techniques.
- * Styles must be an object where the keys are style properties, camelcase, and the values
- * are an array of two items in the format [initialValue, finalValue]
- * @param {Element} element
- * @param {Object} styles
- * @param {Number} animTime
- * @param {Function} onComplete
- */
-function animateStyles(element, styles, animTime = 400, onComplete = null) {
-    const styleNames = Object.keys(styles);
-    for (let style of styleNames) {
-        element.style[style] = styles[style][0];
-    }
-
-    const cleanup = () => {
-        for (let style of styleNames) {
-            element.style[style] = null;
-        }
-        element.style.transition = null;
-        element.removeEventListener('transitionend', cleanup);
-        animateStylesCleanupMap.delete(element);
-        if (onComplete) onComplete();
-    };
-
-    setTimeout(() => {
-        element.style.transition = `all ease-in-out ${animTime}ms`;
-        for (let style of styleNames) {
-            element.style[style] = styles[style][1];
-        }
-
-        element.addEventListener('transitionend', cleanup);
-        animateStylesCleanupMap.set(element, cleanup);
-    }, 15);
-}
-
-/**
- * Run the active cleanup action for the given element.
- * @param {Element} element
- */
-function cleanupExistingElementAnimation(element) {
-    if (animateStylesCleanupMap.has(element)) {
-        const oldCleanup = animateStylesCleanupMap.get(element);
-        oldCleanup();
-    }
-}
\ No newline at end of file
index da921e51590d0c67e63b994e4cf1d4a25ae5e772..5f73c3020cf932685c1369cbe40070405079b19e 100644 (file)
@@ -1,5 +1,4 @@
-
-class Clipboard {
+export class Clipboard {
 
     /**
      * Constructor
@@ -21,7 +20,7 @@ class Clipboard {
      * @return {boolean}
      */
     containsTabularData() {
-        const rtfData = this.data.getData( 'text/rtf');
+        const rtfData = this.data.getData('text/rtf');
         return rtfData && rtfData.includes('\\trowd');
     }
 
@@ -30,8 +29,7 @@ class Clipboard {
      * @return {Array<File>}
      */
     getImages() {
-        const types = this.data.types;
-        const files = this.data.files;
+        const {types} = this.data;
         const images = [];
 
         for (const type of types) {
@@ -41,14 +39,35 @@ class Clipboard {
             }
         }
 
-        for (const file of files) {
-            if (file.type.includes('image')) {
-                images.push(file);
-            }
-        }
+        const imageFiles = this.getFiles().filter(f => f.type.includes('image'));
+        images.push(...imageFiles);
 
         return images;
     }
+
+    /**
+     * Get the files included in the clipboard data.
+     * @return {File[]}
+     */
+    getFiles() {
+        const {files} = this.data;
+        return [...files];
+    }
+
 }
 
-export default Clipboard;
\ No newline at end of file
+export async function copyTextToClipboard(text) {
+    if (window.isSecureContext && navigator.clipboard) {
+        await navigator.clipboard.writeText(text);
+        return;
+    }
+
+    // Backup option where we can't use the navigator.clipboard API
+    const tempInput = document.createElement('textarea');
+    tempInput.style = 'position: absolute; left: -1000px; top: -1000px;';
+    tempInput.value = text;
+    document.body.appendChild(tempInput);
+    tempInput.select();
+    document.execCommand('copy');
+    document.body.removeChild(tempInput);
+}
index d1503db4d4a9f20dc60e6f386e5f53a01c7b9708..beb0ce92fbf38863e91f27e03e8528f7e86cc378 100644 (file)
@@ -1,4 +1,4 @@
-import {kebabToCamel, camelToKebab} from "./text";
+import {kebabToCamel, camelToKebab} from './text';
 
 /**
  * A mapping of active components keyed by name, with values being arrays of component
@@ -19,44 +19,6 @@ const componentModelMap = {};
  */
 const elementComponentMap = new WeakMap();
 
-/**
- * Initialize a component instance on the given dom element.
- * @param {String} name
- * @param {Element} element
- */
-function initComponent(name, element) {
-    /** @type {Function<Component>|undefined} **/
-    const componentModel = componentModelMap[name];
-    if (componentModel === undefined) return;
-
-    // Create our component instance
-    /** @type {Component} **/
-    let instance;
-    try {
-        instance = new componentModel();
-        instance.$name = name;
-        instance.$el = element;
-        const allRefs = parseRefs(name, element);
-        instance.$refs = allRefs.refs;
-        instance.$manyRefs = allRefs.manyRefs;
-        instance.$opts = parseOpts(name, element);
-        instance.setup();
-    } catch (e) {
-        console.error('Failed to create component', e, name, element);
-    }
-
-    // Add to global listing
-    if (typeof components[name] === "undefined") {
-        components[name] = [];
-    }
-    components[name].push(instance);
-
-    // Add to element mapping
-    const elComponents = elementComponentMap.get(element) || {};
-    elComponents[name] = instance;
-    elementComponentMap.set(element, elComponents);
-}
-
 /**
  * Parse out the element references within the given element
  * for the given component name.
@@ -67,7 +29,7 @@ function parseRefs(name, element) {
     const refs = {};
     const manyRefs = {};
 
-    const prefix = `${name}@`
+    const prefix = `${name}@`;
     const selector = `[refs*="${prefix}"]`;
     const refElems = [...element.querySelectorAll(selector)];
     if (element.matches(selector)) {
@@ -93,13 +55,13 @@ function parseRefs(name, element) {
 
 /**
  * Parse out the element component options.
- * @param {String} name
+ * @param {String} componentName
  * @param {Element} element
  * @return {Object<String, String>}
  */
-function parseOpts(name, element) {
+function parseOpts(componentName, element) {
     const opts = {};
-    const prefix = `option:${name}:`;
+    const prefix = `option:${componentName}:`;
     for (const {name, value} of element.attributes) {
         if (name.startsWith(prefix)) {
             const optName = name.replace(prefix, '');
@@ -109,12 +71,50 @@ function parseOpts(name, element) {
     return opts;
 }
 
+/**
+ * Initialize a component instance on the given dom element.
+ * @param {String} name
+ * @param {Element} element
+ */
+function initComponent(name, element) {
+    /** @type {Function<Component>|undefined} * */
+    const ComponentModel = componentModelMap[name];
+    if (ComponentModel === undefined) return;
+
+    // Create our component instance
+    /** @type {Component} * */
+    let instance;
+    try {
+        instance = new ComponentModel();
+        instance.$name = name;
+        instance.$el = element;
+        const allRefs = parseRefs(name, element);
+        instance.$refs = allRefs.refs;
+        instance.$manyRefs = allRefs.manyRefs;
+        instance.$opts = parseOpts(name, element);
+        instance.setup();
+    } catch (e) {
+        console.error('Failed to create component', e, name, element);
+    }
+
+    // Add to global listing
+    if (typeof components[name] === 'undefined') {
+        components[name] = [];
+    }
+    components[name].push(instance);
+
+    // Add to element mapping
+    const elComponents = elementComponentMap.get(element) || {};
+    elComponents[name] = instance;
+    elementComponentMap.set(element, elComponents);
+}
+
 /**
  * Initialize all components found within the given element.
  * @param {Element|Document} parentElement
  */
 export function init(parentElement = document) {
-    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
+    const componentElems = parentElement.querySelectorAll('[component],[components]');
 
     for (const el of componentElems) {
         const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
@@ -162,4 +162,4 @@ export function get(name) {
 export function firstOnElement(element, name) {
     const elComponents = elementComponentMap.get(element) || {};
     return elComponents[name] || null;
-}
\ No newline at end of file
+}
index 119d8fa605850564c0d204188b67e441ac017892..2e6b34aeededad43ba2a57dd8cecea6d4f019d27 100644 (file)
@@ -1,24 +1,23 @@
-
 export function getCurrentDay() {
-    let date = new Date();
-    let month = date.getMonth() + 1;
-    let day = date.getDate();
+    const date = new Date();
+    const month = date.getMonth() + 1;
+    const day = date.getDate();
 
-    return `${date.getFullYear()}-${(month>9?'':'0') + month}-${(day>9?'':'0') + day}`;
+    return `${date.getFullYear()}-${(month > 9 ? '' : '0') + month}-${(day > 9 ? '' : '0') + day}`;
 }
 
 export function utcTimeStampToLocalTime(timestamp) {
-    let date = new Date(timestamp * 1000);
-    let hours = date.getHours();
-    let mins = date.getMinutes();
-    return `${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
+    const date = new Date(timestamp * 1000);
+    const hours = date.getHours();
+    const mins = date.getMinutes();
+    return `${(hours > 9 ? '' : '0') + hours}:${(mins > 9 ? '' : '0') + mins}`;
 }
 
 export function formatDateTime(date) {
-    let month = date.getMonth() + 1;
-    let day = date.getDate();
-    let hours = date.getHours();
-    let mins = date.getMinutes();
+    const month = date.getMonth() + 1;
+    const day = date.getDate();
+    const hours = date.getHours();
+    const mins = date.getMinutes();
 
-    return `${date.getFullYear()}-${(month>9?'':'0') + month}-${(day>9?'':'0') + day} ${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
-}
\ No newline at end of file
+    return `${date.getFullYear()}-${(month > 9 ? '' : '0') + month}-${(day > 9 ? '' : '0') + day} ${(hours > 9 ? '' : '0') + hours}:${(mins > 9 ? '' : '0') + mins}`;
+}
index 882d5228d7593abf80e4cb98c2e7c5133611d47b..78685574850643946fa8b305e479d3f3bc69b9df 100644 (file)
@@ -1,3 +1,29 @@
+/**
+ * Create a new element with the given attrs and children.
+ * Children can be a string for text nodes or other elements.
+ * @param {String} tagName
+ * @param {Object<String, String>} attrs
+ * @param {Element[]|String[]}children
+ * @return {*}
+ */
+export function elem(tagName, attrs = {}, children = []) {
+    const el = document.createElement(tagName);
+
+    for (const [key, val] of Object.entries(attrs)) {
+        el.setAttribute(key, val);
+    }
+
+    for (const child of children) {
+        if (typeof child === 'string') {
+            el.append(document.createTextNode(child));
+        } else {
+            el.append(child);
+        }
+    }
+
+    return el;
+}
+
 /**
  * Run the given callback against each element that matches the given selector.
  * @param {String} selector
@@ -5,7 +31,7 @@
  */
 export function forEach(selector, callback) {
     const elements = document.querySelectorAll(selector);
-    for (let element of elements) {
+    for (const element of elements) {
         callback(element);
     }
 }
@@ -17,7 +43,7 @@ export function forEach(selector, callback) {
  * @param {Function<Event>} callback
  */
 export function onEvents(listenerElement, events, callback) {
-    for (let eventName of events) {
+    for (const eventName of events) {
         listenerElement.addEventListener(eventName, callback);
     }
 }
@@ -35,7 +61,7 @@ export function onSelect(elements, callback) {
 
     for (const listenerElement of elements) {
         listenerElement.addEventListener('click', callback);
-        listenerElement.addEventListener('keydown', (event) => {
+        listenerElement.addEventListener('keydown', event => {
             if (event.key === 'Enter' || event.key === ' ') {
                 event.preventDefault();
                 callback(event);
@@ -58,7 +84,7 @@ export function onEnterPress(elements, callback) {
         if (event.key === 'Enter') {
             callback(event);
         }
-    }
+    };
 
     elements.forEach(e => e.addEventListener('keypress', listener));
 }
@@ -73,7 +99,7 @@ export function onEnterPress(elements, callback) {
  * @param {Function} callback
  */
 export function onChildEvent(listenerElement, childSelector, eventName, callback) {
-    listenerElement.addEventListener(eventName, function(event) {
+    listenerElement.addEventListener(eventName, event => {
         const matchingChild = event.target.closest(childSelector);
         if (matchingChild) {
             callback.call(matchingChild, event, matchingChild);
@@ -91,7 +117,7 @@ export function onChildEvent(listenerElement, childSelector, eventName, callback
 export function findText(selector, text) {
     const elements = document.querySelectorAll(selector);
     text = text.toLowerCase();
-    for (let element of elements) {
+    for (const element of elements) {
         if (element.textContent.toLowerCase().includes(text)) {
             return element;
         }
@@ -105,7 +131,18 @@ export function findText(selector, text) {
  * @param {Element} element
  */
 export function showLoading(element) {
-    element.innerHTML = `<div class="loading-container"><div></div><div></div><div></div></div>`;
+    element.innerHTML = '<div class="loading-container"><div></div><div></div><div></div></div>';
+}
+
+/**
+ * Get a loading element indicator element.
+ * @returns {Element}
+ */
+export function getLoading() {
+    const wrap = document.createElement('div');
+    wrap.classList.add('loading-container');
+    wrap.innerHTML = '<div></div><div></div><div></div>';
+    return wrap;
 }
 
 /**
@@ -130,4 +167,4 @@ export function htmlToDom(html) {
     wrap.innerHTML = html;
     window.$components.init(wrap);
     return wrap.children[0];
-}
\ No newline at end of file
+}
index 67ac4cc0ecb31fd51f52c785990149a17d6da5f4..efc071d3ec4b51a94b4060329b633d340b72ad97 100644 (file)
@@ -1,51 +1,12 @@
+// Docs: https://www.diagrams.net/doc/faq/embed-mode
+
 let iFrame = null;
 let lastApprovedOrigin;
-let onInit, onSave;
-
-/**
- * Show the draw.io editor.
- * @param {String} drawioUrl
- * @param {Function} onInitCallback - Must return a promise with the xml to load for the editor.
- * @param {Function} onSaveCallback - Is called with the drawing data on save.
- */
-function show(drawioUrl, onInitCallback, onSaveCallback) {
-    onInit = onInitCallback;
-    onSave = onSaveCallback;
-
-    iFrame = document.createElement('iframe');
-    iFrame.setAttribute('frameborder', '0');
-    window.addEventListener('message', drawReceive);
-    iFrame.setAttribute('src', drawioUrl);
-    iFrame.setAttribute('class', 'fullscreen');
-    iFrame.style.backgroundColor = '#FFFFFF';
-    document.body.appendChild(iFrame);
-    lastApprovedOrigin = (new URL(drawioUrl)).origin;
-}
-
-function close() {
-    drawEventClose();
-}
+let onInit; let
+    onSave;
 
-/**
- * Receive and handle a message event from the draw.io window.
- * @param {MessageEvent} event
- */
-function drawReceive(event) {
-    if (!event.data || event.data.length < 1) return;
-    if (event.origin !== lastApprovedOrigin) return;
-
-    const message = JSON.parse(event.data);
-    if (message.event === 'init') {
-        drawEventInit();
-    } else if (message.event === 'exit') {
-        drawEventClose();
-    } else if (message.event === 'save') {
-        drawEventSave(message);
-    } else if (message.event === 'export') {
-        drawEventExport(message);
-    } else if (message.event === 'configure') {
-        drawEventConfigure();
-    }
+function drawPostMessage(data) {
+    iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
 }
 
 function drawEventExport(message) {
@@ -55,13 +16,15 @@ function drawEventExport(message) {
 }
 
 function drawEventSave(message) {
-    drawPostMessage({action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing'});
+    drawPostMessage({
+        action: 'export', format: 'xmlpng', xml: message.xml, spin: 'Updating drawing',
+    });
 }
 
 function drawEventInit() {
     if (!onInit) return;
     onInit().then(xml => {
-        drawPostMessage({action: 'load', autosave: 1, xml: xml});
+        drawPostMessage({action: 'load', autosave: 1, xml});
     });
 }
 
@@ -72,29 +35,72 @@ function drawEventConfigure() {
 }
 
 function drawEventClose() {
+    // eslint-disable-next-line no-use-before-define
     window.removeEventListener('message', drawReceive);
     if (iFrame) document.body.removeChild(iFrame);
 }
 
-function drawPostMessage(data) {
-    iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
+/**
+ * Receive and handle a message event from the draw.io window.
+ * @param {MessageEvent} event
+ */
+function drawReceive(event) {
+    if (!event.data || event.data.length < 1) return;
+    if (event.origin !== lastApprovedOrigin) return;
+
+    const message = JSON.parse(event.data);
+    if (message.event === 'init') {
+        drawEventInit();
+    } else if (message.event === 'exit') {
+        drawEventClose();
+    } else if (message.event === 'save') {
+        drawEventSave(message);
+    } else if (message.event === 'export') {
+        drawEventExport(message);
+    } else if (message.event === 'configure') {
+        drawEventConfigure();
+    }
 }
 
-async function upload(imageData, pageUploadedToId) {
-    let data = {
+/**
+ * Show the draw.io editor.
+ * @param {String} drawioUrl
+ * @param {Function} onInitCallback - Must return a promise with the xml to load for the editor.
+ * @param {Function} onSaveCallback - Is called with the drawing data on save.
+ */
+export function show(drawioUrl, onInitCallback, onSaveCallback) {
+    onInit = onInitCallback;
+    onSave = onSaveCallback;
+
+    iFrame = document.createElement('iframe');
+    iFrame.setAttribute('frameborder', '0');
+    window.addEventListener('message', drawReceive);
+    iFrame.setAttribute('src', drawioUrl);
+    iFrame.setAttribute('class', 'fullscreen');
+    iFrame.style.backgroundColor = '#FFFFFF';
+    document.body.appendChild(iFrame);
+    lastApprovedOrigin = (new URL(drawioUrl)).origin;
+}
+
+export async function upload(imageData, pageUploadedToId) {
+    const data = {
         image: imageData,
         uploaded_to: pageUploadedToId,
     };
-    const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data);
+    const resp = await window.$http.post(window.baseUrl('/images/drawio'), data);
     return resp.data;
 }
 
+export function close() {
+    drawEventClose();
+}
+
 /**
  * Load an existing image, by fetching it as Base64 from the system.
  * @param drawingId
  * @returns {Promise<string>}
  */
-async function load(drawingId) {
+export async function load(drawingId) {
     try {
         const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
         return `data:image/png;base64,${resp.data.content}`;
@@ -106,5 +112,3 @@ async function load(drawingId) {
         throw error;
     }
 }
-
-export default {show, close, upload, load};
\ No newline at end of file
index d2dacfa7b7c3f9f50ac21a8ae6cd1e4651472f17..761305793a1cc4a91385c516edf0fcfa6acdcaf1 100644 (file)
@@ -6,13 +6,12 @@ const stack = [];
  * @param {String} eventName
  * @param {*} eventData
  */
-function emit(eventName, eventData) {
+export function emit(eventName, eventData) {
     stack.push({name: eventName, data: eventData});
-    if (typeof listeners[eventName] === 'undefined') return this;
-    let eventsToStart = listeners[eventName];
-    for (let i = 0; i < eventsToStart.length; i++) {
-        let event = eventsToStart[i];
-        event(eventData);
+
+    const listenersToRun = listeners[eventName] || [];
+    for (const listener of listenersToRun) {
+        listener(eventData);
     }
 }
 
@@ -22,7 +21,7 @@ function emit(eventName, eventData) {
  * @param {Function} callback
  * @returns {Events}
  */
-function listen(eventName, callback) {
+export function listen(eventName, callback) {
     if (typeof listeners[eventName] === 'undefined') listeners[eventName] = [];
     listeners[eventName].push(callback);
 }
@@ -34,43 +33,49 @@ function listen(eventName, callback) {
  * @param {String} eventName
  * @param {Object} eventData
  */
-function emitPublic(targetElement, eventName, eventData) {
+export function emitPublic(targetElement, eventName, eventData) {
     const event = new CustomEvent(eventName, {
         detail: eventData,
-        bubbles: true
+        bubbles: true,
     });
     targetElement.dispatchEvent(event);
 }
 
+/**
+ * Emit a success event with the provided message.
+ * @param {String} message
+ */
+export function success(message) {
+    emit('success', message);
+}
+
+/**
+ * Emit an error event with the provided message.
+ * @param {String} message
+ */
+export function error(message) {
+    emit('error', message);
+}
+
 /**
  * Notify of standard server-provided validation errors.
- * @param {Object} error
+ * @param {Object} responseErr
  */
-function showValidationErrors(error) {
-    if (!error.status) return;
-    if (error.status === 422 && error.data) {
-        const message = Object.values(error.data).flat().join('\n');
-        emit('error', message);
+export function showValidationErrors(responseErr) {
+    if (!responseErr.status) return;
+    if (responseErr.status === 422 && responseErr.data) {
+        const message = Object.values(responseErr.data).flat().join('\n');
+        error(message);
     }
 }
 
 /**
  * Notify standard server-provided error messages.
- * @param {Object} error
+ * @param {Object} responseErr
  */
-function showResponseError(error) {
-    if (!error.status) return;
-    if (error.status >= 400 && error.data && error.data.message) {
-        emit('error', error.data.message);
+export function showResponseError(responseErr) {
+    if (!responseErr.status) return;
+    if (responseErr.status >= 400 && responseErr.data && responseErr.data.message) {
+        error(responseErr.data.message);
     }
 }
-
-export default {
-    emit,
-    emitPublic,
-    listen,
-    success: (msg) => emit('success', msg),
-    error: (msg) => emit('error', msg),
-    showValidationErrors,
-    showResponseError,
-}
\ No newline at end of file
index 211ea0c92b75ceaca9fa869527e8563758bb21b6..49d5b6df4e37ab176d3601722dc3f01bab4d06bd 100644 (file)
@@ -1,90 +1,69 @@
-
 /**
- * Perform a HTTP GET request.
- * Can easily pass query parameters as the second parameter.
- * @param {String} url
- * @param {Object} params
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @typedef FormattedResponse
+ * @property {Headers} headers
+ * @property {Response} original
+ * @property {Object|String} data
+ * @property {Boolean} redirected
+ * @property {Number} status
+ * @property {string} statusText
+ * @property {string} url
  */
-async function get(url, params = {}) {
-    return request(url, {
-        method: 'GET',
-        params,
-    });
-}
 
 /**
- * Perform a HTTP POST request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * Get the content from a fetch response.
+ * Checks the content-type header to determine the format.
+ * @param {Response} response
+ * @returns {Promise<Object|String>}
  */
-async function post(url, data = null) {
-    return dataRequest('POST', url, data);
-}
+async function getResponseContent(response) {
+    if (response.status === 204) {
+        return null;
+    }
 
-/**
- * Perform a HTTP PUT request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function put(url, data = null) {
-    return dataRequest('PUT', url, data);
-}
+    const responseContentType = response.headers.get('Content-Type') || '';
+    const subType = responseContentType.split(';')[0].split('/').pop();
 
-/**
- * Perform a HTTP PATCH request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function patch(url, data = null) {
-    return dataRequest('PATCH', url, data);
+    if (subType === 'javascript' || subType === 'json') {
+        return response.json();
+    }
+
+    return response.text();
 }
 
-/**
- * Perform a HTTP DELETE request.
- * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
- */
-async function performDelete(url, data = null) {
-    return dataRequest('DELETE', url, data);
+export class HttpError extends Error {
+
+    constructor(response, content) {
+        super(response.statusText);
+        this.data = content;
+        this.headers = response.headers;
+        this.redirected = response.redirected;
+        this.status = response.status;
+        this.statusText = response.statusText;
+        this.url = response.url;
+        this.original = response;
+    }
+
 }
 
 /**
- * Perform a HTTP request to the back-end that includes data in the body.
- * Parses the body to JSON if an object, setting the correct headers.
  * @param {String} method
  * @param {String} url
- * @param {Object} data
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @param {Object} events
+ * @return {XMLHttpRequest}
  */
-async function dataRequest(method, url, data = null) {
-    const options = {
-        method: method,
-        body: data,
-    };
+export function createXMLHttpRequest(method, url, events = {}) {
+    const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
+    const req = new XMLHttpRequest();
 
-    // Send data as JSON if a plain object
-    if (typeof data === 'object' && !(data instanceof FormData)) {
-        options.headers = {
-            'Content-Type': 'application/json',
-            'X-Requested-With': 'XMLHttpRequest',
-        };
-        options.body = JSON.stringify(data);
+    for (const [eventName, callback] of Object.entries(events)) {
+        req.addEventListener(eventName, callback.bind(req));
     }
 
-    // Ensure FormData instances are sent over POST
-    // Since Laravel does not read multipart/form-data from other types
-    // of request. Hence the addition of the magic _method value.
-    if (data instanceof FormData && method !== 'post') {
-        data.append('_method', method);
-        options.method = 'post';
-    }
+    req.open(method, url);
+    req.withCredentials = true;
+    req.setRequestHeader('X-CSRF-TOKEN', csrfToken);
 
-    return request(url, options)
+    return req;
 }
 
 /**
@@ -92,34 +71,35 @@ async function dataRequest(method, url, data = null) {
  * to communicate with the back-end. Parses & formats the response.
  * @param {String} url
  * @param {Object} options
- * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>}
+ * @returns {Promise<FormattedResponse>}
  */
 async function request(url, options = {}) {
-    if (!url.startsWith('http')) {
-        url = window.baseUrl(url);
+    let requestUrl = url;
+
+    if (!requestUrl.startsWith('http')) {
+        requestUrl = window.baseUrl(requestUrl);
     }
 
     if (options.params) {
-        const urlObj = new URL(url);
-        for (let paramName of Object.keys(options.params)) {
+        const urlObj = new URL(requestUrl);
+        for (const paramName of Object.keys(options.params)) {
             const value = options.params[paramName];
             if (typeof value !== 'undefined' && value !== null) {
                 urlObj.searchParams.set(paramName, value);
             }
         }
-        url = urlObj.toString();
+        requestUrl = urlObj.toString();
     }
 
     const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
-    options = Object.assign({}, options, {
-        'credentials': 'same-origin',
-    });
-    options.headers = Object.assign({}, options.headers || {}, {
-        'baseURL': window.baseUrl(''),
+    const requestOptions = {...options, credentials: 'same-origin'};
+    requestOptions.headers = {
+        ...requestOptions.headers || {},
+        baseURL: window.baseUrl(''),
         'X-CSRF-TOKEN': csrfToken,
-    });
+    };
 
-    const response = await fetch(url, options);
+    const response = await fetch(requestUrl, requestOptions);
     const content = await getResponseContent(response);
     const returnData = {
         data: content,
@@ -139,44 +119,91 @@ async function request(url, options = {}) {
 }
 
 /**
- * Get the content from a fetch response.
- * Checks the content-type header to determine the format.
- * @param {Response} response
- * @returns {Promise<Object|String>}
+ * Perform a HTTP request to the back-end that includes data in the body.
+ * Parses the body to JSON if an object, setting the correct headers.
+ * @param {String} method
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
  */
-async function getResponseContent(response) {
-    if (response.status === 204) {
-        return null;
-    }
+async function dataRequest(method, url, data = null) {
+    const options = {
+        method,
+        body: data,
+    };
 
-    const responseContentType = response.headers.get('Content-Type') || '';
-    const subType = responseContentType.split(';')[0].split('/').pop();
+    // Send data as JSON if a plain object
+    if (typeof data === 'object' && !(data instanceof FormData)) {
+        options.headers = {
+            'Content-Type': 'application/json',
+            'X-Requested-With': 'XMLHttpRequest',
+        };
+        options.body = JSON.stringify(data);
+    }
 
-    if (subType === 'javascript' || subType === 'json') {
-        return await response.json();
+    // Ensure FormData instances are sent over POST
+    // Since Laravel does not read multipart/form-data from other types
+    // of request. Hence the addition of the magic _method value.
+    if (data instanceof FormData && method !== 'post') {
+        data.append('_method', method);
+        options.method = 'post';
     }
 
-    return await response.text();
+    return request(url, options);
 }
 
-class HttpError extends Error {
-    constructor(response, content) {
-        super(response.statusText);
-        this.data = content;
-        this.headers = response.headers;
-        this.redirected = response.redirected;
-        this.status = response.status;
-        this.statusText = response.statusText;
-        this.url = response.url;
-        this.original = response;
-    }
+/**
+ * Perform a HTTP GET request.
+ * Can easily pass query parameters as the second parameter.
+ * @param {String} url
+ * @param {Object} params
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function get(url, params = {}) {
+    return request(url, {
+        method: 'GET',
+        params,
+    });
+}
+
+/**
+ * Perform a HTTP POST request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function post(url, data = null) {
+    return dataRequest('POST', url, data);
+}
+
+/**
+ * Perform a HTTP PUT request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function put(url, data = null) {
+    return dataRequest('PUT', url, data);
+}
+
+/**
+ * Perform a HTTP PATCH request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+export async function patch(url, data = null) {
+    return dataRequest('PATCH', url, data);
+}
+
+/**
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
+ */
+async function performDelete(url, data = null) {
+    return dataRequest('DELETE', url, data);
 }
 
-export default {
-    get: get,
-    post: post,
-    put: put,
-    patch: patch,
-    delete: performDelete,
-    HttpError: HttpError,
-};
\ No newline at end of file
+export {performDelete as delete};
index 0f866ceaaacd368ad317eb7e14a7b572ce96f93e..34111bb2d37886e3d98a7e150fd7ada800ba186b 100644 (file)
@@ -57,7 +57,6 @@ export class KeyboardNavigationHandler {
      * @param {KeyboardEvent} event
      */
     #keydownHandler(event) {
-
         // Ignore certain key events in inputs to allow text editing.
         if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
             return;
@@ -72,7 +71,7 @@ export class KeyboardNavigationHandler {
         } else if (event.key === 'Escape') {
             if (this.onEscape) {
                 this.onEscape(event);
-            } else if  (document.activeElement) {
+            } else if (document.activeElement) {
                 document.activeElement.blur();
             }
         } else if (event.key === 'Enter' && this.onEnter) {
@@ -88,8 +87,9 @@ export class KeyboardNavigationHandler {
         const focusable = [];
         const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
         for (const container of this.containers) {
-            focusable.push(...container.querySelectorAll(selector))
+            focusable.push(...container.querySelectorAll(selector));
         }
         return focusable;
     }
-}
\ No newline at end of file
+
+}
index ea82f993e8f82dcee5673f13bb183f9bc74e3d51..d5e6fa7987438e1ab771ec63dba124d463440540 100644 (file)
@@ -4,7 +4,7 @@
  * @returns {string}
  */
 export function kebabToCamel(kebab) {
-    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
+    const ucFirst = word => word.slice(0, 1).toUpperCase() + word.slice(1);
     const words = kebab.split('-');
     return words[0] + words.slice(1).map(ucFirst).join('');
 }
@@ -15,5 +15,5 @@ export function kebabToCamel(kebab) {
  * @returns {String}
  */
 export function camelToKebab(camelStr) {
-    return camelStr.replace(/[A-Z]/g, (str, offset) =>  (offset > 0 ? '-' : '') + str.toLowerCase());
-}
\ No newline at end of file
+    return camelStr.replace(/[A-Z]/g, (str, offset) => (offset > 0 ? '-' : '') + str.toLowerCase());
+}
index 62bb51f56aacb5f0216e0e4621ffdfcae0d34481..e562a9152e91924e4707f63ce43e4460648b4a7a 100644 (file)
@@ -5,11 +5,7 @@
  */
 class Translator {
 
-    /**
-     * Create an instance, Passing in the required translations
-     * @param translations
-     */
-    constructor(translations) {
+    constructor() {
         this.store = new Map();
         this.parseTranslations();
     }
@@ -19,7 +15,7 @@ class Translator {
      */
     parseTranslations() {
         const translationMetaTags = document.querySelectorAll('meta[name="translation"]');
-        for (let tag of translationMetaTags) {
+        for (const tag of translationMetaTags) {
             const key = tag.getAttribute('key');
             const value = tag.getAttribute('value');
             this.store.set(key, value);
@@ -27,7 +23,7 @@ class Translator {
     }
 
     /**
-     * Get a translation, Same format as laravel's 'trans' helper
+     * Get a translation, Same format as Laravel's 'trans' helper
      * @param key
      * @param replacements
      * @returns {*}
@@ -38,8 +34,8 @@ class Translator {
     }
 
     /**
-     * Get pluralised text, Dependant on the given count.
-     * Same format at laravel's 'trans_choice' helper.
+     * Get pluralised text, Dependent on the given count.
+     * Same format at Laravel's 'trans_choice' helper.
      * @param key
      * @param count
      * @param replacements
@@ -52,7 +48,7 @@ class Translator {
 
     /**
      * Parse the given translation and find the correct plural option
-     * to use. Similar format at laravel's 'trans_choice' helper.
+     * to use. Similar format at Laravel's 'trans_choice' helper.
      * @param {String} translation
      * @param {Number} count
      * @param {Object} replacements
@@ -64,7 +60,7 @@ class Translator {
         const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
         let result = null;
 
-        for (let t of splitText) {
+        for (const t of splitText) {
             // Parse exact matches
             const exactMatches = t.match(exactCountRegex);
             if (exactMatches !== null && Number(exactMatches[1]) === count) {
@@ -117,14 +113,17 @@ class Translator {
      */
     performReplacements(string, replacements) {
         if (!replacements) return string;
-        const replaceMatches = string.match(/:([\S]+)/g);
+        const replaceMatches = string.match(/:(\S+)/g);
         if (replaceMatches === null) return string;
+        let updatedString = string;
+
         replaceMatches.forEach(match => {
             const key = match.substring(1);
             if (typeof replacements[key] === 'undefined') return;
-            string = string.replace(match, replacements[key]);
+            updatedString = updatedString.replace(match, replacements[key]);
         });
-        return string;
+
+        return updatedString;
     }
 
 }
index 238f8b1d88c7b4d1f2be0d684a9f816be9461949..dd97d81aaf47113b301660794eda9b881d54e1e8 100644 (file)
@@ -1,5 +1,3 @@
-
-
 /**
  * Returns a function, that, as long as it continues to be invoked, will not
  * be triggered. The function will be called after it stops being called for
@@ -13,9 +11,9 @@
  */
 export function debounce(func, wait, immediate) {
     let timeout;
-    return function() {
-        const context = this, args = arguments;
-        const later = function() {
+    return function debouncedWrapper(...args) {
+        const context = this;
+        const later = function debouncedTimeout() {
             timeout = null;
             if (!immediate) func.apply(context, args);
         };
@@ -24,7 +22,7 @@ export function debounce(func, wait, immediate) {
         timeout = setTimeout(later, wait);
         if (callNow) func.apply(context, args);
     };
-};
+}
 
 /**
  * Scroll and highlight an element.
@@ -55,11 +53,11 @@ export function scrollAndHighlightElement(element) {
  */
 export function escapeHtml(unsafe) {
     return unsafe
-        .replace(/&/g, "&amp;")
-        .replace(/</g, "&lt;")
-        .replace(/>/g, "&gt;")
-        .replace(/"/g, "&quot;")
-        .replace(/'/g, "&#039;");
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#039;');
 }
 
 /**
@@ -68,6 +66,7 @@ export function escapeHtml(unsafe) {
  * @returns {string}
  */
 export function uniqueId() {
-    const S4 = () => (((1+Math.random())*0x10000)|0).toString(16).substring(1);
-    return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
-}
\ No newline at end of file
+    // eslint-disable-next-line no-bitwise
+    const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+    return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
+}
index 89a809084caaace4971ce98afbc56fd45407231d..2095cd2ca4f3ff73cff6c3fe40808fc53f1b96b7 100644 (file)
@@ -1,8 +1,8 @@
 import {
     init,
     attributesModule,
-    toVNode
-} from "snabbdom";
+    toVNode,
+} from 'snabbdom';
 
 let patcher;
 
@@ -12,7 +12,6 @@ let patcher;
 function getPatcher() {
     if (patcher) return patcher;
 
-
     patcher = init([
         attributesModule,
     ]);
@@ -28,4 +27,4 @@ export function patchDomFromHtmlString(domTarget, html) {
     const contentDom = document.createElement('div');
     contentDom.innerHTML = html;
     getPatcher()(toVNode(domTarget), toVNode(contentDom));
-}
\ No newline at end of file
+}
index a25debac123d75c515814400f75ee44144600f40..d0a5acdc24d8ffd67204e54b7f11b0f299e81243 100644 (file)
@@ -2,7 +2,6 @@
  * @param {Editor} editor
  */
 export function listen(editor) {
-
     // Replace editor content
     window.$events.listen('editor::replace', ({html}) => {
         editor.setContent(html);
@@ -31,4 +30,4 @@ export function listen(editor) {
             editor.focus();
         }
     });
-}
\ No newline at end of file
+}
index 85c1919d494e8bcd776dcbe9fd93ebebf5b61184..a0e7156ee6866bdbecdbe9dd43f057aa26b228ff 100644 (file)
@@ -1,32 +1,35 @@
-import {register as registerShortcuts} from "./shortcuts";
-import {listen as listenForCommonEvents} from "./common-events";
-import {scrollToQueryString} from "./scrolling";
-import {listenForDragAndPaste} from "./drop-paste-handling";
-import {getPrimaryToolbar, registerAdditionalToolbars} from "./toolbars";
-import {registerCustomIcons} from "./icons";
+import {register as registerShortcuts} from './shortcuts';
+import {listen as listenForCommonEvents} from './common-events';
+import {scrollToQueryString} from './scrolling';
+import {listenForDragAndPaste} from './drop-paste-handling';
+import {getPrimaryToolbar, registerAdditionalToolbars} from './toolbars';
+import {registerCustomIcons} from './icons';
 
-import {getPlugin as getCodeeditorPlugin} from "./plugin-codeeditor";
-import {getPlugin as getDrawioPlugin} from "./plugin-drawio";
-import {getPlugin as getCustomhrPlugin} from "./plugins-customhr";
-import {getPlugin as getImagemanagerPlugin} from "./plugins-imagemanager";
-import {getPlugin as getAboutPlugin} from "./plugins-about";
-import {getPlugin as getDetailsPlugin} from "./plugins-details";
-import {getPlugin as getTasklistPlugin} from "./plugins-tasklist";
+import {getPlugin as getCodeeditorPlugin} from './plugin-codeeditor';
+import {getPlugin as getDrawioPlugin} from './plugin-drawio';
+import {getPlugin as getCustomhrPlugin} from './plugins-customhr';
+import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager';
+import {getPlugin as getAboutPlugin} from './plugins-about';
+import {getPlugin as getDetailsPlugin} from './plugins-details';
+import {getPlugin as getTasklistPlugin} from './plugins-tasklist';
 
-const style_formats = [
-    {title: "Large Header", format: "h2", preview: 'color: blue;'},
-    {title: "Medium Header", format: "h3"},
-    {title: "Small Header", format: "h4"},
-    {title: "Tiny Header", format: "h5"},
-    {title: "Paragraph", format: "p", exact: true, classes: ''},
-    {title: "Blockquote", format: "blockquote"},
+const styleFormats = [
+    {title: 'Large Header', format: 'h2', preview: 'color: blue;'},
+    {title: 'Medium Header', format: 'h3'},
+    {title: 'Small Header', format: 'h4'},
+    {title: 'Tiny Header', format: 'h5'},
     {
-        title: "Callouts", items: [
-            {title: "Information", format: 'calloutinfo'},
-            {title: "Success", format: 'calloutsuccess'},
-            {title: "Warning", format: 'calloutwarning'},
-            {title: "Danger", format: 'calloutdanger'}
-        ]
+        title: 'Paragraph', format: 'p', exact: true, classes: '',
+    },
+    {title: 'Blockquote', format: 'blockquote'},
+    {
+        title: 'Callouts',
+        items: [
+            {title: 'Information', format: 'calloutinfo'},
+            {title: 'Success', format: 'calloutsuccess'},
+            {title: 'Warning', format: 'calloutwarning'},
+            {title: 'Danger', format: 'calloutdanger'},
+        ],
     },
 ];
 
@@ -37,10 +40,10 @@ const formats = {
     calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
     calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
     calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
-    calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
+    calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}},
 };
 
-const color_map = [
+const colorMap = [
     '#BFEDD2', '',
     '#FBEEB8', '',
     '#F8CAC6', '',
@@ -66,14 +69,13 @@ const color_map = [
     '#34495E', '',
 
     '#000000', '',
-    '#ffffff', ''
+    '#ffffff', '',
 ];
 
-function file_picker_callback(callback, value, meta) {
-
+function filePickerCallback(callback, value, meta) {
     // field_name, url, type, win
     if (meta.filetype === 'file') {
-        /** @type {EntitySelectorPopup} **/
+        /** @type {EntitySelectorPopup} * */
         const selector = window.$components.first('entity-selector-popup');
         selector.show(entity => {
             callback(entity.link, {
@@ -85,13 +87,12 @@ function file_picker_callback(callback, value, meta) {
 
     if (meta.filetype === 'image') {
         // Show image manager
-        /** @type {ImageManager} **/
+        /** @type {ImageManager} * */
         const imageManager = window.$components.first('image-manager');
-        imageManager.show(function (image) {
+        imageManager.show(image => {
             callback(image.url, {alt: image.name});
         }, 'gallery');
     }
-
 }
 
 /**
@@ -100,30 +101,30 @@ function file_picker_callback(callback, value, meta) {
  */
 function gatherPlugins(options) {
     const plugins = [
-        "image",
-        "table",
-        "link",
-        "autolink",
-        "fullscreen",
-        "code",
-        "customhr",
-        "autosave",
-        "lists",
-        "codeeditor",
-        "media",
-        "imagemanager",
-        "about",
-        "details",
-        "tasklist",
+        'image',
+        'table',
+        'link',
+        'autolink',
+        'fullscreen',
+        'code',
+        'customhr',
+        'autosave',
+        'lists',
+        'codeeditor',
+        'media',
+        'imagemanager',
+        'about',
+        'details',
+        'tasklist',
         options.textDirection === 'rtl' ? 'directionality' : '',
     ];
 
-    window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin(options));
-    window.tinymce.PluginManager.add('customhr', getCustomhrPlugin(options));
-    window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin(options));
-    window.tinymce.PluginManager.add('about', getAboutPlugin(options));
-    window.tinymce.PluginManager.add('details', getDetailsPlugin(options));
-    window.tinymce.PluginManager.add('tasklist', getTasklistPlugin(options));
+    window.tinymce.PluginManager.add('codeeditor', getCodeeditorPlugin());
+    window.tinymce.PluginManager.add('customhr', getCustomhrPlugin());
+    window.tinymce.PluginManager.add('imagemanager', getImagemanagerPlugin());
+    window.tinymce.PluginManager.add('about', getAboutPlugin());
+    window.tinymce.PluginManager.add('details', getDetailsPlugin());
+    window.tinymce.PluginManager.add('tasklist', getTasklistPlugin());
 
     if (options.drawioUrl) {
         window.tinymce.PluginManager.add('drawio', getDrawioPlugin(options));
@@ -137,11 +138,11 @@ function gatherPlugins(options) {
  * Fetch custom HTML head content from the parent page head into the editor.
  */
 function fetchCustomHeadContent() {
-    const headContentLines = document.head.innerHTML.split("\n");
+    const headContentLines = document.head.innerHTML.split('\n');
     const startLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- Start: custom user content -->');
     const endLineIndex = headContentLines.findIndex(line => line.trim() === '<!-- End: custom user content -->');
     if (startLineIndex === -1 || endLineIndex === -1) {
-        return ''
+        return '';
     }
     return headContentLines.slice(startLineIndex + 1, endLineIndex).join('\n');
 }
@@ -152,10 +153,10 @@ function fetchCustomHeadContent() {
  * @param {Editor} editor
  */
 function setupBrFilter(editor) {
-    editor.serializer.addNodeFilter('br', function(nodes) {
+    editor.serializer.addNodeFilter('br', nodes => {
         for (const node of nodes) {
             if (node.parent && node.parent.name === 'code') {
-                const newline = tinymce.html.Node.create('#text');
+                const newline = window.tinymce.html.Node.create('#text');
                 newline.value = '\n';
                 node.replace(newline);
             }
@@ -168,7 +169,14 @@ function setupBrFilter(editor) {
  * @return {function(Editor)}
  */
 function getSetupCallback(options) {
-    return function(editor) {
+    return function setupCallback(editor) {
+        function editorChange() {
+            if (options.darkMode) {
+                editor.contentDocument.documentElement.classList.add('dark-mode');
+            }
+            window.$events.emit('editor-html-change', '');
+        }
+
         editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
         listenForCommonEvents(editor);
         listenForDragAndPaste(editor, options);
@@ -184,13 +192,6 @@ function getSetupCallback(options) {
             setupBrFilter(editor);
         });
 
-        function editorChange() {
-            if (options.darkMode) {
-                editor.contentDocument.documentElement.classList.add('dark-mode');
-            }
-            window.$events.emit('editor-html-change', '');
-        }
-
         // Custom handler hook
         window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor});
 
@@ -200,9 +201,9 @@ function getSetupCallback(options) {
             icon: 'sourcecode',
             onAction() {
                 editor.execCommand('mceToggleFormat', false, 'code');
-            }
-        })
-    }
+            },
+        });
+    };
 }
 
 /**
@@ -229,7 +230,6 @@ body {
  * @return {Object}
  */
 export function build(options) {
-
     // Set language
     window.tinymce.addI18n(options.language, options.translationMap);
 
@@ -241,7 +241,7 @@ export function build(options) {
         width: '100%',
         height: '100%',
         selector: '#html-editor',
-        cache_suffix: '?version=' + version,
+        cache_suffix: `?version=${version}`,
         content_css: [
             window.baseUrl('/dist/styles.css'),
         ],
@@ -263,18 +263,18 @@ export function build(options) {
         automatic_uploads: false,
         custom_elements: 'doc-root,code-block',
         valid_children: [
-            "-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]",
-            "+div[pre|img]",
-            "-doc-root[doc-root|#text]",
-            "-li[details]",
-            "+code-block[pre]",
-            "+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]"
+            '-div[p|h1|h2|h3|h4|h5|h6|blockquote|code-block]',
+            '+div[pre|img]',
+            '-doc-root[doc-root|#text]',
+            '-li[details]',
+            '+code-block[pre]',
+            '+doc-root[p|h1|h2|h3|h4|h5|h6|blockquote|code-block|div]',
         ].join(','),
         plugins: gatherPlugins(options),
         contextmenu: false,
         toolbar: getPrimaryToolbar(options),
         content_style: getContentStyle(options),
-        style_formats,
+        style_formats: styleFormats,
         style_formats_merge: false,
         media_alt_source: false,
         media_poster: false,
@@ -282,10 +282,10 @@ export function build(options) {
         table_style_by_css: true,
         table_use_colgroups: true,
         file_picker_types: 'file image',
-        color_map,
-        file_picker_callback,
+        color_map: colorMap,
+        file_picker_callback: filePickerCallback,
         paste_preprocess(plugin, args) {
-            const content = args.content;
+            const {content} = args;
             if (content.indexOf('<img src="file://') !== -1) {
                 args.content = '';
             }
@@ -296,7 +296,7 @@ export function build(options) {
         },
         setup(editor) {
             registerCustomIcons(editor);
-            registerAdditionalToolbars(editor, options);
+            registerAdditionalToolbars(editor);
             getSetupCallback(options)(editor);
         },
     };
@@ -312,4 +312,4 @@ export function build(options) {
  * @property {int} pageId
  * @property {Object} translations
  * @property {Object} translationMap
- */
\ No newline at end of file
+ */
index 866d25a24e828a5fbcbf99ab1bb51a950563bc69..33078cd1d5687b74fb877ef8dd2719dd4e0f59ff 100644 (file)
@@ -1,4 +1,4 @@
-import Clipboard from "../services/clipboard";
+import {Clipboard} from '../services/clipboard';
 
 let wrap;
 let draggedContentEditable;
@@ -7,6 +7,25 @@ function hasTextContent(node) {
     return node && !!(node.textContent || node.innerText);
 }
 
+/**
+ * Upload an image file to the server
+ * @param {File} file
+ * @param {int} pageId
+ */
+async function uploadImageFile(file, pageId) {
+    if (file === null || file.type.indexOf('image') !== 0) {
+        throw new Error('Not an image file');
+    }
+
+    const remoteFilename = file.name || `image-${Date.now()}.png`;
+    const formData = new FormData();
+    formData.append('file', file, remoteFilename);
+    formData.append('uploaded_to', pageId);
+
+    const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
+    return resp.data;
+}
+
 /**
  * Handle pasting images from clipboard.
  * @param {Editor} editor
@@ -23,8 +42,7 @@ function paste(editor, options, event) {
 
     const images = clipboard.getImages();
     for (const imageFile of images) {
-
-        const id = "image-" + Math.random().toString(16).slice(2);
+        const id = `image-${Math.random().toString(16).slice(2)}`;
         const loadingImage = window.baseUrl('/loading.gif');
         event.preventDefault();
 
@@ -44,37 +62,17 @@ function paste(editor, options, event) {
             }).catch(err => {
                 editor.dom.remove(id);
                 window.$events.emit('error', options.translations.imageUploadErrorText);
-                console.log(err);
+                console.error(err);
             });
         }, 10);
     }
 }
 
-/**
- * Upload an image file to the server
- * @param {File} file
- * @param {int} pageId
- */
-async function uploadImageFile(file, pageId) {
-    if (file === null || file.type.indexOf('image') !== 0) {
-        throw new Error(`Not an image file`);
-    }
-
-    const remoteFilename = file.name || `image-${Date.now()}.png`;
-    const formData = new FormData();
-    formData.append('file', file, remoteFilename);
-    formData.append('uploaded_to', pageId);
-
-    const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
-    return resp.data;
-}
-
 /**
  * @param {Editor} editor
- * @param {WysiwygConfigOptions} options
  */
-function dragStart(editor, options) {
-    let node = editor.selection.getNode();
+function dragStart(editor) {
+    const node = editor.selection.getNode();
 
     if (node.nodeName === 'IMG') {
         wrap = editor.dom.getParent(node, '.mceTemp');
@@ -96,8 +94,12 @@ function dragStart(editor, options) {
  * @param {DragEvent} event
  */
 function drop(editor, options, event) {
-    let dom = editor.dom,
-        rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
+    const {dom} = editor;
+    const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
+        event.clientX,
+        event.clientY,
+        editor.getDoc(),
+    );
 
     // Template insertion
     const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
@@ -105,7 +107,7 @@ function drop(editor, options, event) {
         event.preventDefault();
         window.$http.get(`/templates/${templateId}`).then(resp => {
             editor.selection.setRng(rng);
-            editor.undoManager.transact(function () {
+            editor.undoManager.transact(() => {
                 editor.execCommand('mceInsertContent', false, resp.data.html);
             });
         });
@@ -117,7 +119,7 @@ function drop(editor, options, event) {
     } else if (wrap) {
         event.preventDefault();
 
-        editor.undoManager.transact(function () {
+        editor.undoManager.transact(() => {
             editor.selection.setRng(rng);
             editor.selection.setNode(wrap);
             dom.remove(wrap);
@@ -127,7 +129,7 @@ function drop(editor, options, event) {
     // Handle contenteditable section drop
     if (!event.isDefaultPrevented() && draggedContentEditable) {
         event.preventDefault();
-        editor.undoManager.transact(function () {
+        editor.undoManager.transact(() => {
             const selectedNode = editor.selection.getNode();
             const range = editor.selection.getRng();
             const selectedNodeRoot = selectedNode.closest('body > *');
@@ -152,7 +154,7 @@ function drop(editor, options, event) {
  * @param {WysiwygConfigOptions} options
  */
 export function listenForDragAndPaste(editor, options) {
-    editor.on('dragstart', () => dragStart(editor, options));
-    editor.on('drop',  event => drop(editor, options, event));
+    editor.on('dragstart', () => dragStart(editor));
+    editor.on('drop', event => drop(editor, options, event));
     editor.on('paste', event => paste(editor, options, event));
-}
\ No newline at end of file
+}
index 2c2457fe15f585bf15569c6767a85bfca03145f1..3045df2278e8aae4e9c51548b364099ae5bacc9d 100644 (file)
@@ -5,17 +5,15 @@ const icons = {
     'table-insert-column-before': '<svg width="24" height="24"><path d="M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z"/></svg>',
     'table-insert-row-above': '<svg width="24" height="24"><path d="M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z"/></svg>',
     'table-insert-row-after': '<svg width="24" height="24"><path d="M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z"/></svg>',
-    'table': '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2ZM5 14v5h6v-5zm14 0h-6v5h6zm0-7h-6v5h6zM5 12h6V7H5Z"/></svg>',
+    table: '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2ZM5 14v5h6v-5zm14 0h-6v5h6zm0-7h-6v5h6zM5 12h6V7H5Z"/></svg>',
     'table-delete-table': '<svg width="24" height="24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z"/><path d="m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z" style="fill-rule:nonzero;stroke-width:1.20992"/></svg>',
 };
 
-
 /**
  * @param {Editor} editor
  */
 export function registerCustomIcons(editor) {
-
     for (const [name, svg] of Object.entries(icons)) {
         editor.ui.registry.addIcon(name, svg);
     }
-}
\ No newline at end of file
+}
index cd0078b1d914da39ccdf3df993527f44f7ce4626..fa3804ea8368b8a8afd52631937de2023fe95606 100644 (file)
@@ -10,8 +10,8 @@ function elemIsCodeBlock(elem) {
  */
 function showPopup(editor, code, language, callback) {
     window.$components.first('code-editor').open(code, language, (newCode, newLang) => {
-        callback(newCode, newLang)
-        editor.focus()
+        callback(newCode, newLang);
+        editor.focus();
     });
 }
 
@@ -36,6 +36,12 @@ function defineCodeBlockCustomElement(editor) {
     const win = doc.defaultView;
 
     class CodeBlockElement extends win.HTMLElement {
+
+        /**
+         * @type {?SimpleEditorInterface}
+         */
+        editor = null;
+
         constructor() {
             super();
             this.attachShadow({mode: 'open'});
@@ -47,12 +53,13 @@ function defineCodeBlockCustomElement(editor) {
             cmContainer.style.pointerEvents = 'none';
             cmContainer.contentEditable = 'false';
             cmContainer.classList.add('CodeMirrorContainer');
+            cmContainer.classList.toggle('dark-mode', document.documentElement.classList.contains('dark-mode'));
 
             this.shadowRoot.append(...copiedStyles, cmContainer);
         }
 
         getLanguage() {
-            const getLanguageFromClassList = (classes) => {
+            const getLanguageFromClassList = classes => {
                 const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
                 return (langClasses[0] || '').replace('language-', '');
             };
@@ -63,11 +70,9 @@ function defineCodeBlockCustomElement(editor) {
         }
 
         setContent(content, language) {
-            if (this.cm) {
-                importVersioned('code').then(Code => {
-                    Code.setContent(this.cm, content);
-                    Code.setMode(this.cm, language, content);
-                });
+            if (this.editor) {
+                this.editor.setContent(content);
+                this.editor.setMode(language, content);
             }
 
             let pre = this.querySelector('pre');
@@ -98,7 +103,7 @@ function defineCodeBlockCustomElement(editor) {
 
         connectedCallback() {
             const connectedTime = Date.now();
-            if (this.cm) {
+            if (this.editor) {
                 return;
             }
 
@@ -109,15 +114,16 @@ function defineCodeBlockCustomElement(editor) {
             this.style.height = `${height}px`;
 
             const container = this.shadowRoot.querySelector('.CodeMirrorContainer');
-            const renderCodeMirror = (Code) => {
-                this.cm = Code.wysiwygView(container, content, this.getLanguage());
-                setTimeout(() => Code.updateLayout(this.cm), 10);
-                setTimeout(() => this.style.height = null, 12);
+            const renderEditor = Code => {
+                this.editor = Code.wysiwygView(container, this.shadowRoot, content, this.getLanguage());
+                setTimeout(() => {
+                    this.style.height = null;
+                }, 12);
             };
 
-            window.importVersioned('code').then((Code) => {
+            window.importVersioned('code').then(Code => {
                 const timeout = (Date.now() - connectedTime < 20) ? 20 : 0;
-                setTimeout(() => renderCodeMirror(Code), timeout);
+                setTimeout(() => renderEditor(Code), timeout);
             });
         }
 
@@ -131,26 +137,24 @@ function defineCodeBlockCustomElement(editor) {
                 }
             }
         }
+
     }
 
     win.customElements.define('code-block', CodeBlockElement);
 }
 
-
 /**
  * @param {Editor} editor
- * @param {String} url
  */
-function register(editor, url) {
-
-    editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>')
+function register(editor) {
+    editor.ui.registry.addIcon('codeblock', '<svg width="24" height="24"><path d="M4 3h16c.6 0 1 .4 1 1v16c0 .6-.4 1-1 1H4a1 1 0 0 1-1-1V4c0-.6.4-1 1-1Zm1 2v14h14V5Z"/><path d="M11.103 15.423c.277.277.277.738 0 .922a.692.692 0 0 1-1.106 0l-4.057-3.78a.738.738 0 0 1 0-1.107l4.057-3.872c.276-.277.83-.277 1.106 0a.724.724 0 0 1 0 1.014L7.6 12.012ZM12.897 8.577c-.245-.312-.2-.675.08-.955.28-.281.727-.27 1.027.033l4.057 3.78a.738.738 0 0 1 0 1.107l-4.057 3.872c-.277.277-.83.277-1.107 0a.724.724 0 0 1 0-1.014l3.504-3.412z"/></svg>');
 
     editor.ui.registry.addButton('codeeditor', {
         tooltip: 'Insert code block',
         icon: 'codeblock',
         onAction() {
             editor.execCommand('codeeditor');
-        }
+        },
     });
 
     editor.ui.registry.addButton('editcodeeditor', {
@@ -158,7 +162,7 @@ function register(editor, url) {
         icon: 'edit-block',
         onAction() {
             editor.execCommand('codeeditor');
-        }
+        },
     });
 
     editor.addCommand('codeeditor', () => {
@@ -180,17 +184,17 @@ function register(editor, url) {
         }
     });
 
-    editor.on('dblclick', event => {
-        let selectedNode = editor.selection.getNode();
+    editor.on('dblclick', () => {
+        const selectedNode = editor.selection.getNode();
         if (elemIsCodeBlock(selectedNode)) {
             showPopupForCodeBlock(editor, selectedNode);
         }
     });
 
     editor.on('PreInit', () => {
-        editor.parser.addNodeFilter('pre', function(elms) {
+        editor.parser.addNodeFilter('pre', elms => {
             for (const el of elms) {
-                const wrapper = tinymce.html.Node.create('code-block', {
+                const wrapper = window.tinymce.html.Node.create('code-block', {
                     contenteditable: 'false',
                 });
 
@@ -203,13 +207,13 @@ function register(editor, url) {
             }
         });
 
-        editor.parser.addNodeFilter('code-block', function(elms) {
+        editor.parser.addNodeFilter('code-block', elms => {
             for (const el of elms) {
                 el.attr('contenteditable', 'false');
             }
         });
 
-        editor.serializer.addNodeFilter('code-block', function(elms) {
+        editor.serializer.addNodeFilter('code-block', elms => {
             for (const el of elms) {
                 el.unwrap();
             }
@@ -217,12 +221,12 @@ function register(editor, url) {
     });
 
     editor.ui.registry.addContextToolbar('codeeditor', {
-        predicate: function (node) {
+        predicate(node) {
             return node.nodeName.toLowerCase() === 'code-block';
         },
         items: 'editcodeeditor',
         position: 'node',
-        scope: 'node'
+        scope: 'node',
     });
 
     editor.on('PreInit', () => {
@@ -231,9 +235,8 @@ function register(editor, url) {
 }
 
 /**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index 9f4a065adf867561f52e7ddc0e7f79202b13c275..7b1750786eafeb25bd51e9fa161ab1fe2f19c794 100644 (file)
@@ -1,4 +1,4 @@
-import DrawIO from "../services/drawio";
+import * as DrawIO from '../services/drawio';
 
 let pageEditor = null;
 let currentNode = null;
@@ -16,12 +16,12 @@ function showDrawingManager(mceEditor, selectedNode = null) {
     pageEditor = mceEditor;
     currentNode = selectedNode;
 
-    /** @type {ImageManager} **/
+    /** @type {ImageManager} * */
     const imageManager = window.$components.first('image-manager');
-    imageManager.show(function (image) {
+    imageManager.show(image => {
         if (selectedNode) {
             const imgElem = selectedNode.querySelector('img');
-            pageEditor.undoManager.transact(function () {
+            pageEditor.undoManager.transact(() => {
                 pageEditor.dom.setAttrib(imgElem, 'src', image.url);
                 pageEditor.dom.setAttrib(selectedNode, 'drawio-diagram', image.id);
             });
@@ -32,32 +32,26 @@ function showDrawingManager(mceEditor, selectedNode = null) {
     }, 'drawio');
 }
 
-function showDrawingEditor(mceEditor, selectedNode = null) {
-    pageEditor = mceEditor;
-    currentNode = selectedNode;
-    DrawIO.show(options.drawioUrl, drawingInit, updateContent);
-}
-
 async function updateContent(pngData) {
-    const id = "image-" + Math.random().toString(16).slice(2);
+    const id = `image-${Math.random().toString(16).slice(2)}`;
     const loadingImage = window.baseUrl('/loading.gif');
 
-    const handleUploadError = (error) => {
+    const handleUploadError = error => {
         if (error.status === 413) {
             window.$events.emit('error', options.translations.serverUploadLimitText);
         } else {
             window.$events.emit('error', options.translations.imageUploadErrorText);
         }
-        console.log(error);
+        console.error(error);
     };
 
     // Handle updating an existing image
     if (currentNode) {
         DrawIO.close();
-        let imgElem = currentNode.querySelector('img');
+        const imgElem = currentNode.querySelector('img');
         try {
             const img = await DrawIO.upload(pngData, options.pageId);
-            pageEditor.undoManager.transact(function () {
+            pageEditor.undoManager.transact(() => {
                 pageEditor.dom.setAttrib(imgElem, 'src', img.url);
                 pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
             });
@@ -72,7 +66,7 @@ async function updateContent(pngData) {
         DrawIO.close();
         try {
             const img = await DrawIO.upload(pngData, options.pageId);
-            pageEditor.undoManager.transact(function () {
+            pageEditor.undoManager.transact(() => {
                 pageEditor.dom.setAttrib(id, 'src', img.url);
                 pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
             });
@@ -83,7 +77,6 @@ async function updateContent(pngData) {
     }, 5);
 }
 
-
 function drawingInit() {
     if (!currentNode) {
         return Promise.resolve('');
@@ -93,6 +86,66 @@ function drawingInit() {
     return DrawIO.load(drawingId);
 }
 
+function showDrawingEditor(mceEditor, selectedNode = null) {
+    pageEditor = mceEditor;
+    currentNode = selectedNode;
+    DrawIO.show(options.drawioUrl, drawingInit, updateContent);
+}
+
+/**
+ * @param {Editor} editor
+ */
+function register(editor) {
+    editor.addCommand('drawio', () => {
+        const selectedNode = editor.selection.getNode();
+        showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
+    });
+
+    editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`);
+
+    editor.ui.registry.addSplitButton('drawio', {
+        tooltip: 'Insert/edit drawing',
+        icon: 'diagram',
+        onAction() {
+            editor.execCommand('drawio');
+            // Hack to de-focus the tinymce editor toolbar
+            window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
+        },
+        fetch(callback) {
+            callback([
+                {
+                    type: 'choiceitem',
+                    text: 'Drawing manager',
+                    value: 'drawing-manager',
+                },
+            ]);
+        },
+        onItemAction(api, value) {
+            if (value === 'drawing-manager') {
+                const selectedNode = editor.selection.getNode();
+                showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
+            }
+        },
+    });
+
+    editor.on('dblclick', () => {
+        const selectedNode = editor.selection.getNode();
+        if (!isDrawing(selectedNode)) return;
+        showDrawingEditor(editor, selectedNode);
+    });
+
+    editor.on('SetContent', () => {
+        const drawings = editor.dom.select('body > div[drawio-diagram]');
+        if (!drawings.length) return;
+
+        editor.undoManager.transact(() => {
+            for (const drawing of drawings) {
+                drawing.setAttribute('contenteditable', 'false');
+            }
+        });
+    });
+}
+
 /**
  *
  * @param {WysiwygConfigOptions} providedOptions
@@ -100,56 +153,5 @@ function drawingInit() {
  */
 export function getPlugin(providedOptions) {
     options = providedOptions;
-    return function(editor, url) {
-
-        editor.addCommand('drawio', () => {
-            const selectedNode = editor.selection.getNode();
-            showDrawingEditor(editor, isDrawing(selectedNode) ? selectedNode : null);
-        });
-
-        editor.ui.registry.addIcon('diagram', `<svg width="24" height="24" fill="${options.darkMode ? '#BBB' : '#000000'}" xmlns="http://www.w3.org/2000/svg"><path d="M20.716 7.639V2.845h-4.794v1.598h-7.99V2.845H3.138v4.794h1.598v7.99H3.138v4.794h4.794v-1.598h7.99v1.598h4.794v-4.794h-1.598v-7.99zM4.736 4.443h1.598V6.04H4.736zm1.598 14.382H4.736v-1.598h1.598zm9.588-1.598h-7.99v-1.598H6.334v-7.99h1.598V6.04h7.99v1.598h1.598v7.99h-1.598zm3.196 1.598H17.52v-1.598h1.598zM17.52 6.04V4.443h1.598V6.04zm-4.21 7.19h-2.79l-.582 1.599H8.643l2.717-7.191h1.119l2.724 7.19h-1.302zm-2.43-1.006h2.086l-1.039-3.06z"/></svg>`)
-
-        editor.ui.registry.addSplitButton('drawio', {
-            tooltip: 'Insert/edit drawing',
-            icon: 'diagram',
-            onAction() {
-                editor.execCommand('drawio');
-                // Hack to de-focus the tinymce editor toolbar
-                window.document.body.dispatchEvent(new Event('mousedown', {bubbles: true}));
-            },
-            fetch(callback) {
-                callback([
-                    {
-                        type: 'choiceitem',
-                        text: 'Drawing manager',
-                        value: 'drawing-manager',
-                    }
-                ]);
-            },
-            onItemAction(api, value) {
-                if (value === 'drawing-manager') {
-                    const selectedNode = editor.selection.getNode();
-                    showDrawingManager(editor, isDrawing(selectedNode) ? selectedNode : null);
-                }
-            }
-        });
-
-        editor.on('dblclick', event => {
-            let selectedNode = editor.selection.getNode();
-            if (!isDrawing(selectedNode)) return;
-            showDrawingEditor(editor, selectedNode);
-        });
-
-        editor.on('SetContent', function () {
-            const drawings = editor.dom.select('body > div[drawio-diagram]');
-            if (!drawings.length) return;
-
-            editor.undoManager.transact(function () {
-                for (const drawing of drawings) {
-                    drawing.setAttribute('contenteditable', 'false');
-                }
-            });
-        });
-
-    };
-}
\ No newline at end of file
+    return register;
+}
index 1585de72d2023a0a6b6c6059e25443962e5cba1e..096b4f96805a98ba8676ed7c7f95789b096bc014 100644 (file)
@@ -1,9 +1,7 @@
 /**
  * @param {Editor} editor
- * @param {String} url
  */
-function register(editor, url) {
-
+function register(editor) {
     const aboutDialog = {
         title: 'About the WYSIWYG Editor',
         url: window.baseUrl('/help/wysiwyg'),
@@ -13,17 +11,14 @@ function register(editor, url) {
         icon: 'help',
         tooltip: 'About the editor',
         onAction() {
-            tinymce.activeEditor.windowManager.openUrl(aboutDialog);
-        }
+            window.tinymce.activeEditor.windowManager.openUrl(aboutDialog);
+        },
     });
-
 }
 
-
 /**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index df1984d4ee998ae4e61f4f6b95721fee1f5919fb..f5da947f21c151a16cba5922b9586956dd63eb67 100644 (file)
@@ -1,12 +1,11 @@
 /**
  * @param {Editor} editor
- * @param {String} url
  */
-function register(editor, url) {
-    editor.addCommand('InsertHorizontalRule', function () {
-        let hrElem = document.createElement('hr');
-        let cNode = editor.selection.getNode();
-        let parentNode = cNode.parentNode;
+function register(editor) {
+    editor.addCommand('InsertHorizontalRule', () => {
+        const hrElem = document.createElement('hr');
+        const cNode = editor.selection.getNode();
+        const {parentNode} = cNode;
         parentNode.insertBefore(hrElem, cNode);
     });
 
@@ -15,15 +14,13 @@ function register(editor, url) {
         tooltip: 'Insert horizontal line',
         onAction() {
             editor.execCommand('InsertHorizontalRule');
-        }
+        },
     });
 }
 
-
 /**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index 44a0a35ab1228a4ffbe0ee49a263e8595684764a..c4a6d927d2b53646144d0a7650ed12e978505bf0 100644 (file)
@@ -1,102 +1,4 @@
-/**
- * @param {Editor} editor
- * @param {String} url
- */
-import {blockElementTypes} from "./util";
-
-function register(editor, url) {
-
-    editor.ui.registry.addIcon('details', '<svg width="24" height="24"><path d="M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z"/></svg>');
-    editor.ui.registry.addIcon('togglefold', '<svg height="24"  width="24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>');
-    editor.ui.registry.addIcon('togglelabel', '<svg height="18" width="18" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>');
-
-    editor.ui.registry.addButton('details', {
-        icon: 'details',
-        tooltip: 'Insert collapsible block',
-        onAction() {
-            editor.execCommand('InsertDetailsBlock');
-        }
-    });
-
-    editor.ui.registry.addButton('removedetails', {
-        icon: 'table-delete-table',
-        tooltip: 'Unwrap',
-        onAction() {
-            unwrapDetailsInSelection(editor)
-        }
-    });
-
-    editor.ui.registry.addButton('editdetials', {
-        icon: 'togglelabel',
-        tooltip: 'Edit label',
-        onAction() {
-            showDetailLabelEditWindow(editor);
-        }
-    });
-
-    editor.on('dblclick', event => {
-        if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;
-        showDetailLabelEditWindow(editor);
-    });
-
-    editor.ui.registry.addButton('toggledetails', {
-        icon: 'togglefold',
-        tooltip: 'Toggle open/closed',
-        onAction() {
-            const details = getSelectedDetailsBlock(editor);
-            details.toggleAttribute('open');
-            editor.focus();
-        }
-    });
-
-    editor.addCommand('InsertDetailsBlock', function () {
-        let content = editor.selection.getContent({format: 'html'});
-        const details = document.createElement('details');
-        const summary = document.createElement('summary');
-        const id = 'details-' + Date.now();
-        details.setAttribute('data-id', id)
-        details.appendChild(summary);
-
-        if (!content) {
-            content = '<p><br></p>';
-        }
-
-        details.innerHTML += content;
-        editor.insertContent(details.outerHTML);
-        editor.focus();
-
-        const domDetails = editor.dom.select(`[data-id="${id}"]`)[0] || null;
-        if (domDetails) {
-            const firstChild = domDetails.querySelector('doc-root > *');
-            if (firstChild) {
-                firstChild.focus();
-            }
-            domDetails.removeAttribute('data-id');
-        }
-    });
-
-    editor.ui.registry.addContextToolbar('details', {
-        predicate: function (node) {
-            return node.nodeName.toLowerCase() === 'details';
-        },
-        items: 'editdetials toggledetails removedetails',
-        position: 'node',
-        scope: 'node'
-    });
-
-    editor.on('PreInit', () => {
-        setupElementFilters(editor);
-    });
-}
-
-/**
- * @param {Editor} editor
- */
-function showDetailLabelEditWindow(editor) {
-    const details = getSelectedDetailsBlock(editor);
-    const dialog = editor.windowManager.open(detailsDialog(editor));
-    dialog.setData({summary: getSummaryTextFromDetails(details)});
-}
+import {blockElementTypes} from './util';
 
 /**
  * @param {Editor} editor
@@ -105,15 +7,18 @@ function getSelectedDetailsBlock(editor) {
     return editor.selection.getNode().closest('details');
 }
 
-/**
- * @param {Element} element
- */
-function getSummaryTextFromDetails(element) {
-    const summary = element.querySelector('summary');
-    if (!summary) {
-        return '';
-    }
-    return summary.textContent;
+function setSummary(editor, summaryContent) {
+    const details = getSelectedDetailsBlock(editor);
+    if (!details) return;
+
+    editor.undoManager.transact(() => {
+        let summary = details.querySelector('summary');
+        if (!summary) {
+            summary = document.createElement('summary');
+            details.prepend(summary);
+        }
+        summary.textContent = summaryContent;
+    });
 }
 
 /**
@@ -135,34 +40,40 @@ function detailsDialog(editor) {
         buttons: [
             {
                 type: 'cancel',
-                text: 'Cancel'
+                text: 'Cancel',
             },
             {
                 type: 'submit',
                 text: 'Save',
                 primary: true,
-            }
+            },
         ],
         onSubmit(api) {
             const {summary} = api.getData();
             setSummary(editor, summary);
             api.close();
-        }
+        },
+    };
+}
+
+/**
+ * @param {Element} element
+ */
+function getSummaryTextFromDetails(element) {
+    const summary = element.querySelector('summary');
+    if (!summary) {
+        return '';
     }
+    return summary.textContent;
 }
 
-function setSummary(editor, summaryContent) {
+/**
+ * @param {Editor} editor
+ */
+function showDetailLabelEditWindow(editor) {
     const details = getSelectedDetailsBlock(editor);
-    if (!details) return;
-
-    editor.undoManager.transact(() => {
-        let summary = details.querySelector('summary');
-        if (!summary) {
-            summary = document.createElement('summary');
-            details.prepend(summary);
-        }
-        summary.textContent = summaryContent;
-    });
+    const dialog = editor.windowManager.open(detailsDialog(editor));
+    dialog.setData({summary: getSummaryTextFromDetails(details)});
 }
 
 /**
@@ -188,27 +99,21 @@ function unwrapDetailsInSelection(editor) {
 }
 
 /**
- * @param {Editor} editor
+ * @param {tinymce.html.Node} detailsEl
  */
-function setupElementFilters(editor) {
-    editor.parser.addNodeFilter('details', function(elms) {
-        for (const el of elms) {
-            ensureDetailsWrappedInEditable(el);
-        }
-    });
-
-    editor.serializer.addNodeFilter('details', function(elms) {
-        for (const el of elms) {
-            unwrapDetailsEditable(el);
-            el.attr('open', null);
+function unwrapDetailsEditable(detailsEl) {
+    detailsEl.attr('contenteditable', null);
+    let madeUnwrap = false;
+    for (const child of detailsEl.children()) {
+        if (child.name === 'doc-root') {
+            child.unwrap();
+            madeUnwrap = true;
         }
-    });
+    }
 
-    editor.serializer.addNodeFilter('doc-root', function(elms) {
-        for (const el of elms) {
-            el.unwrap();
-        }
-    });
+    if (madeUnwrap) {
+        unwrapDetailsEditable(detailsEl);
+    }
 }
 
 /**
@@ -218,7 +123,7 @@ function ensureDetailsWrappedInEditable(detailsEl) {
     unwrapDetailsEditable(detailsEl);
 
     detailsEl.attr('contenteditable', 'false');
-    const rootWrap = tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
+    const rootWrap = window.tinymce.html.Node.create('doc-root', {contenteditable: 'true'});
     let previousBlockWrap = null;
 
     for (const child of detailsEl.children()) {
@@ -227,7 +132,7 @@ function ensureDetailsWrappedInEditable(detailsEl) {
 
         if (!isBlock) {
             if (!previousBlockWrap) {
-                previousBlockWrap = tinymce.html.Node.create('p');
+                previousBlockWrap = window.tinymce.html.Node.create('p');
                 rootWrap.append(previousBlockWrap);
             }
             previousBlockWrap.append(child);
@@ -241,28 +146,119 @@ function ensureDetailsWrappedInEditable(detailsEl) {
 }
 
 /**
- * @param {tinymce.html.Node} detailsEl
+ * @param {Editor} editor
  */
-function unwrapDetailsEditable(detailsEl) {
-    detailsEl.attr('contenteditable', null);
-    let madeUnwrap = false;
-    for (const child of detailsEl.children()) {
-        if (child.name === 'doc-root') {
-            child.unwrap();
-            madeUnwrap = true;
+function setupElementFilters(editor) {
+    editor.parser.addNodeFilter('details', elms => {
+        for (const el of elms) {
+            ensureDetailsWrappedInEditable(el);
         }
-    }
+    });
 
-    if (madeUnwrap) {
-        unwrapDetailsEditable(detailsEl);
-    }
+    editor.serializer.addNodeFilter('details', elms => {
+        for (const el of elms) {
+            unwrapDetailsEditable(el);
+            el.attr('open', null);
+        }
+    });
+
+    editor.serializer.addNodeFilter('doc-root', elms => {
+        for (const el of elms) {
+            el.unwrap();
+        }
+    });
 }
 
+/**
+ * @param {Editor} editor
+ */
+function register(editor) {
+    editor.ui.registry.addIcon('details', '<svg width="24" height="24"><path d="M8.2 9a.5.5 0 0 0-.4.8l4 5.6a.5.5 0 0 0 .8 0l4-5.6a.5.5 0 0 0-.4-.8ZM20.122 18.151h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7zM20.122 3.042h-16c-.964 0-.934 2.7 0 2.7h16c1.139 0 1.173-2.7 0-2.7z"/></svg>');
+    editor.ui.registry.addIcon('togglefold', '<svg height="24"  width="24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>');
+    editor.ui.registry.addIcon('togglelabel', '<svg height="18" width="18" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>');
+
+    editor.ui.registry.addButton('details', {
+        icon: 'details',
+        tooltip: 'Insert collapsible block',
+        onAction() {
+            editor.execCommand('InsertDetailsBlock');
+        },
+    });
+
+    editor.ui.registry.addButton('removedetails', {
+        icon: 'table-delete-table',
+        tooltip: 'Unwrap',
+        onAction() {
+            unwrapDetailsInSelection(editor);
+        },
+    });
+
+    editor.ui.registry.addButton('editdetials', {
+        icon: 'togglelabel',
+        tooltip: 'Edit label',
+        onAction() {
+            showDetailLabelEditWindow(editor);
+        },
+    });
+
+    editor.on('dblclick', event => {
+        if (!getSelectedDetailsBlock(editor) || event.target.closest('doc-root')) return;
+        showDetailLabelEditWindow(editor);
+    });
+
+    editor.ui.registry.addButton('toggledetails', {
+        icon: 'togglefold',
+        tooltip: 'Toggle open/closed',
+        onAction() {
+            const details = getSelectedDetailsBlock(editor);
+            details.toggleAttribute('open');
+            editor.focus();
+        },
+    });
+
+    editor.addCommand('InsertDetailsBlock', () => {
+        let content = editor.selection.getContent({format: 'html'});
+        const details = document.createElement('details');
+        const summary = document.createElement('summary');
+        const id = `details-${Date.now()}`;
+        details.setAttribute('data-id', id);
+        details.appendChild(summary);
+
+        if (!content) {
+            content = '<p><br></p>';
+        }
+
+        details.innerHTML += content;
+        editor.insertContent(details.outerHTML);
+        editor.focus();
+
+        const domDetails = editor.dom.select(`[data-id="${id}"]`)[0] || null;
+        if (domDetails) {
+            const firstChild = domDetails.querySelector('doc-root > *');
+            if (firstChild) {
+                firstChild.focus();
+            }
+            domDetails.removeAttribute('data-id');
+        }
+    });
+
+    editor.ui.registry.addContextToolbar('details', {
+        predicate(node) {
+            return node.nodeName.toLowerCase() === 'details';
+        },
+        items: 'editdetials toggledetails removedetails',
+        position: 'node',
+        scope: 'node',
+    });
+
+    editor.on('PreInit', () => {
+        setupElementFilters(editor);
+    });
+}
 
 /**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index 6969a50e22264bb77e96c626dc93d04329edeb3e..37b5bfafd653b7f45fc523fd7b2775cbdd3e0c7c 100644 (file)
@@ -1,32 +1,29 @@
 /**
  * @param {Editor} editor
- * @param {String} url
  */
-function register(editor, url) {
+function register(editor) {
     // Custom Image picker button
     editor.ui.registry.addButton('imagemanager-insert', {
         title: 'Insert image',
         icon: 'image',
         tooltip: 'Insert image',
         onAction() {
-            /** @type {ImageManager} **/
+            /** @type {ImageManager} * */
             const imageManager = window.$components.first('image-manager');
-            imageManager.show(function (image) {
+            imageManager.show(image => {
                 const imageUrl = image.thumbs.display || image.url;
                 let html = `<a href="${image.url}" target="_blank">`;
                 html += `<img src="${imageUrl}" alt="${image.name}">`;
                 html += '</a>';
                 editor.execCommand('mceInsertContent', false, html);
             }, 'gallery');
-        }
+        },
     });
 }
 
-
 /**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index d220ac02da71f241be949049e02e50a1374e09e9..38725a1809e91107cd6f6f4ea2c40e32fb449102 100644 (file)
@@ -6,11 +6,10 @@ function register(editor, url) {
 
 }
 
-
 /**
  * @param {WysiwygConfigOptions} options
  * @return {register}
  */
 export function getPlugin(options) {
     return register;
-}
\ No newline at end of file
+}
index 4afbfa8e623600466cb52538ee577b4686a8745d..191f8364927817c5129858719608c7b31669cc33 100644 (file)
@@ -1,9 +1,82 @@
 /**
+ * @param {Element} element
+ * @return {boolean}
+ */
+function elementWithinTaskList(element) {
+    const listEl = element.closest('li');
+    return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
+}
+
+/**
+ * @param {MouseEvent} event
+ * @param {Element} clickedEl
  * @param {Editor} editor
- * @param {String} url
  */
-function register(editor, url) {
+function handleTaskListItemClick(event, clickedEl, editor) {
+    const bounds = clickedEl.getBoundingClientRect();
+    const withinBounds = event.clientX <= bounds.right
+        && event.clientX >= bounds.left
+        && event.clientY >= bounds.top
+        && event.clientY <= bounds.bottom;
+
+    // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
+    if (!withinBounds) {
+        editor.undoManager.transact(() => {
+            if (clickedEl.hasAttribute('checked')) {
+                clickedEl.removeAttribute('checked');
+            } else {
+                clickedEl.setAttribute('checked', 'checked');
+            }
+        });
+    }
+}
+
+/**
+ * @param {AstNode} node
+ */
+function parseTaskListNode(node) {
+    // Force task list item class
+    node.attr('class', 'task-list-item');
 
+    // Copy checkbox status and remove checkbox within editor
+    for (const child of node.children()) {
+        if (child.name === 'input') {
+            if (child.attr('checked') === 'checked') {
+                node.attr('checked', 'checked');
+            }
+            child.remove();
+        }
+    }
+}
+
+/**
+ * @param {AstNode} node
+ */
+function serializeTaskListNode(node) {
+    // Get checked status and clean it from list node
+    const isChecked = node.attr('checked') === 'checked';
+    node.attr('checked', null);
+
+    const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
+    if (isChecked) {
+        inputAttrs.checked = 'checked';
+    }
+
+    // Create & insert checkbox input element
+    const checkbox = window.tinymce.html.Node.create('input', inputAttrs);
+    checkbox.shortEnded = true;
+
+    if (node.firstChild) {
+        node.insert(checkbox, node.firstChild, true);
+    } else {
+        node.append(checkbox);
+    }
+}
+
+/**
+ * @param {Editor} editor
+ */
+function register(editor) {
     // Tasklist UI buttons
     editor.ui.registry.addIcon('tasklist', '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22,8c0-0.55-0.45-1-1-1h-7c-0.55,0-1,0.45-1,1s0.45,1,1,1h7C21.55,9,22,8.55,22,8z M13,16c0,0.55,0.45,1,1,1h7 c0.55,0,1-0.45,1-1c0-0.55-0.45-1-1-1h-7C13.45,15,13,15.45,13,16z M10.47,4.63c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25 c-0.39,0.39-1.02,0.39-1.42,0L2.7,8.16c-0.39-0.39-0.39-1.02,0-1.41c0.39-0.39,1.02-0.39,1.41,0l1.42,1.42l3.54-3.54 C9.45,4.25,10.09,4.25,10.47,4.63z M10.48,12.64c0.39,0.39,0.39,1.02,0,1.41l-4.23,4.25c-0.39,0.39-1.02,0.39-1.42,0L2.7,16.16 c-0.39-0.39-0.39-1.02,0-1.41s1.02-0.39,1.41,0l1.42,1.42l3.54-3.54C9.45,12.25,10.09,12.25,10.48,12.64L10.48,12.64z"/></svg>');
     editor.ui.registry.addToggleButton('tasklist', {
@@ -28,13 +101,13 @@ function register(editor, url) {
                 const inList = parentListEl && parentListEl.classList.contains('task-list-item');
                 api.setActive(Boolean(inList));
             });
-        }
+        },
     });
 
     // Tweak existing bullet list button active state to not be active
     // when we're in a task list.
     const existingBullListButton = editor.ui.registry.getAll().buttons.bullist;
-    existingBullListButton.onSetup = function(api) {
+    existingBullListButton.onSetup = function customBullListOnSetup(api) {
         editor.on('NodeChange', event => {
             const parentList = event.parents.find(el => el.nodeName === 'LI');
             const inTaskList = parentList && parentList.classList.contains('task-list-item');
@@ -42,38 +115,38 @@ function register(editor, url) {
             api.setActive(Boolean(inUlList && !inTaskList));
         });
     };
-    existingBullListButton.onAction = function() {
+    existingBullListButton.onAction = function customBullListOnAction() {
         // Cheeky hack to prevent list toggle action treating tasklists as normal
         // unordered lists which would unwrap the list on toggle from tasklist to bullet list.
         // Instead we quickly jump through an ordered list first if we're within a tasklist.
         if (elementWithinTaskList(editor.selection.getNode())) {
             editor.execCommand('InsertOrderedList', null, {
-                'list-item-attributes': {class: null}
+                'list-item-attributes': {class: null},
             });
         }
 
         editor.execCommand('InsertUnorderedList', null, {
-            'list-item-attributes': {class: null}
+            'list-item-attributes': {class: null},
         });
     };
     // Tweak existing number list to not allow classes on child items
     const existingNumListButton = editor.ui.registry.getAll().buttons.numlist;
-    existingNumListButton.onAction = function() {
+    existingNumListButton.onAction = function customNumListButtonOnAction() {
         editor.execCommand('InsertOrderedList', null, {
-            'list-item-attributes': {class: null}
+            'list-item-attributes': {class: null},
         });
     };
 
     // Setup filters on pre-init
     editor.on('PreInit', () => {
-        editor.parser.addNodeFilter('li', function(nodes) {
+        editor.parser.addNodeFilter('li', nodes => {
             for (const node of nodes) {
                 if (node.attributes.map.class === 'task-list-item') {
                     parseTaskListNode(node);
                 }
             }
         });
-        editor.serializer.addNodeFilter('li', function(nodes) {
+        editor.serializer.addNodeFilter('li', nodes => {
             for (const node of nodes) {
                 if (node.attributes.map.class === 'task-list-item') {
                     serializeTaskListNode(node);
@@ -83,7 +156,7 @@ function register(editor, url) {
     });
 
     // Handle checkbox click in editor
-    editor.on('click', function(event) {
+    editor.on('click', event => {
         const clickedEl = event.target;
         if (clickedEl.nodeName === 'LI' && clickedEl.classList.contains('task-list-item')) {
             handleTaskListItemClick(event, clickedEl, editor);
@@ -93,79 +166,8 @@ function register(editor, url) {
 }
 
 /**
- * @param {Element} element
- * @return {boolean}
- */
-function elementWithinTaskList(element) {
-    const listEl = element.closest('li');
-    return listEl && listEl.parentNode.nodeName === 'UL' && listEl.classList.contains('task-list-item');
-}
-
-/**
- * @param {MouseEvent} event
- * @param {Element} clickedEl
- * @param {Editor} editor
- */
-function handleTaskListItemClick(event, clickedEl, editor) {
-    const bounds = clickedEl.getBoundingClientRect();
-    const withinBounds = event.clientX <= bounds.right
-                        && event.clientX >= bounds.left
-                        && event.clientY >= bounds.top
-                        && event.clientY <= bounds.bottom;
-
-    // Outside of the task list item bounds mean we're probably clicking the pseudo-element.
-    if (!withinBounds) {
-        editor.undoManager.transact(() => {
-            if (clickedEl.hasAttribute('checked')) {
-                clickedEl.removeAttribute('checked');
-            }  else {
-                clickedEl.setAttribute('checked', 'checked');
-            }
-        });
-    }
-}
-
-/**
- * @param {AstNode} node
- */
-function parseTaskListNode(node) {
-    // Force task list item class
-    node.attr('class', 'task-list-item');
-
-    // Copy checkbox status and remove checkbox within editor
-    for (const child of node.children()) {
-        if (child.name === 'input') {
-            if (child.attr('checked') === 'checked') {
-                node.attr('checked', 'checked');
-            }
-            child.remove();
-        }
-    }
-}
-
-/**
- * @param {AstNode} node
- */
-function serializeTaskListNode(node) {
-    // Get checked status and clean it from list node
-    const isChecked = node.attr('checked') === 'checked';
-    node.attr('checked', null);
-
-    const inputAttrs = {type: 'checkbox', disabled: 'disabled'};
-    if (isChecked) {
-        inputAttrs.checked = 'checked';
-    }
-
-    // Create & insert checkbox input element
-    const checkbox = tinymce.html.Node.create('input', inputAttrs);
-    checkbox.shortEnded = true;
-    node.firstChild ? node.insert(checkbox, node.firstChild, true) : node.append(checkbox);
-}
-
-/**
- * @param {WysiwygConfigOptions} options
  * @return {register}
  */
-export function getPlugin(options) {
+export function getPlugin() {
     return register;
-}
\ No newline at end of file
+}
index f14ef4c643abe778c24e96ca0974aa8e4265bfb0..92f8f158323ca5b1c3a0ea5b828fb937dae1964b 100644 (file)
@@ -1,16 +1,3 @@
-/**
- * Scroll to a section dictated by the current URL query string, if present.
- * Used when directly editing a specific section of the page.
- * @param {Editor} editor
- */
-export function scrollToQueryString(editor) {
-    const queryParams = (new URL(window.location)).searchParams;
-    const scrollId = queryParams.get('content-id');
-    if (scrollId) {
-        scrollToText(editor, scrollId);
-    }
-}
-
 /**
  * @param {Editor} editor
  * @param {String} scrollId
@@ -26,4 +13,17 @@ function scrollToText(editor, scrollId) {
     editor.selection.select(element, true);
     editor.selection.collapse(false);
     editor.focus();
-}
\ No newline at end of file
+}
+
+/**
+ * Scroll to a section dictated by the current URL query string, if present.
+ * Used when directly editing a specific section of the page.
+ * @param {Editor} editor
+ */
+export function scrollToQueryString(editor) {
+    const queryParams = (new URL(window.location)).searchParams;
+    const scrollId = queryParams.get('content-id');
+    if (scrollId) {
+        scrollToText(editor, scrollId);
+    }
+}
index ef364ddadab16b1e95105e7218c4b742cb91a6e4..1c20df9c5516c8df6a640151b0b5be4c1b99fa11 100644 (file)
@@ -4,7 +4,7 @@
 export function register(editor) {
     // Headers
     for (let i = 1; i < 5; i++) {
-        editor.shortcuts.add('meta+' + i, '', ['FormatBlock', false, 'h' + (i+1)]);
+        editor.shortcuts.add(`meta+${i}`, '', ['FormatBlock', false, `h${i + 1}`]);
     }
 
     // Other block shortcuts
@@ -30,24 +30,25 @@ export function register(editor) {
     });
 
     // Loop through callout styles
-    editor.shortcuts.add('meta+9', '', function() {
+    editor.shortcuts.add('meta+9', '', () => {
         const selectedNode = editor.selection.getNode();
         const callout = selectedNode ? selectedNode.closest('.callout') : null;
 
         const formats = ['info', 'success', 'warning', 'danger'];
-        const currentFormatIndex = formats.findIndex(format => callout && callout.classList.contains(format));
+        const currentFormatIndex = formats.findIndex(format => {
+            return callout && callout.classList.contains(format);
+        });
         const newFormatIndex = (currentFormatIndex + 1) % formats.length;
         const newFormat = formats[newFormatIndex];
 
-        editor.formatter.apply('callout' + newFormat);
+        editor.formatter.apply(`callout${newFormat}`);
     });
 
     // Link selector shortcut
-    editor.shortcuts.add('meta+shift+K', '', function() {
-        /** @var {EntitySelectorPopup} **/
+    editor.shortcuts.add('meta+shift+K', '', () => {
+        /** @var {EntitySelectorPopup} * */
         const selectorPopup = window.$components.first('entity-selector-popup');
-        selectorPopup.show(function(entity) {
-
+        selectorPopup.show(entity => {
             if (editor.selection.isCollapsed()) {
                 editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));
             } else {
@@ -56,6 +57,6 @@ export function register(editor) {
 
             editor.selection.collapse(false);
             editor.focus();
-        })
+        });
     });
-}
\ No newline at end of file
+}
index 9debb08b5c4a5d9049ee37638764f5dd8dc1f90a..4663ad132e13f05457c4d6db1697145f7024222d 100644 (file)
@@ -13,7 +13,7 @@ export function getPrimaryToolbar(options) {
         'bullist numlist listoverflow',
         textDirPlugins,
         'link table imagemanager-insert insertoverflow',
-        'code about fullscreen'
+        'code about fullscreen',
     ];
 
     return toolbar.filter(row => Boolean(row)).join(' | ');
@@ -26,17 +26,17 @@ function registerPrimaryToolbarGroups(editor) {
     editor.ui.registry.addGroupToolbarButton('formatoverflow', {
         icon: 'more-drawer',
         tooltip: 'More',
-        items: 'strikethrough superscript subscript inlinecode removeformat'
+        items: 'strikethrough superscript subscript inlinecode removeformat',
     });
     editor.ui.registry.addGroupToolbarButton('listoverflow', {
         icon: 'more-drawer',
         tooltip: 'More',
-        items: 'tasklist outdent indent'
+        items: 'tasklist outdent indent',
     });
     editor.ui.registry.addGroupToolbarButton('insertoverflow', {
         icon: 'more-drawer',
         tooltip: 'More',
-        items: 'customhr codeeditor drawio media details'
+        items: 'customhr codeeditor drawio media details',
     });
 }
 
@@ -50,7 +50,7 @@ function registerLinkContextToolbar(editor) {
         },
         position: 'node',
         scope: 'node',
-        items: 'link unlink openlink'
+        items: 'link unlink openlink',
     });
 }
 
@@ -64,16 +64,15 @@ function registerImageContextToolbar(editor) {
         },
         position: 'node',
         scope: 'node',
-        items: 'image'
+        items: 'image',
     });
 }
 
 /**
  * @param {Editor} editor
- * @param {WysiwygConfigOptions} options
  */
-export function registerAdditionalToolbars(editor, options) {
+export function registerAdditionalToolbars(editor) {
     registerPrimaryToolbarGroups(editor);
     registerLinkContextToolbar(editor);
     registerImageContextToolbar(editor);
-}
\ No newline at end of file
+}
index 1f63b6529cbf4524cdfa9c3342bb31a763b0af44..68b6aabfce54bc4c632629ac876db60fa2d1df82 100644 (file)
@@ -1,5 +1,3 @@
-
-
 export const blockElementTypes = [
     'p',
     'h1',
@@ -15,5 +13,5 @@ export const blockElementTypes = [
     'details',
     'ul',
     'ol',
-    'table'
-];
\ No newline at end of file
+    'table',
+];
index 330923d4fd18bd12f055b9ab2d39e5931e8214f9..0fd347cf893a3bc37f1f5379e849aa6140f552a9 100644 (file)
-/* BASICS */
-
-.CodeMirror {
-  /* Set height, width, borders, and global font properties here */
-  font-family: monospace;
-  height: 300px;
-  color: black;
-  direction: ltr;
-}
-
-/* PADDING */
-
-.CodeMirror-lines {
-  padding: 4px 0; /* Vertical padding around content */
-}
-.CodeMirror pre.CodeMirror-line,
-.CodeMirror pre.CodeMirror-line-like {
-  padding: 0 4px; /* Horizontal padding of content */
-}
-
-.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
-  background-color: white; /* The little square between H and V scrollbars */
-}
-
-/* GUTTER */
-
-.CodeMirror-gutters {
-  border-right: 1px solid #ddd;
-  background-color: #f7f7f7;
-  white-space: nowrap;
-}
-.CodeMirror-linenumbers {}
-.CodeMirror-linenumber {
-  padding: 0 3px 0 5px;
-  min-width: 20px;
-  text-align: right;
-  color: #999;
-  white-space: nowrap;
-}
-
-.CodeMirror-guttermarker { color: black; }
-.CodeMirror-guttermarker-subtle { color: #999; }
-
-/* CURSOR */
-
-.CodeMirror-cursor {
-  border-left: 1px solid black;
-  border-right: none;
-  width: 0;
-}
-/* Shown when moving in bi-directional text */
-.CodeMirror div.CodeMirror-secondarycursor {
-  border-left: 1px solid silver;
-}
-.cm-fat-cursor .CodeMirror-cursor {
-  width: auto;
-  border: 0 !important;
-  background: #7e7;
-}
-.cm-fat-cursor div.CodeMirror-cursors {
-  z-index: 1;
-}
-.cm-fat-cursor-mark {
-  background-color: rgba(20, 255, 20, 0.5);
-  -webkit-animation: blink 1.06s steps(1) infinite;
-  -moz-animation: blink 1.06s steps(1) infinite;
-  animation: blink 1.06s steps(1) infinite;
-}
-.cm-animate-fat-cursor {
-  width: auto;
-  border: 0;
-  -webkit-animation: blink 1.06s steps(1) infinite;
-  -moz-animation: blink 1.06s steps(1) infinite;
-  animation: blink 1.06s steps(1) infinite;
-  background-color: #7e7;
-}
-@-moz-keyframes blink {
-  0% {}
-  50% { background-color: transparent; }
-  100% {}
-}
-@-webkit-keyframes blink {
-  0% {}
-  50% { background-color: transparent; }
-  100% {}
-}
-@keyframes blink {
-  0% {}
-  50% { background-color: transparent; }
-  100% {}
-}
-
-/* Can style cursor different in overwrite (non-insert) mode */
-.CodeMirror-overwrite .CodeMirror-cursor {}
-
-.cm-tab { display: inline-block; text-decoration: inherit; }
-
-.CodeMirror-rulers {
-  position: absolute;
-  left: 0; right: 0; top: -50px; bottom: 0;
-  overflow: hidden;
-}
-.CodeMirror-ruler {
-  border-left: 1px solid #ccc;
-  top: 0; bottom: 0;
-  position: absolute;
-}
-
-/* DEFAULT THEME */
-
-.cm-s-default .cm-header {color: blue;}
-.cm-s-default .cm-quote {color: #090;}
-.cm-negative {color: #d44;}
-.cm-positive {color: #292;}
-.cm-header, .cm-strong {font-weight: bold;}
-.cm-em {font-style: italic;}
-.cm-link {text-decoration: underline;}
-.cm-strikethrough {text-decoration: line-through;}
-
-.cm-s-default .cm-keyword {color: #708;}
-.cm-s-default .cm-atom {color: #219;}
-.cm-s-default .cm-number {color: #164;}
-.cm-s-default .cm-def {color: #00f;}
-.cm-s-default .cm-variable,
-.cm-s-default .cm-punctuation,
-.cm-s-default .cm-property,
-.cm-s-default .cm-operator {}
-.cm-s-default .cm-variable-2 {color: #05a;}
-.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
-.cm-s-default .cm-comment {color: #a50;}
-.cm-s-default .cm-string {color: #a11;}
-.cm-s-default .cm-string-2 {color: #f50;}
-.cm-s-default .cm-meta {color: #555;}
-.cm-s-default .cm-qualifier {color: #555;}
-.cm-s-default .cm-builtin {color: #30a;}
-.cm-s-default .cm-bracket {color: #997;}
-.cm-s-default .cm-tag {color: #170;}
-.cm-s-default .cm-attribute {color: #00c;}
-.cm-s-default .cm-hr {color: #999;}
-.cm-s-default .cm-link {color: #00c;}
-
-.cm-s-default .cm-error {color: #f00;}
-.cm-invalidchar {color: #f00;}
-
-.CodeMirror-composing { border-bottom: 2px solid; }
-
-/* Default styles for common addons */
-
-div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
-div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
-.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
-.CodeMirror-activeline-background {background: #e8f2ff;}
-
-/* STOP */
-
-/* The rest of this file contains styles related to the mechanics of
-   the editor. You probably shouldn't touch them. */
-
-.CodeMirror {
-  position: relative;
-  overflow: hidden;
-  background: white;
-}
-
-.CodeMirror-scroll {
-  overflow: scroll !important; /* Things will break if this is overridden */
-  /* 50px is the magic margin used to hide the element's real scrollbars */
-  /* See overflow: hidden in .CodeMirror */
-  margin-bottom: -50px; margin-right: -50px;
-  padding-bottom: 50px;
-  height: 100%;
-  outline: none; /* Prevent dragging from highlighting the element */
-  position: relative;
-}
-.CodeMirror-sizer {
-  position: relative;
-  border-right: 50px solid transparent;
-}
-
-/* The fake, visible scrollbars. Used to force redraw during scrolling
-   before actual scrolling happens, thus preventing shaking and
-   flickering artifacts. */
-.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
-  position: absolute;
-  z-index: 6;
-  display: none;
-  outline: none;
-}
-.CodeMirror-vscrollbar {
-  right: 0; top: 0;
-  overflow-x: hidden;
-  overflow-y: scroll;
-}
-.CodeMirror-hscrollbar {
-  bottom: 0; left: 0;
-  overflow-y: hidden;
-  overflow-x: scroll;
-}
-.CodeMirror-scrollbar-filler {
-  right: 0; bottom: 0;
-}
-.CodeMirror-gutter-filler {
-  left: 0; bottom: 0;
-}
-
-.CodeMirror-gutters {
-  position: absolute; left: 0; top: 0;
-  min-height: 100%;
-  z-index: 3;
-}
-.CodeMirror-gutter {
-  white-space: normal;
-  height: 100%;
-  display: inline-block;
-  vertical-align: top;
-  margin-bottom: -50px;
-}
-.CodeMirror-gutter-wrapper {
-  position: absolute;
-  z-index: 4;
-  background: none !important;
-  border: none !important;
-}
-.CodeMirror-gutter-background {
-  position: absolute;
-  top: 0; bottom: 0;
-  z-index: 4;
-}
-.CodeMirror-gutter-elt {
-  position: absolute;
-  cursor: default;
-  z-index: 4;
-}
-.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
-.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
-
-.CodeMirror-lines {
-  cursor: text;
-  min-height: 1px; /* prevents collapsing before first draw */
-}
-.CodeMirror pre.CodeMirror-line,
-.CodeMirror pre.CodeMirror-line-like {
-  /* Reset some styles that the rest of the page might have set */
-  -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
-  border-width: 0;
-  background: transparent;
-  font-family: inherit;
-  font-size: inherit;
-  margin: 0;
-  white-space: pre;
-  word-wrap: normal;
-  line-height: inherit;
-  color: inherit;
-  z-index: 2;
-  position: relative;
-  overflow: visible;
-  -webkit-tap-highlight-color: transparent;
-  -webkit-font-variant-ligatures: contextual;
-  font-variant-ligatures: contextual;
-}
-.CodeMirror-wrap pre.CodeMirror-line,
-.CodeMirror-wrap pre.CodeMirror-line-like {
-  word-wrap: break-word;
-  white-space: pre-wrap;
-  word-break: normal;
-}
-
-.CodeMirror-linebackground {
-  position: absolute;
-  left: 0; right: 0; top: 0; bottom: 0;
-  z-index: 0;
-}
-
-.CodeMirror-linewidget {
-  position: relative;
-  z-index: 2;
-  padding: 0.1px; /* Force widget margins to stay inside of the container */
-}
-
-.CodeMirror-widget {}
-
-.CodeMirror-rtl pre { direction: rtl; }
-
-.CodeMirror-code {
-  outline: none;
-}
-
-/* Force content-box sizing for the elements where we expect it */
-.CodeMirror-scroll,
-.CodeMirror-sizer,
-.CodeMirror-gutter,
-.CodeMirror-gutters,
-.CodeMirror-linenumber {
-  -moz-box-sizing: content-box;
-  box-sizing: content-box;
-}
-
-.CodeMirror-measure {
-  position: absolute;
-  width: 100%;
-  height: 0;
-  overflow: hidden;
-  visibility: hidden;
-}
-
-.CodeMirror-cursor {
-  position: absolute;
-  pointer-events: none;
-}
-.CodeMirror-measure pre { position: static; }
-
-div.CodeMirror-cursors {
-  visibility: hidden;
-  position: relative;
-  z-index: 3;
-}
-div.CodeMirror-dragcursors {
-  visibility: visible;
-}
-
-.CodeMirror-focused div.CodeMirror-cursors {
-  visibility: visible;
-}
-
-.CodeMirror-selected { background: #d9d9d9; }
-.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
-.CodeMirror-crosshair { cursor: crosshair; }
-.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
-.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
-
-.cm-searching {
-  background-color: #ffa;
-  background-color: rgba(255, 255, 0, .4);
-}
-
-/* Used to force a border model for a node */
-.cm-force-border { padding-right: .1px; }
-
-@media print {
-  /* Hide the cursor when printing */
-  .CodeMirror div.CodeMirror-cursors {
-    visibility: hidden;
-  }
-}
-
-/* See issue #2901 */
-.cm-tab-wrap-hack:after { content: ''; }
-
-/* Help users use markselection to safely style text background */
-span.CodeMirror-selectedtext { background: none; }
-
-/* STOP */
-
-/**
- * Codemirror Darcula theme
- */
-
 /**
-    Name: IntelliJ IDEA darcula theme
-    From IntelliJ IDEA by JetBrains
+ * Custom CodeMirror BookStack overrides
  */
 
-.cm-s-darcula  { font-family: Consolas, Menlo, Monaco, 'Lucida Console', 'Liberation Mono', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Courier New', monospace, serif;}
-.cm-s-darcula.CodeMirror { background: #2B2B2B; color: #A9B7C6; }
-
-.cm-s-darcula span.cm-meta { color: #BBB529; }
-.cm-s-darcula span.cm-number { color: #6897BB; }
-.cm-s-darcula span.cm-keyword { color: #CC7832; line-height: 1em; font-weight: bold; }
-.cm-s-darcula span.cm-def { color: #A9B7C6; font-style: italic; }
-.cm-s-darcula span.cm-variable { color: #A9B7C6; }
-.cm-s-darcula span.cm-variable-2 { color: #A9B7C6; }
-.cm-s-darcula span.cm-variable-3 { color: #9876AA; }
-.cm-s-darcula span.cm-type { color: #AABBCC; font-weight: bold; }
-.cm-s-darcula span.cm-property { color: #FFC66D; }
-.cm-s-darcula span.cm-operator { color: #A9B7C6; }
-.cm-s-darcula span.cm-string { color: #6A8759; }
-.cm-s-darcula span.cm-string-2 { color: #6A8759; }
-.cm-s-darcula span.cm-comment { color: #61A151; font-style: italic; }
-.cm-s-darcula span.cm-link { color: #CC7832; }
-.cm-s-darcula span.cm-atom { color: #CC7832; }
-.cm-s-darcula span.cm-error { color: #BC3F3C; }
-.cm-s-darcula span.cm-tag { color: #629755; font-weight: bold; font-style: italic; text-decoration: underline; }
-.cm-s-darcula span.cm-attribute { color: #6897bb; }
-.cm-s-darcula span.cm-qualifier { color: #6A8759; }
-.cm-s-darcula span.cm-bracket { color: #A9B7C6; }
-.cm-s-darcula span.cm-builtin { color: #FF9E59; }
-.cm-s-darcula span.cm-special { color: #FF9E59; }
-.cm-s-darcula span.cm-matchhighlight { color: #FFFFFF; background-color: rgba(50, 89, 48, .7); font-weight: normal;}
-.cm-s-darcula span.cm-searching { color: #FFFFFF; background-color: rgba(61, 115, 59, .7); font-weight: normal;}
-
-.cm-s-darcula .CodeMirror-cursor { border-left: 1px solid #A9B7C6; }
-.cm-s-darcula .CodeMirror-activeline-background { background: #323232; }
-.cm-s-darcula .CodeMirror-gutters { background: #313335; border-right: 1px solid #313335; }
-.cm-s-darcula .CodeMirror-guttermarker { color: #FFEE80; }
-.cm-s-darcula .CodeMirror-guttermarker-subtle { color: #D0D0D0; }
-.cm-s-darcula .CodeMirrir-linenumber { color: #606366; }
-.cm-s-darcula .CodeMirror-matchingbracket { background-color: #3B514D; color: #FFEF28 !important; font-weight: bold; }
-
-.cm-s-darcula div.CodeMirror-selected { background: #214283; }
-
-.CodeMirror-hints.darcula {
-  font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
-  color: #9C9E9E;
-  background-color: #3B3E3F !important;
-}
-
-.CodeMirror-hints.darcula .CodeMirror-hint-active {
-  background-color: #494D4E !important;
-  color: #9C9E9E !important;
-}
-
-/**
- * Custom BookStack overrides
- */
-.CodeMirror, .CodeMirror pre {
+.cm-editor {
   font-size: 12px;
-}
-.CodeMirror {
-  font-size: 12px;
-  height: auto;
+  border: 1px solid #ddd;
+  line-height: 1.4;
   margin-bottom: $-l;
-  border: 1px solid;
-  @include lightDark(border-color, #DDD, #111);
-}
-.CodeMirror pre::after {
-  display: none;
-}
-html.dark-mode .CodeMirror pre {
-  background-color: transparent;
 }
 
-.cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 0; color: #333; }
+.page-content .cm-editor,
+.CodeMirrorContainer .cm-editor {
+  border-radius: 4px;
+}
 
-.code-fill .CodeMirror {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  margin-bottom: 0;
-  border: 0;
+// Manual dark-mode definition so that it applies to code blocks within the shadow
+// dom which are used within the WYSIWYG editor, as the .dark-mode on the parent
+// <html> node are not applies so instead we have the class on the parent element.
+.dark-mode .cm-editor {
+  border-color: #444;
 }
 
 /**
  * Custom Copy Button
  */
-.CodeMirror-copy {
+.cm-copy-button {
   position: absolute;
+  display: flex;
+  align-items: center;
+  justify-content: center;
   top: -1px;
   right: -1px;
   background-color: #EEE;
   border: 1px solid #DDD;
+  border-radius: 0 4px 0 0;
   @include lightDark(background-color, #eee, #333);
   @include lightDark(border-color, #ddd, #444);
-  @include lightDark(fill, #444, #888);
-  padding: $-xs;
+  @include lightDark(color, #444, #888);
   line-height: 0;
   cursor: pointer;
   z-index: 5;
   user-select: none;
   opacity: 0;
   pointer-events: none;
+  width: 32px;
+  height: 32px;
+  transition: background-color linear 60ms, color linear 60ms;
   svg {
-    transition: all ease-in 240ms;
-    transform: translateY(0);
+    fill: currentColor;
   }
   &.success {
-    background-color: lighten($positive, 10%);
-    svg {
-      fill: #FFF;
-      transform: translateY(-3px);
-    }
+    background: $positive;
+    color: #FFF;
+  }
+  &:focus {
+    outline: 0 !important;
   }
 }
-.CodeMirror:hover .CodeMirror-copy {
+.cm-editor:hover .cm-copy-button  {
   user-select: all;
-  opacity: 1;
+  opacity: .6;
   pointer-events: all;
 }
\ No newline at end of file
index 825501364664abbcc7bddd09e666acd1c2ca0525..5ba1286c0c35600cc97c7e82a48e7307b0829357 100644 (file)
   z-index: 999;
   display: flex;
   flex-direction: column;
+  position: relative;
   &.small {
     margin: 2% auto;
     width: 800px;
   box-shadow: none;
   color: #FFF;
   padding: $-xs $-m;
+  cursor: pointer;
 }
 
 .popup-header button:not(.popup-header-close) {
@@ -202,10 +204,147 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   min-height: 70vh;
 }
 
-.dropzone-container {
-  position: relative;
-  @include lightDark(background-color, #eee, #222);
+.dropzone-overlay {
+  position: absolute;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 1.333rem;
+  width: 98%;
+  height: 98%;
+  left: 1%;
+  top: 1%;
+  border-radius: 4px;
+  border: 1px dashed var(--color-primary);
+  font-style: italic;
+  box-sizing: content-box;
+  background-clip: padding-box;
   background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");
+  background-color: var(--color-primary);
+  color: #FFF;
+  opacity: .8;
+  z-index: 9;
+  pointer-events: none;
+  animation: dzAnimIn 240ms ease-in-out;
+}
+
+.dropzone-landing-area {
+  background-color: var(--color-primary-light);
+  padding: $-m $-l;
+  width: 100%;
+  border: 1px dashed var(--color-primary);
+  color: var(--color-primary);
+  border-radius: 4px;
+}
+
+@keyframes dzAnimIn {
+  0% {
+    opacity: 0;
+    transform: scale(.7);
+  }
+  60% {
+    transform: scale(1.1);
+  }
+  100% {
+    transform: scale(1);
+    opacity: .8;
+  }
+}
+
+@keyframes dzFileItemIn {
+  0% {
+    opacity: .5;
+    transform: translateY(28px);
+  }
+  100% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+@keyframes dzFileItemOut {
+  0% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+  100% {
+    opacity: .5;
+    transform: translateY(28px);
+  }
+}
+
+.dropzone-file-item {
+  width: 260px;
+  height: 80px;
+  position: relative;
+  display: flex;
+  margin: 1rem;
+  flex-direction: row;
+  @include lightDark(background, #FFF, #444);
+  box-shadow: $bs-large;
+  border-radius: 4px;
+  overflow: hidden;
+  padding-bottom: 3px;
+  animation: dzFileItemIn ease-in-out 240ms;
+  transition: transform ease-in-out 120ms, box-shadow ease-in-out 120ms;
+  cursor: pointer;
+  &:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 3px 8px 1px rgba(22, 22, 22, 0.2);
+  }
+}
+.dropzone-file-item.dismiss {
+  animation: dzFileItemOut ease-in-out 240ms;
+}
+.dropzone-file-item .loading-container {
+  text-align: start !important;
+  margin: 0;
+}
+.dropzone-file-item-image-wrap {
+  width: 80px;
+  position: relative;
+  background-color: var(--color-primary-light);
+  img {
+    object-fit: cover;
+    width: 100%;
+    height: 100%;
+    opacity: .8;
+  }
+}
+.dropzone-file-item-text-wrap {
+  flex: 1;
+  display: block;
+  padding: 1rem;
+  overflow: auto;
+}
+.dropzone-file-item-progress {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  font-size: 0;
+  height: 3px;
+  background-color: var(--color-primary);
+  transition: width ease-in-out 240ms;
+}
+.dropzone-file-item-label,
+.dropzone-file-item-status {
+  align-items: center;
+  font-size: .8rem;
+  font-weight: 700;
+}
+.dropzone-file-item-status[data-status] {
+  display: flex;
+  font-size: .6rem;
+  font-weight: 500;
+  line-height: 1.2;
+}
+.dropzone-file-item-status[data-status="success"] {
+  color: $positive;
+}
+.dropzone-file-item-status[data-status="error"] {
+  color: $negative;
+}
+.dropzone-file-item-status[data-status] + .dropzone-file-item-label {
+  display: none;
 }
 
 .image-manager-list .image {
@@ -256,13 +395,11 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 .image-manager .load-more {
   display: block;
   text-align: center;
-  @include lightDark(background-color, #EEE, #444);
   padding: $-s $-m;
-  color: #AAA;
   clear: both;
-  font-size: 20px;
-  cursor: pointer;
-  font-style: italic;
+  .loading-container {
+    margin: 0;
+  }
 }
 
 .image-manager .loading-container {
@@ -279,7 +416,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
     min-height: auto;
     padding: $-m;
   }
-  img {
+  .image-manager-viewer img {
     max-width: 100%;
     max-height: 180px;
     display: block;
@@ -295,10 +432,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
       display: inline-block;
     }
   }
-  .dropzone-container {
-    border-bottom: 1px solid #DDD;
-    @include lightDark(border-color, #ddd, #000);
-  }
 }
 
 .image-manager-list {
@@ -318,295 +451,18 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   }
 }
 
-// Dropzone
-/*
- * The MIT License
- * Copyright (c) 2012 Matias Meno <[email protected]>
- */
-.dz-message {
-  font-size: 1em;
-  line-height: 2.85;
-  font-style: italic;
-  color: #888;
-  text-align: center;
-  cursor: pointer;
-  padding: $-l $-m;
-  transition: all ease-in-out 120ms;
-}
-
-.dz-drag-hover .dz-message {
-  background-color: rgb(16, 126, 210);
-  color: #EEE;
-}
-
-@keyframes passing-through {
-  0% {
-    opacity: 0;
-    transform: translateY(40px);
-  }
-  30%, 70% {
-    opacity: 1;
-    transform: translateY(0px);
-  }
-  100% {
-    opacity: 0;
-    transform: translateY(-40px);
-  }
-}
-
-@keyframes slide-in {
-  0% {
-    opacity: 0;
-    transform: translateY(40px);
-  }
-  30% {
-    opacity: 1;
-    transform: translateY(0px);
-  }
-}
-
-@keyframes pulse {
-  0% {
-    transform: scale(1);
-  }
-  10% {
-    transform: scale(1.1);
-  }
-  20% {
-    transform: scale(1);
-  }
-}
-
-.dropzone, .dropzone * {
-  box-sizing: border-box;
-}
-
-.dz-preview {
-  position: relative;
-  display: inline-block;
-  vertical-align: top;
-  margin: 12px;
-  min-height: 80px;
-}
-
-.dz-preview:hover {
-  z-index: 1000;
-}
-
-.dz-preview:hover .dz-details {
-  opacity: 1;
-}
-
-.dz-preview.dz-file-preview .dz-image {
-  border-radius: 4px;
-  background: #e9e9e9;
-}
-
-.dz-preview.dz-file-preview .dz-details {
-  opacity: 1;
-}
-
-.dz-preview.dz-image-preview {
-  background: white;
-}
-
-.dz-preview.dz-image-preview .dz-details {
-  transition: opacity 0.2s linear;
-}
-
-.dz-preview .dz-remove {
-  font-size: 13px;
-  text-align: center;
-  display: block;
-  cursor: pointer;
-  border: none;
-  margin-top: 3px;
-}
-
-.dz-preview .dz-remove:hover {
-  text-decoration: underline;
-}
-
-.dz-preview:hover .dz-details {
-  opacity: 1;
-}
-
-.dz-preview .dz-details {
-  z-index: 20;
-  position: absolute;
-  top: 0;
-  left: 0;
-  opacity: 0;
-  font-size: 10px;
-  min-width: 100%;
-  max-width: 100%;
-  padding: 6px 3px;
-  text-align: center;
-  color: rgba(0, 0, 0, 0.9);
-  line-height: 150%;
-}
-
-.dz-preview .dz-details .dz-size {
-  margin-bottom: 0.5em;
-  font-size: 12px;
-}
-
-.dz-preview .dz-details .dz-filename {
-  white-space: nowrap;
-}
-
-.dz-preview .dz-details .dz-filename:hover span {
-  border: 1px solid rgba(200, 200, 200, 0.8);
-  background-color: rgba(255, 255, 255, 0.8);
-}
-
-.dz-preview .dz-details .dz-filename:not(:hover) {
-  overflow: hidden;
-  text-overflow: ellipsis;
-}
-
-.dz-preview .dz-details .dz-filename:not(:hover) span {
-  border: 1px solid transparent;
-}
-
-.dz-preview .dz-details .dz-filename span {
-  background-color: rgba(255, 255, 255, 0.4);
-  padding: 0 0.4em;
-  border-radius: 3px;
-}
-
-.dz-preview:hover .dz-image img {
-  filter: blur(8px);
-}
-
-.dz-preview .dz-image {
-  border-radius: 4px;
-  overflow: hidden;
-  width: 80px;
-  height: 80px;
-  position: relative;
-  display: block;
-  z-index: 10;
-}
-
-.dz-preview .dz-image img {
-  display: block;
-}
-
-.dz-preview.dz-success .dz-success-mark {
-  animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-}
-
-.dz-preview.dz-error .dz-error-mark {
-  opacity: 1;
-  animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-}
-
-.dz-preview .dz-success-mark, .dz-preview .dz-error-mark {
-  pointer-events: none;
-  opacity: 0;
-  z-index: 1001;
-  position: absolute;
-  display: block;
-  top: 50%;
-  left: 50%;
-  margin-inline-start: -27px;
-  margin-top: -35px;
-}
-
-.dz-preview .dz-success-mark svg, .dz-preview .dz-error-mark svg {
-  display: block;
-  width: 54px;
-  height: 54px;
-}
-
-.dz-preview.dz-processing .dz-progress {
-  opacity: 1;
-  transition: all 0.2s linear;
-}
-
-.dz-preview.dz-complete .dz-progress {
-  opacity: 0;
-  transition: opacity 0.4s ease-in;
-}
-
-.dz-preview:not(.dz-processing) .dz-progress {
-  animation: pulse 6s ease infinite;
-}
-
-.dz-preview .dz-progress {
-  opacity: 1;
-  z-index: 1000;
-  pointer-events: none;
-  position: absolute;
-  height: 16px;
-  left: 50%;
-  top: 50%;
-  margin-top: -8px;
-  width: 80px;
-  margin-inline-start: -40px;
-  background: rgba(255, 255, 255, 0.9);
-  transform: scale(1);
-  border-radius: 8px;
-  overflow: hidden;
-}
-
-.dz-preview .dz-progress .dz-upload {
-  background: #333;
-  background: linear-gradient(to bottom, #666, #444);
-  position: absolute;
-  top: 0;
-  left: 0;
-  bottom: 0;
-  width: 0;
-  transition: width 300ms ease-in-out;
-}
-
-.dz-preview.dz-error .dz-error-message {
-  display: block;
-}
-
-.dz-preview.dz-error {
-  .dz-image, .dz-details {
-    &:hover ~ .dz-error-message {
-      opacity: 1;
-      pointer-events: auto;
-    }
+.image-manager [role="tablist"] button[role="tab"] {
+  border-right: 1px solid #DDD;
+  @include lightDark(border-color, #DDD, #000);
+  &:last-child {
+    border-right: none;
   }
 }
 
-.dz-preview .dz-error-message {
-  pointer-events: none;
-  z-index: 1000;
-  position: absolute;
-  display: block;
-  display: none;
-  opacity: 0;
-  transition: opacity 0.3s ease;
-  border-radius: 4px;
-  font-size: 12px;
-  line-height: 1.2;
-  top: 88px;
-  left: -12px;
-  width: 160px;
-  background: $negative;
-  padding: $-xs;
-  color: white;
+.image-manager-header {
+  z-index: 4;
 }
 
-.dz-preview .dz-error-message:after {
-  content: '';
-  position: absolute;
-  top: -6px;
-  left: 44px;
-  width: 0;
-  height: 0;
-  border-inline-start: 6px solid transparent;
-  border-inline-end: 6px solid transparent;
-  border-bottom: 6px solid $negative;
-}
-
-
 .tab-container [role="tablist"] {
   display: flex;
   align-items: end;
@@ -730,7 +586,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
 .code-editor-main {
   flex: 1;
   min-width: 0;
-  .CodeMirror {
+  .cm-editor {
     margin-bottom: 0;
     z-index: 1;
     max-width: 100%;
index b7fc52f7dbdc38793505469653c61293359bdbf8..37f8f1bfcf720d28568eb522033a845fc27629ab 100644 (file)
@@ -75,6 +75,7 @@
   @include lightDark(border-color, #ddd, #000);
   position: relative;
   flex: 1;
+  min-width: 0;
 }
 .markdown-editor-wrap + .markdown-editor-wrap {
   flex-basis: 50%;
   flex-grow: 0;
 }
 
+.markdown-editor-wrap .cm-editor {
+  flex: 1;
+  max-width: 100%;
+  border: 0;
+  margin: 0;
+}
+
 .markdown-panel-divider {
   width: 2px;
   @include lightDark(background-color, #ddd, #000);
index 19333faf71baf7c4a6d69e7cfda11a3e4e7902cd..541978a65e2ac14aba3d34ee1624c3a6afcca849 100644 (file)
@@ -253,6 +253,15 @@ body.flexbox {
   position: relative;
 }
 
+.fixed {
+  position: fixed;
+  z-index: 20;
+  &.top-right {
+    top: 0;
+    right: 0;
+  }
+}
+
 .hidden {
   display: none !important;
 }
index 57718e647167417cf89689fbcabde43af9b42c5d..a88d58f99dcbbc6c0c6252e22cb6ab2a9e33aebc 100755 (executable)
@@ -178,6 +178,14 @@ body.tox-fullscreen, body.markdown-fullscreen {
       white-space: pre-wrap;
     }
   }
+
+  .cm-editor {
+    margin-bottom: 1.375em;
+  }
+
+  video {
+    max-width: 100%;
+  }
 }
 
 // Page content pointers
@@ -448,4 +456,4 @@ body.tox-fullscreen, body.markdown-fullscreen {
   @media (prefers-contrast: more) {
     opacity: 1;
   }
-}
\ No newline at end of file
+}
index edf8ce6144c9d7e2171adf25faf5a8668c985e56..6745d2a546e52da6b6300494b54862e58c11eacc 100644 (file)
@@ -178,18 +178,19 @@ sub, .subscript {
 pre {
   font-size: 12px;
   border: 1px solid #DDD;
-  @include lightDark(background-color, #f5f5f5, #2B2B2B);
+  @include lightDark(background-color, #FFF, #2B2B2B);
   @include lightDark(border-color, #DDD, #111);
-  padding-left: 31px;
+  border-radius: 4px;
+  padding-left: 26px;
   position: relative;
   padding-top: 3px;
   padding-bottom: 3px;
-  &:after {
+  &:before {
     content: '';
     display: block;
     position: absolute;
     top: 0;
-    width: 29px;
+    width: 22.4px;
     left: 0;
     height: 100%;
     @include lightDark(background-color, #f5f5f5, #313335);
index 0f4ec70417a2a4bce81df7aad3771b8629925e83..2ed6806468767df04c72a928e70913549a7fffdc 100644 (file)
@@ -120,29 +120,36 @@ $loadingSize: 10px;
 .contained-search-box {
   display: flex;
   height: 38px;
+  z-index: -1;
   input, button {
+    height: 100%;
     border-radius: 0;
     border: 1px solid #ddd;
     @include lightDark(border-color, #ddd, #000);
     margin-inline-start: -1px;
+    &:last-child {
+      border-inline-end: 0;
+    }
   }
   input {
     flex: 5;
     padding: $-xs $-s;
     &:focus, &:active {
-      outline: 0;
+      outline: 1px dotted var(--color-primary);
+      outline-offset: -2px;
+      border: 1px solid #ddd;
+      @include lightDark(border-color, #ddd, #000);
     }
   }
   button {
     width: 60px;
   }
+  button.primary-background {
+    border-color: var(--color-primary);
+  }
   button i {
     padding: 0;
   }
-  button.cancel.active {
-    background-color: $negative;
-    color: #EEE;
-  }
   svg {
     margin: 0;
   }
index 7358b5cd765e4335ead15d92937d5ac3a35a0c64..75b71c6beb7d355e15ccf32440945fac07a4acbb 100644 (file)
         HTTP POST calls upon events occurring in BookStack.
     </li>
     <li>
-        <a href="https://github.com/BookStackApp/BookStack/blob/master/dev/docs/visual-theme-system.md" target="_blank" rel="noopener noreferrer">Visual Theme System</a> -
+        <a href="https://github.com/BookStackApp/BookStack/blob/development/dev/docs/visual-theme-system.md" target="_blank" rel="noopener noreferrer">Visual Theme System</a> -
         Methods to override views, translations and icons within BookStack.
     </li>
     <li>
-        <a href="https://github.com/BookStackApp/BookStack/blob/master/dev/docs/logical-theme-system.md" target="_blank" rel="noopener noreferrer">Logical Theme System</a> -
+        <a href="https://github.com/BookStackApp/BookStack/blob/development/dev/docs/logical-theme-system.md" target="_blank" rel="noopener noreferrer">Logical Theme System</a> -
         Methods to extend back-end functionality within BookStack.
     </li>
 </ul>
index 15837448ac0be451ee46c1d2600ee63e41fa4e89..cfb514df0bd2b435c0d32a08e6ce7a5d119cea2f 100644 (file)
@@ -1,7 +1,7 @@
 <div component="ajax-form"
      option:ajax-form:url="/attachments/{{ $attachment->id }}"
      option:ajax-form:method="put"
-     option:ajax-form:response-container=".attachment-edit-container"
+     option:ajax-form:response-container="#edit-form-container"
      option:ajax-form:success-message="{{ trans('entities.attachments_updated_success') }}">
     <h5>{{ trans('entities.attachments_edit_file') }}</h5>
 
     </div>
 
     <div component="tabs" class="tab-container">
-        <div class="nav-tabs">
-            <button refs="tabs@toggleFile" type="button" class="tab-item {{ $attachment->external ? '' : 'selected' }}">{{ trans('entities.attachments_upload') }}</button>
-            <button refs="tabs@toggleLink" type="button" class="tab-item {{ $attachment->external ? 'selected' : '' }}">{{ trans('entities.attachments_set_link') }}</button>
+        <div class="nav-tabs" role="tablist">
+            <button id="attachment-edit-file-tab"
+                    type="button"
+                    aria-controls="attachment-edit-file-panel"
+                    aria-selected="{{ $attachment->external ? 'false' : 'true' }}"
+                    role="tab">{{ trans('entities.attachments_upload') }}</button>
+            <button id="attachment-edit-link-tab"
+                    type="button"
+                    aria-controls="attachment-edit-link-panel"
+                    aria-selected="{{ $attachment->external ? 'true' : 'false' }}"
+                    role="tab">{{ trans('entities.attachments_set_link') }}</button>
         </div>
-        <div refs="tabs@contentFile" class="mb-m {{ $attachment->external ? 'hidden' : '' }}">
-            @include('form.dropzone', [
+        <div id="attachment-edit-file-panel"
+             @if($attachment->external) hidden @endif
+             tabindex="0"
+             role="tabpanel"
+             aria-labelledby="attachment-edit-file-tab"
+             class="mb-m">
+            @include('form.simple-dropzone', [
                 'placeholder' => trans('entities.attachments_edit_drop_upload'),
                 'url' =>  url('/attachments/upload/' . $attachment->id),
                 'successMessage' => trans('entities.attachments_file_updated'),
             ])
         </div>
-        <div refs="tabs@contentLink" class="{{ $attachment->external ? '' : 'hidden' }}">
+        <div id="attachment-edit-link-panel"
+             @if(!$attachment->external) hidden @endif
+             tabindex="0"
+             role="tabpanel"
+             aria-labelledby="attachment-edit-link-tab">
             <div class="form-group">
                 <label for="attachment_edit_url">{{ trans('entities.attachments_link_url') }}</label>
                 <input type="text" id="attachment_edit_url"
@@ -43,6 +60,8 @@
     </div>
 
     <button component="event-emit-select"
-            option:event-emit-select:name="edit-back" type="button" class="button outline">{{ trans('common.back') }}</button>
+            option:event-emit-select:name="edit-back"
+            type="button"
+            class="button outline">{{ trans('common.back') }}</button>
     <button refs="ajax-form@submit" type="button" class="button">{{ trans('common.save') }}</button>
 </div>
\ No newline at end of file
index b51daa40e061a6f22e85fc0ef978b94e5e5326db..a2fba83eb48e28981528d084eb990020ed991c69 100644 (file)
@@ -4,7 +4,7 @@
 <div component="ajax-form"
      option:ajax-form:url="/attachments/link"
      option:ajax-form:method="post"
-     option:ajax-form:response-container=".link-form-container"
+     option:ajax-form:response-container="#link-form-container"
      option:ajax-form:success-message="{{ trans('entities.attachments_link_attached') }}">
     <input type="hidden" name="attachment_link_uploaded_to" value="{{ $pageId }}">
     <p class="text-muted small">{{ trans('entities.attachments_explain_link') }}</p>
@@ -22,6 +22,9 @@
             <div class="text-neg text-small">{{ $errors->first('attachment_link_url') }}</div>
         @endif
     </div>
+    <button component="event-emit-select"
+            option:event-emit-select:name="edit-back"
+            type="button" class="button outline">{{ trans('common.cancel') }}</button>
     <button refs="ajax-form@submit"
             type="button"
             class="button">{{ trans('entities.attach') }}</button>
index 7d14d00e7d29600fa9b16e020de1b756e26176d8..4526172d4db63b2d62ca7a362bfa4ea131d4b2aa 100644 (file)
@@ -6,66 +6,44 @@
      class="toolbox-tab-content">
 
     <h4>{{ trans('entities.attachments') }}</h4>
-    <div class="px-l files">
+    <div component="dropzone"
+         option:dropzone:url="{{ url('/attachments/upload?uploaded_to=' . $page->id) }}"
+         option:dropzone:success-message="{{ trans('entities.attachments_file_uploaded') }}"
+         option:dropzone:error-message="{{ trans('errors.attachment_upload_error') }}"
+         option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
+         option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
+         option:dropzone:zone-text="{{ trans('entities.attachments_dropzone') }}"
+         option:dropzone:file-accept="*"
+         class="px-l files">
 
-        <div refs="attachments@listContainer">
+        <div refs="attachments@list-container dropzone@drop-target" class="relative">
             <p class="text-muted small">{{ trans('entities.attachments_explain') }} <span
                         class="text-warn">{{ trans('entities.attachments_explain_instant_save') }}</span></p>
 
-            <div component="tabs" refs="attachments@mainTabs" class="tab-container">
-                <div role="tablist">
-                    <button id="attachment-tab-items"
-                            role="tab"
-                            aria-selected="true"
-                            aria-controls="attachment-panel-items"
-                            type="button"
-                            class="tab-item">{{ trans('entities.attachments_items') }}</button>
-                    <button id="attachment-tab-upload"
-                            role="tab"
-                            aria-selected="false"
-                            aria-controls="attachment-panel-upload"
-                            type="button"
-                            class="tab-item">{{ trans('entities.attachments_upload') }}</button>
-                    <button id="attachment-tab-links"
-                            role="tab"
-                            aria-selected="false"
-                            aria-controls="attachment-panel-links"
-                            type="button"
-                            class="tab-item">{{ trans('entities.attachments_link') }}</button>
-                </div>
-                <div id="attachment-panel-items"
-                     tabindex="0"
-                     role="tabpanel"
-                     aria-labelledby="attachment-tab-items"
-                     refs="attachments@list">
-                    @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
-                </div>
-                <div id="attachment-panel-upload"
-                     tabindex="0"
-                     role="tabpanel"
-                     hidden
-                     aria-labelledby="attachment-tab-upload">
-                    @include('form.dropzone', [
-                        'placeholder' => trans('entities.attachments_dropzone'),
-                        'url' =>  url('/attachments/upload?uploaded_to=' . $page->id),
-                        'successMessage' => trans('entities.attachments_file_uploaded'),
-                    ])
-                </div>
-                <div id="attachment-panel-links"
-                     tabindex="0"
-                     role="tabpanel"
-                     hidden
-                     aria-labelledby="attachment-tab-links"
-                     class="link-form-container">
-                    @include('attachments.manager-link-form', ['pageId' => $page->id])
-                </div>
+            <hr class="mb-s">
+
+            <div class="flex-container-row">
+                <button refs="dropzone@select-button" type="button" class="button outline small">{{ trans('entities.attachments_upload') }}</button>
+                <button refs="attachments@attach-link-button" type="button" class="button outline small">{{ trans('entities.attachments_link') }}</button>
+            </div>
+            <div>
+                <p class="text-muted text-small">{{ trans('entities.attachments_upload_drop') }}</p>
             </div>
+            <div refs="dropzone@status-area" class="fixed top-right px-m py-m"></div>
 
-        </div>
+            <hr>
 
-        <div refs="attachments@editContainer" class="hidden attachment-edit-container">
+            <div refs="attachments@list-panel">
+                @include('attachments.manager-list', ['attachments' => $page->attachments->all()])
+            </div>
 
         </div>
+    </div>
 
+    <div id="link-form-container" refs="attachments@links-container" hidden class="px-l">
+        @include('attachments.manager-link-form', ['pageId' => $page->id])
     </div>
+
+    <div id="edit-form-container" refs="attachments@edit-container" hidden class="px-l"></div>
+
 </div>
\ No newline at end of file
diff --git a/resources/views/form/dropzone.blade.php b/resources/views/form/dropzone.blade.php
deleted file mode 100644 (file)
index 118761d..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-{{--
-@url - URL to upload to.
-@placeholder - Placeholder text
-@successMessage
---}}
-<div component="dropzone"
-     option:dropzone:url="{{ $url }}"
-     option:dropzone:success-message="{{ $successMessage ?? '' }}"
-     option:dropzone:remove-message="{{ trans('components.image_upload_remove') }}"
-     option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
-     option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
-     option:dropzone:timeout-message="{{ trans('errors.file_upload_timeout') }}"
-
-     class="dropzone-container text-center">
-    <button type="button" class="dz-message">{{ $placeholder }}</button>
-</div>
\ No newline at end of file
diff --git a/resources/views/form/simple-dropzone.blade.php b/resources/views/form/simple-dropzone.blade.php
new file mode 100644 (file)
index 0000000..cfd3d90
--- /dev/null
@@ -0,0 +1,21 @@
+{{--
+@url - URL to upload to.
+@placeholder - Placeholder text
+@successMessage
+--}}
+<div component="dropzone"
+     option:dropzone:url="{{ $url }}"
+     option:dropzone:success-message="{{ $successMessage }}"
+     option:dropzone:error-message="{{ trans('errors.attachment_upload_error') }}"
+     option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
+     option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
+     option:dropzone:zone-text="{{ trans('entities.attachments_dropzone') }}"
+     option:dropzone:file-accept="*"
+     class="relative">
+    <div refs="dropzone@status-area"></div>
+    <button type="button"
+            refs="dropzone@select-button dropzone@drop-target"
+            class="dropzone-landing-area text-center">
+        {{ $placeholder }}
+    </button>
+</div>
\ No newline at end of file
index 9c8f927611a95924fef882e705b6f63fb69bf45b..8700a4ccb763ff25dc86ce20b3270bfb6b451d52 100644 (file)
@@ -23,7 +23,7 @@
                     <div refs="code-editor@language-options-container" class="lang-options">
                         @php
                             $languages = [
-                                'Bash', 'CSS', 'C', 'C++', 'C#', 'Dart', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI',
+                                'Bash', 'CSS', 'C', 'C++', 'C#', 'Clojure', 'Dart', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI',
                                 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'MATLAB', 'MSSQL', 'MySQL', 'Nginx', 'OCaml',
                                 'Octave', 'Pascal', 'Perl', 'PHP', 'PL/SQL', 'PostgreSQL', 'Powershell', 'Python', 'Ruby', 'Rust', 'Scheme', 'Shell', 'Smarty',
                                  'SQL', 'SQLite', 'Swift', 'Twig', 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML',
index 22fe7adddb69b2ebb599a24c31dab2ab7a6f9c7e..ccf79fb6d3d8ca8c38db5f551ffadc885c7c16b4 100644 (file)
@@ -19,5 +19,7 @@
 </div>
 @endforeach
 @if($hasMore)
-    <div class="load-more">{{ trans('components.image_load_more') }}</div>
+    <div class="load-more">
+        <button type="button" class="button small outline">{{ trans('components.image_load_more') }}</button>
+    </div>
 @endif
\ No newline at end of file
index 5832c0954fc9374a79fc99e81cbd4f400ac24e78..0594c67e807d9ecf903da2009bd8b804c0e1440a 100644 (file)
@@ -1,4 +1,11 @@
-<div component="image-manager"
+<div components="image-manager dropzone"
+     option:dropzone:url="{{ url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0])) }}"
+     option:dropzone:success-message="{{ trans('components.image_upload_success') }}"
+     option:dropzone:error-message="{{ trans('errors.image_upload_error') }}"
+     option:dropzone:upload-limit="{{ config('app.upload_limit') }}"
+     option:dropzone:upload-limit-message="{{ trans('errors.server_upload_limit') }}"
+     option:dropzone:zone-text="{{ trans('components.image_dropzone_drop') }}"
+     option:dropzone:file-accept="image/*"
      option:image-manager:uploaded-to="{{ $uploaded_to ?? 0 }}"
      class="image-manager">
 
 
             <div class="popup-header primary-background">
                 <div class="popup-title">{{ trans('components.image_select') }}</div>
+                <button refs="dropzone@selectButton image-manager@uploadButton" type="button">
+                    <span>@icon('upload')</span>
+                    <span>{{ trans('components.image_upload') }}</span>
+                </button>
                 <button refs="popup@hide" type="button" class="popup-header-close">@icon('close')</button>
             </div>
 
-            <div class="flex-fill image-manager-body">
+            <div refs="dropzone@drop-target" class="flex-fill image-manager-body">
 
                 <div class="image-manager-content">
-                    <div role="tablist" class="image-manager-header primary-background-light grid third no-gap">
+                    <div role="tablist" class="image-manager-header grid third no-gap">
                         <button refs="image-manager@filterTabs"
                                 data-filter="all"
                                 role="tab"
                 <div class="image-manager-sidebar flex-container-column">
 
                     <div refs="image-manager@dropzoneContainer">
-                        @include('form.dropzone', [
-                            'placeholder' => trans('components.image_dropzone'),
-                            'successMessage' => trans('components.image_upload_success'),
-                            'url' => url('/images/gallery?' . http_build_query(['uploaded_to' => $uploaded_to ?? 0]))
-                        ])
+                        <div refs="dropzone@status-area"></div>
+                    </div>
+
+                    <div refs="image-manager@form-container-placeholder" class="p-m text-small text-muted">
+                        <p>{{ trans('components.image_intro') }}</p>
+                        <p refs="image-manager@upload-hint">{{ trans('components.image_intro_upload') }}</p>
                     </div>
 
-                    <div refs="image-manager@formContainer" class="inner flex"></div>
+                    <div refs="image-manager@formContainer" class="inner flex">
+                    </div>
                 </div>
 
             </div>
index fd8a20a0484f68722ad026f6dcf1245187b5fdb4..c488f0e115c8b9eb49120735fd898583440f1866 100644 (file)
@@ -30,7 +30,7 @@
             </div>
         </div>
 
-        <div markdown-input class="flex flex-fill">
+        <div class="flex flex-fill" dir="ltr">
             <textarea id="markdown-editor-input"
                       refs="markdown-editor@input"
                       @if($errors->has('markdown')) class="text-neg" @endif
index b8fe62fa45cee2317d9d759b89e693d8e41aeda7..5bafa6e153fffea70da598c60e53b212e382c064 100644 (file)
@@ -5,8 +5,8 @@
     <div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
         <span class="icon mr-xxs">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
         <div class="input-group inline block">
-            <input readonly="readonly" type="text" id="pointer-url" placeholder="url">
-            <button class="button outline icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
+            <input refs="pointer@input" readonly="readonly" type="text" id="pointer-url" placeholder="url">
+            <button refs="pointer@button" class="button outline icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
         </div>
         @if(userCan('page-update', $page))
             <a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
index d1b64d455270e66b2c077ffe9b270b2021f1838a..c809cdb3af11f7cd5e1aee157d21e60d323c260c 100644 (file)
@@ -13,6 +13,8 @@ use BookStack\Http\Controllers\Api\BookExportApiController;
 use BookStack\Http\Controllers\Api\BookshelfApiController;
 use BookStack\Http\Controllers\Api\ChapterApiController;
 use BookStack\Http\Controllers\Api\ChapterExportApiController;
+use BookStack\Http\Controllers\Api\ContentPermissionApiController;
+use BookStack\Http\Controllers\Api\ImageGalleryApiController;
 use BookStack\Http\Controllers\Api\PageApiController;
 use BookStack\Http\Controllers\Api\PageExportApiController;
 use BookStack\Http\Controllers\Api\RecycleBinApiController;
@@ -62,6 +64,12 @@ Route::get('pages/{id}/export/pdf', [PageExportApiController::class, 'exportPdf'
 Route::get('pages/{id}/export/plaintext', [PageExportApiController::class, 'exportPlainText']);
 Route::get('pages/{id}/export/markdown', [PageExportApiController::class, 'exportMarkdown']);
 
+Route::get('image-gallery', [ImageGalleryApiController::class, 'list']);
+Route::post('image-gallery', [ImageGalleryApiController::class, 'create']);
+Route::get('image-gallery/{id}', [ImageGalleryApiController::class, 'read']);
+Route::put('image-gallery/{id}', [ImageGalleryApiController::class, 'update']);
+Route::delete('image-gallery/{id}', [ImageGalleryApiController::class, 'delete']);
+
 Route::get('search', [SearchApiController::class, 'all']);
 
 Route::get('shelves', [BookshelfApiController::class, 'list']);
@@ -85,3 +93,6 @@ Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
 Route::get('recycle-bin', [RecycleBinApiController::class, 'list']);
 Route::put('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'restore']);
 Route::delete('recycle-bin/{deletionId}', [RecycleBinApiController::class, 'destroy']);
+
+Route::get('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'read']);
+Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']);
diff --git a/storage/backups/.gitignore b/storage/backups/.gitignore
new file mode 100644 (file)
index 0000000..d6b7ef3
--- /dev/null
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tests/Api/ContentPermissionsApiTest.php b/tests/Api/ContentPermissionsApiTest.php
new file mode 100644 (file)
index 0000000..50b82e5
--- /dev/null
@@ -0,0 +1,262 @@
+<?php
+
+namespace Tests\Api;
+
+use Tests\TestCase;
+
+class ContentPermissionsApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected string $baseEndpoint = '/api/content-permissions';
+
+    public function test_user_roles_manage_permission_needed_for_all_endpoints()
+    {
+        $page = $this->entities->page();
+        $endpointMap = [
+            ['get', "/api/content-permissions/page/{$page->id}"],
+            ['put', "/api/content-permissions/page/{$page->id}"],
+        ];
+        $editor = $this->users->editor();
+
+        $this->actingAs($editor, 'api');
+        foreach ($endpointMap as [$method, $uri]) {
+            $resp = $this->json($method, $uri);
+            $resp->assertStatus(403);
+            $resp->assertJson($this->permissionErrorResponse());
+        }
+
+        $this->permissions->grantUserRolePermissions($editor, ['restrictions-manage-all']);
+
+        foreach ($endpointMap as [$method, $uri]) {
+            $resp = $this->json($method, $uri);
+            $this->assertNotEquals(403, $resp->getStatusCode());
+        }
+    }
+
+    public function test_read_endpoint_shows_expected_detail()
+    {
+        $page = $this->entities->page();
+        $owner = $this->users->newUser();
+        $role = $this->users->createRole();
+        $this->permissions->addEntityPermission($page, ['view', 'delete'], $role);
+        $this->permissions->changeEntityOwner($page, $owner);
+        $this->permissions->setFallbackPermissions($page, ['update', 'create']);
+
+        $this->actingAsApiAdmin();
+        $resp = $this->getJson($this->baseEndpoint . "/page/{$page->id}");
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => [
+                'id' => $owner->id, 'name' => $owner->name, 'slug' => $owner->slug,
+            ],
+            'role_permissions' => [
+                [
+                    'role_id' => $role->id,
+                    'view' => true,
+                    'create' => false,
+                    'update' => false,
+                    'delete' => true,
+                    'role' => [
+                        'id' => $role->id,
+                        'display_name' => $role->display_name,
+                    ]
+                ]
+            ],
+            'fallback_permissions' => [
+                'inheriting' => false,
+                'view' => false,
+                'create' => true,
+                'update' => true,
+                'delete' => false,
+            ],
+        ]);
+    }
+
+    public function test_read_endpoint_shows_expected_detail_when_items_are_empty()
+    {
+        $page = $this->entities->page();
+        $page->permissions()->delete();
+        $page->owned_by = null;
+        $page->save();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->getJson($this->baseEndpoint . "/page/{$page->id}");
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => null,
+            'role_permissions' => [],
+            'fallback_permissions' => [
+                'inheriting' => true,
+                'view' => null,
+                'create' => null,
+                'update' => null,
+                'delete' => null,
+            ],
+        ]);
+    }
+
+    public function test_update_endpoint_can_change_owner()
+    {
+        $page = $this->entities->page();
+        $newOwner = $this->users->newUser();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            'owner_id' => $newOwner->id,
+        ]);
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => ['id' => $newOwner->id, 'name' => $newOwner->name, 'slug' => $newOwner->slug],
+            'role_permissions' => [],
+            'fallback_permissions' => [
+                'inheriting' => true,
+                'view' => null,
+                'create' => null,
+                'update' => null,
+                'delete' => null,
+            ],
+        ]);
+    }
+
+    public function test_update_can_set_role_permissions()
+    {
+        $page = $this->entities->page();
+        $page->owned_by = null;
+        $page->save();
+        $newRoleA = $this->users->createRole();
+        $newRoleB = $this->users->createRole();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            'role_permissions' => [
+                ['role_id' => $newRoleA->id, 'view' => true, 'create' => false, 'update' => false, 'delete' => false],
+                ['role_id' => $newRoleB->id, 'view' => true, 'create' => false, 'update' => true, 'delete' => true],
+            ],
+        ]);
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => null,
+            'role_permissions' => [
+                [
+                    'role_id' => $newRoleA->id,
+                    'view' => true,
+                    'create' => false,
+                    'update' => false,
+                    'delete' => false,
+                    'role' => [
+                        'id' => $newRoleA->id,
+                        'display_name' => $newRoleA->display_name,
+                    ]
+                ],
+                [
+                    'role_id' => $newRoleB->id,
+                    'view' => true,
+                    'create' => false,
+                    'update' => true,
+                    'delete' => true,
+                    'role' => [
+                        'id' => $newRoleB->id,
+                        'display_name' => $newRoleB->display_name,
+                    ]
+                ]
+            ],
+            'fallback_permissions' => [
+                'inheriting' => true,
+                'view' => null,
+                'create' => null,
+                'update' => null,
+                'delete' => null,
+            ],
+        ]);
+    }
+
+    public function test_update_can_set_fallback_permissions()
+    {
+        $page = $this->entities->page();
+        $page->owned_by = null;
+        $page->save();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            'fallback_permissions' => [
+                'inheriting' => false,
+                'view' => true,
+                'create' => true,
+                'update' => true,
+                'delete' => false,
+            ],
+        ]);
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => null,
+            'role_permissions' => [],
+            'fallback_permissions' => [
+                'inheriting' => false,
+                'view' => true,
+                'create' => true,
+                'update' => true,
+                'delete' => false,
+            ],
+        ]);
+    }
+
+    public function test_update_can_clear_roles_permissions()
+    {
+        $page = $this->entities->page();
+        $this->permissions->addEntityPermission($page, ['view'], $this->users->createRole());
+        $page->owned_by = null;
+        $page->save();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            'role_permissions' => [],
+        ]);
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => null,
+            'role_permissions' => [],
+            'fallback_permissions' => [
+                'inheriting' => true,
+                'view' => null,
+                'create' => null,
+                'update' => null,
+                'delete' => null,
+            ],
+        ]);
+    }
+
+    public function test_update_can_clear_fallback_permissions()
+    {
+        $page = $this->entities->page();
+        $this->permissions->setFallbackPermissions($page, ['view', 'update']);
+        $page->owned_by = null;
+        $page->save();
+
+        $this->actingAsApiAdmin();
+        $resp = $this->putJson($this->baseEndpoint . "/page/{$page->id}", [
+            'fallback_permissions' => [
+                'inheriting' => true,
+            ],
+        ]);
+
+        $resp->assertOk();
+        $resp->assertExactJson([
+            'owner' => null,
+            'role_permissions' => [],
+            'fallback_permissions' => [
+                'inheriting' => true,
+                'view' => null,
+                'create' => null,
+                'update' => null,
+                'delete' => null,
+            ],
+        ]);
+    }
+}
diff --git a/tests/Api/ImageGalleryApiTest.php b/tests/Api/ImageGalleryApiTest.php
new file mode 100644 (file)
index 0000000..17c9051
--- /dev/null
@@ -0,0 +1,347 @@
+<?php
+
+namespace Tests\Api;
+
+use BookStack\Entities\Models\Page;
+use BookStack\Uploads\Image;
+use Tests\TestCase;
+
+class ImageGalleryApiTest extends TestCase
+{
+    use TestsApi;
+
+    protected string $baseEndpoint = '/api/image-gallery';
+
+    public function test_index_endpoint_returns_expected_image_and_count()
+    {
+        $this->actingAsApiAdmin();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
+        $resp->assertJson(['data' => [
+            [
+                'id' => $image->id,
+                'name' => $image->name,
+                'url' => $image->url,
+                'path' => $image->path,
+                'type' => 'gallery',
+                'uploaded_to' => $imagePage->id,
+                'created_by' => $this->users->admin()->id,
+                'updated_by' => $this->users->admin()->id,
+            ],
+        ]]);
+
+        $resp->assertJson(['total' => Image::query()->count()]);
+    }
+
+    public function test_index_endpoint_doesnt_show_images_for_those_uploaded_to_non_visible_pages()
+    {
+        $this->actingAsApiEditor();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);
+        $resp->assertJsonCount(1, 'data');
+        $resp->assertJson(['total' => 1]);
+
+        $this->permissions->disableEntityInheritedPermissions($imagePage);
+
+        $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);
+        $resp->assertJsonCount(0, 'data');
+        $resp->assertJson(['total' => 0]);
+    }
+
+    public function test_index_endpoint_doesnt_show_other_image_types()
+    {
+        $this->actingAsApiEditor();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $typesByCountExpectation = [
+            'cover_book' => 0,
+            'drawio' => 1,
+            'gallery' => 1,
+            'user' => 0,
+            'system' => 0,
+        ];
+
+        foreach ($typesByCountExpectation as $type => $count) {
+            $image->type = $type;
+            $image->save();
+
+            $resp = $this->getJson($this->baseEndpoint . '?filter[id]=' . $image->id);
+            $resp->assertJsonCount($count, 'data');
+            $resp->assertJson(['total' => $count]);
+        }
+    }
+
+    public function test_create_endpoint()
+    {
+        $this->actingAsApiAdmin();
+
+        $imagePage = $this->entities->page();
+        $resp = $this->call('POST', $this->baseEndpoint, [
+            'type' => 'gallery',
+            'uploaded_to' => $imagePage->id,
+            'name' => 'My awesome image!',
+        ], [], [
+            'image' => $this->files->uploadedImage('my-cool-image.png'),
+        ]);
+
+        $resp->assertStatus(200);
+
+        $image = Image::query()->where('uploaded_to', '=', $imagePage->id)->first();
+        $expectedUser = [
+            'id' => $this->users->admin()->id,
+            'name' => $this->users->admin()->name,
+            'slug' => $this->users->admin()->slug,
+        ];
+        $resp->assertJson([
+            'id' => $image->id,
+            'name' => 'My awesome image!',
+            'url' => $image->url,
+            'path' => $image->path,
+            'type' => 'gallery',
+            'uploaded_to' => $imagePage->id,
+            'created_by' => $expectedUser,
+            'updated_by' => $expectedUser,
+        ]);
+    }
+
+    public function test_create_endpoint_requires_image_create_permissions()
+    {
+        $user = $this->users->editor();
+        $this->actingAsForApi($user);
+        $this->permissions->removeUserRolePermissions($user, ['image-create-all']);
+
+        $makeRequest = function () {
+            return $this->call('POST', $this->baseEndpoint, []);
+        };
+
+        $resp = $makeRequest();
+        $resp->assertStatus(403);
+
+        $this->permissions->grantUserRolePermissions($user, ['image-create-all']);
+
+        $resp = $makeRequest();
+        $resp->assertStatus(422);
+    }
+
+    public function test_create_fails_if_uploaded_to_not_visible_or_not_exists()
+    {
+        $this->actingAsApiEditor();
+
+        $makeRequest = function (int $uploadedTo) {
+            return $this->call('POST', $this->baseEndpoint, [
+                'type' => 'gallery',
+                'uploaded_to' => $uploadedTo,
+                'name' => 'My awesome image!',
+            ], [], [
+                'image' => $this->files->uploadedImage('my-cool-image.png'),
+            ]);
+        };
+
+        $page = $this->entities->page();
+        $this->permissions->disableEntityInheritedPermissions($page);
+        $resp = $makeRequest($page->id);
+        $resp->assertStatus(404);
+
+        $resp = $makeRequest(Page::query()->max('id') + 55);
+        $resp->assertStatus(404);
+    }
+
+    public function test_create_has_restricted_types()
+    {
+        $this->actingAsApiEditor();
+
+        $typesByStatusExpectation = [
+            'cover_book' => 422,
+            'drawio' => 200,
+            'gallery' => 200,
+            'user' => 422,
+            'system' => 422,
+        ];
+
+        $makeRequest = function (string $type) {
+            return $this->call('POST', $this->baseEndpoint, [
+                'type' => $type,
+                'uploaded_to' => $this->entities->page()->id,
+                'name' => 'My awesome image!',
+            ], [], [
+                'image' => $this->files->uploadedImage('my-cool-image.png'),
+            ]);
+        };
+
+        foreach ($typesByStatusExpectation as $type => $status) {
+            $resp = $makeRequest($type);
+            $resp->assertStatus($status);
+        }
+    }
+
+    public function test_create_will_use_file_name_if_no_name_provided_in_request()
+    {
+        $this->actingAsApiEditor();
+
+        $imagePage = $this->entities->page();
+        $resp = $this->call('POST', $this->baseEndpoint, [
+            'type' => 'gallery',
+            'uploaded_to' => $imagePage->id,
+        ], [], [
+            'image' => $this->files->uploadedImage('my-cool-image.png'),
+        ]);
+        $resp->assertStatus(200);
+
+        $this->assertDatabaseHas('images', [
+            'type' => 'gallery',
+            'uploaded_to' => $imagePage->id,
+            'name' => 'my-cool-image.png',
+        ]);
+    }
+
+    public function test_read_endpoint()
+    {
+        $this->actingAsApiAdmin();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$image->id}");
+        $resp->assertStatus(200);
+
+        $expectedUser = [
+            'id' => $this->users->admin()->id,
+            'name' => $this->users->admin()->name,
+            'slug' => $this->users->admin()->slug,
+        ];
+
+        $displayUrl = $image->getThumb(1680, null, true);
+        $resp->assertJson([
+            'id' => $image->id,
+            'name' => $image->name,
+            'url' => $image->url,
+            'path' => $image->path,
+            'type' => 'gallery',
+            'uploaded_to' => $imagePage->id,
+            'created_by' => $expectedUser,
+            'updated_by' => $expectedUser,
+            'content' => [
+                'html' => "<a href=\"{$image->url}\" target=\"_blank\"><img src=\"{$displayUrl}\" alt=\"{$image->name}\"></a>",
+                'markdown' => "![{$image->name}]({$displayUrl})",
+            ],
+        ]);
+        $this->assertStringStartsWith('http://', $resp->json('thumbs.gallery'));
+        $this->assertStringStartsWith('http://', $resp->json('thumbs.display'));
+    }
+
+    public function test_read_endpoint_provides_different_content_for_drawings()
+    {
+        $this->actingAsApiAdmin();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $image->type = 'drawio';
+        $image->save();
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$image->id}");
+        $resp->assertStatus(200);
+
+        $drawing = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$image->url}\"></div>";
+        $resp->assertJson([
+            'id' => $image->id,
+            'content' => [
+                'html' => $drawing,
+                'markdown' => $drawing,
+            ],
+        ]);
+    }
+
+    public function test_read_endpoint_does_not_show_if_no_permissions_for_related_page()
+    {
+        $this->actingAsApiEditor();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $this->permissions->disableEntityInheritedPermissions($imagePage);
+
+        $resp = $this->getJson($this->baseEndpoint . "/{$image->id}");
+        $resp->assertStatus(404);
+    }
+
+    public function test_update_endpoint()
+    {
+        $this->actingAsApiAdmin();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", [
+            'name' => 'My updated image name!',
+        ]);
+
+        $resp->assertStatus(200);
+        $resp->assertJson([
+            'id' => $image->id,
+            'name' => 'My updated image name!',
+        ]);
+        $this->assertDatabaseHas('images', [
+            'id' => $image->id,
+            'name' => 'My updated image name!',
+        ]);
+    }
+
+    public function test_update_endpoint_requires_image_delete_permission()
+    {
+        $user = $this->users->editor();
+        $this->actingAsForApi($user);
+        $imagePage = $this->entities->page();
+        $this->permissions->removeUserRolePermissions($user, ['image-update-all', 'image-update-own']);
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']);
+        $resp->assertStatus(403);
+        $resp->assertJson($this->permissionErrorResponse());
+
+        $this->permissions->grantUserRolePermissions($user, ['image-update-all']);
+        $resp = $this->putJson($this->baseEndpoint . "/{$image->id}", ['name' => 'My new name']);
+        $resp->assertStatus(200);
+    }
+
+    public function test_delete_endpoint()
+    {
+        $this->actingAsApiAdmin();
+        $imagePage = $this->entities->page();
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+        $this->assertDatabaseHas('images', ['id' => $image->id]);
+
+        $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}");
+
+        $resp->assertStatus(204);
+        $this->assertDatabaseMissing('images', ['id' => $image->id]);
+    }
+
+    public function test_delete_endpoint_requires_image_delete_permission()
+    {
+        $user = $this->users->editor();
+        $this->actingAsForApi($user);
+        $imagePage = $this->entities->page();
+        $this->permissions->removeUserRolePermissions($user, ['image-delete-all', 'image-delete-own']);
+        $data = $this->files->uploadGalleryImageToPage($this, $imagePage);
+        $image = Image::findOrFail($data['response']->id);
+
+        $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}");
+        $resp->assertStatus(403);
+        $resp->assertJson($this->permissionErrorResponse());
+
+        $this->permissions->grantUserRolePermissions($user, ['image-delete-all']);
+        $resp = $this->deleteJson($this->baseEndpoint . "/{$image->id}");
+        $resp->assertStatus(204);
+    }
+}
index 501f2875458616da28bf33906ae49ac267c0465d..c566fd8de337fc2401ad796f5eeab483a5ca5e7c 100644 (file)
@@ -2,15 +2,28 @@
 
 namespace Tests\Api;
 
+use BookStack\Auth\User;
+
 trait TestsApi
 {
-    protected $apiTokenId = 'apitoken';
-    protected $apiTokenSecret = 'password';
+    protected string $apiTokenId = 'apitoken';
+    protected string $apiTokenSecret = 'password';
+
+    /**
+     * Set the given user as the current logged-in user via the API driver.
+     * This does not ensure API access. The user may still lack required role permissions.
+     */
+    protected function actingAsForApi(User $user): static
+    {
+        parent::actingAs($user, 'api');
+
+        return $this;
+    }
 
     /**
      * Set the API editor role as the current user via the API driver.
      */
-    protected function actingAsApiEditor()
+    protected function actingAsApiEditor(): static
     {
         $this->actingAs($this->users->editor(), 'api');
 
@@ -20,7 +33,7 @@ trait TestsApi
     /**
      * Set the API admin role as the current user via the API driver.
      */
-    protected function actingAsApiAdmin()
+    protected function actingAsApiAdmin(): static
     {
         $this->actingAs($this->users->admin(), 'api');
 
index 35acb7752165d1592791b3ba0b7972dbd16d2471..41727e7b71d7223a00af51f03f0735334875ab35 100644 (file)
@@ -5,6 +5,8 @@ namespace Tests\Auth;
 use BookStack\Actions\ActivityType;
 use BookStack\Auth\Role;
 use BookStack\Auth\User;
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
 use GuzzleHttp\Psr7\Request;
 use GuzzleHttp\Psr7\Response;
 use Illuminate\Testing\TestResponse;
@@ -397,7 +399,6 @@ class OidcTest extends TestCase
         config()->set([
             'oidc.external_id_claim' => 'super_awesome_id',
         ]);
-        $roleA = Role::factory()->create(['display_name' => 'Wizards']);
 
         $resp = $this->runLogin([
             'email'            => '[email protected]',
@@ -464,6 +465,60 @@ class OidcTest extends TestCase
         $this->assertTrue($user->hasRole($roleA->id));
     }
 
+    public function test_oidc_id_token_pre_validate_theme_event_without_return()
+    {
+        $args = [];
+        $callback = function (...$eventArgs) use (&$args) {
+            $args = $eventArgs;
+        };
+        Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback);
+
+        $resp = $this->runLogin([
+            'email' => '[email protected]',
+            'sub'   => 'benny1010101',
+            'name'  => 'Benny',
+        ]);
+        $resp->assertRedirect('/');
+
+        $this->assertDatabaseHas('users', [
+            'external_auth_id' => 'benny1010101',
+        ]);
+
+        $this->assertArrayHasKey('iss', $args[0]);
+        $this->assertArrayHasKey('sub', $args[0]);
+        $this->assertEquals('Benny', $args[0]['name']);
+        $this->assertEquals('benny1010101', $args[0]['sub']);
+
+        $this->assertArrayHasKey('access_token', $args[1]);
+        $this->assertArrayHasKey('expires_in', $args[1]);
+        $this->assertArrayHasKey('refresh_token', $args[1]);
+    }
+
+    public function test_oidc_id_token_pre_validate_theme_event_with_return()
+    {
+        $callback = function (...$eventArgs) {
+            return array_merge($eventArgs[0], [
+                'email' => '[email protected]',
+                'sub' => 'lenny1010101',
+                'name' => 'Lenny',
+            ]);
+        };
+        Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback);
+
+        $resp = $this->runLogin([
+            'email' => '[email protected]',
+            'sub'   => 'benny1010101',
+            'name'  => 'Benny',
+        ]);
+        $resp->assertRedirect('/');
+
+        $this->assertDatabaseHas('users', [
+            'email' => '[email protected]',
+            'external_auth_id' => 'lenny1010101',
+            'name' => 'Lenny',
+        ]);
+    }
+
     protected function withAutodiscovery()
     {
         config()->set([
index 0ee419610ca330e959cb492812e75e0e714cf219..1a3e4abbec35000112322c154642752ec103d5f8 100644 (file)
@@ -193,6 +193,9 @@ class Saml2Test extends TestCase
         $req = $this->post('/saml2/logout');
         $redirect = $req->headers->get('location');
         $this->assertStringStartsWith('http://saml.local/saml2/idp/SingleLogoutService.php', $redirect);
+        $sloData = $this->parseSamlDataFromUrl($redirect, 'SAMLRequest');
+        $this->assertStringContainsString('<samlp:SessionIndex>_4fe7c0d1572d64b27f930aa6f236a6f42e930901cc</samlp:SessionIndex>', $sloData);
+
         $this->withGet(['SAMLResponse' => $this->sloResponseData], $handleLogoutResponse);
     }
 
@@ -379,11 +382,16 @@ class Saml2Test extends TestCase
     {
         $req = $this->post('/saml2/login');
         $location = $req->headers->get('Location');
-        $query = explode('?', $location)[1];
+        return $this->parseSamlDataFromUrl($location, 'SAMLRequest');
+    }
+
+    protected function parseSamlDataFromUrl(string $url, string $paramName): string
+    {
+        $query = explode('?', $url)[1];
         $params = [];
         parse_str($query, $params);
 
-        return gzinflate(base64_decode($params['SAMLRequest']));
+        return gzinflate(base64_decode($params[$paramName]));
     }
 
     protected function withGet(array $options, callable $callback)
index 4563fb651ead8e1348f6f2aff205b2dbce88f7b5..170269941a2fb35a6d079e170088ff7ca5e69f48 100644 (file)
@@ -444,6 +444,26 @@ class EntitySearchTest extends TestCase
         $search->assertSee($page->getUrl(), false);
     }
 
+    public function test_backslashes_can_be_searched_upon()
+    {
+        $page = $this->entities->newPage(['name' => 'TermA', 'html' => '
+            <p>More info is at the path \\\\cat\\dog\\badger</p>
+        ']);
+        $page->tags()->save(new Tag(['name' => '\\Category', 'value' => '\\animals\\fluffy']));
+
+        $search = $this->asEditor()->get('/search?term=' . urlencode('\\\\cat\\dog'));
+        $search->assertSee($page->getUrl(), false);
+
+        $search = $this->asEditor()->get('/search?term=' . urlencode('"\\dog\\"'));
+        $search->assertSee($page->getUrl(), false);
+
+        $search = $this->asEditor()->get('/search?term=' . urlencode('"\\badger\\"'));
+        $search->assertDontSee($page->getUrl(), false);
+
+        $search = $this->asEditor()->get('/search?term=' . urlencode('[\\Categorylike%\\fluffy]'));
+        $search->assertSee($page->getUrl(), false);
+    }
+
     public function test_searches_with_user_filters_adds_them_into_advanced_search_form()
     {
         $resp = $this->asEditor()->get('/search?term=' . urlencode('test {updated_by:dan} {created_by:dan}'));
index 6d6224abf4be6d63b0407a1a0e42eeec568e7bd5..d8845fe127662d8008b1c88262761f6476810d71 100644 (file)
@@ -108,6 +108,18 @@ class PageContentTest extends TestCase
         $htmlContent->assertSee('my cat is awesome and scratchy');
     }
 
+    public function test_page_includes_can_be_nested_up_to_three_times()
+    {
+        $page = $this->entities->page();
+        $tag = "{{@{$page->id}#bkmrk-test}}";
+        $page->html = '<p id="bkmrk-test">Hello Barry ' . $tag . '</p>';
+        $page->save();
+
+        $pageResp = $this->asEditor()->get($page->getUrl());
+        $this->withHtml($pageResp)->assertElementContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);
+        $this->withHtml($pageResp)->assertElementNotContains('#bkmrk-test', 'Hello Barry Hello Barry Hello Barry Hello Barry Hello Barry ' . $tag);
+    }
+
     public function test_page_content_scripts_removed_by_default()
     {
         $this->asEditor();
index 03ae7b307d4d594bb93e93934487185108e9c50a..bc8163056f345bf1dbf25da6a526bf6aafbb9662 100644 (file)
@@ -23,8 +23,8 @@ use League\CommonMark\Environment\Environment;
 
 class ThemeTest extends TestCase
 {
-    protected $themeFolderName;
-    protected $themeFolderPath;
+    protected string $themeFolderName;
+    protected string $themeFolderPath;
 
     public function test_translation_text_can_be_overridden_via_theme()
     {
index 9966a4fb15114683393cb46e603da2e6431aa6d2..10376751633c5e225a1451cc361ddb41870ae2e5 100644 (file)
@@ -3,6 +3,8 @@
 namespace Tests\Unit;
 
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
+use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
 use Tests\TestCase;
 
 /**
@@ -96,11 +98,30 @@ class ConfigTest extends TestCase
         $this->checkEnvConfigResult('EXPORT_PAGE_SIZE', 'a4', 'snappy.pdf.options.page-size', 'A4');
     }
 
-    public function test_sendmail_command_is_configurage()
+    public function test_sendmail_command_is_configurable()
     {
         $this->checkEnvConfigResult('MAIL_SENDMAIL_COMMAND', '/var/sendmail -o', 'mail.mailers.sendmail.path', '/var/sendmail -o');
     }
 
+    public function test_mail_disable_ssl_verification_alters_mailer()
+    {
+        $getStreamOptions = function (): array {
+            /** @var EsmtpTransport $transport */
+            $transport = Mail::mailer('smtp')->getSymfonyTransport();
+            return $transport->getStream()->getStreamOptions();
+        };
+
+        $this->assertEmpty($getStreamOptions());
+
+
+        $this->runWithEnv('MAIL_VERIFY_SSL', 'false', function () use ($getStreamOptions) {
+            $options = $getStreamOptions();
+            $this->assertArrayHasKey('ssl', $options);
+            $this->assertFalse($options['ssl']['verify_peer']);
+            $this->assertFalse($options['ssl']['verify_peer_name']);
+        });
+    }
+
     /**
      * Set an environment variable of the given name and value
      * then check the given config key to see if it matches the given result.
index 53040ea086b00edc218f3cbb072c27d321dfbc87..97e36001c63a92a44d012b4c5a1f23e2d51d8833 100644 (file)
@@ -163,7 +163,8 @@ class ImageTest extends TestCase
 
         $file = $this->files->imageFromBase64File('bad-php.base64', $fileName);
         $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
-        $upload->assertStatus(302);
+        $upload->assertStatus(500);
+        $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message'));
 
         $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
 
@@ -185,7 +186,8 @@ class ImageTest extends TestCase
 
         $file = $this->files->imageFromBase64File('bad-phtml.base64', $fileName);
         $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []);
-        $upload->assertStatus(302);
+        $upload->assertStatus(500);
+        $this->assertStringContainsString('The file must have a valid & supported image extension', $upload->json('message'));
 
         $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
     }