Compare commits

...

414 Commits

Author SHA1 Message Date
Dan Brown
bf6a6af683 Updated version and assets for release v22.11 2022-11-30 12:30:21 +00:00
Dan Brown
914790fd99 Merge branch 'development' into release 2022-11-30 12:29:52 +00:00
Dan Brown
69d702c783 Updated locale list to align with lang folders 2022-11-30 12:13:50 +00:00
Dan Brown
dd92cf9e96 Updated translator attribution before v22.11 release 2022-11-30 12:02:10 +00:00
Dan Brown
0cd0b44cdb New Crowdin updates (#3828) 2022-11-30 12:01:19 +00:00
Dan Brown
31c28be57a Converted md settings to localstorage, added preview resize 2022-11-28 14:08:20 +00:00
Dan Brown
38db3a28ea Merge pull request #3878 from BookStackApp/dark_style_cleanup
Cleaned up dark mode styles inc. setting browser color scheme
2022-11-28 12:42:16 +00:00
Dan Brown
09fa2d2c9c Cleaned up dark mode styles inc. setting browser color scheme
Forces browser colorscheme based on BookStack color scheme, via
'color-scheme' css property.
Sets proper dark mode colors for some previously missed areas like
templates and attachment control buttons.
Also fixed search bar icon position for some search inputs.
2022-11-28 12:38:30 +00:00
Dan Brown
b786ed07be Merge pull request #3875 from BookStackApp/md_editor_updates
Markdown Editor Updates
2022-11-28 12:21:33 +00:00
Dan Brown
0527c4a1ea Added test to preference boolean endpoint 2022-11-28 12:17:22 +00:00
Dan Brown
ec3713bc74 Connected md editor settings to logic for functionality 2022-11-28 12:12:36 +00:00
Dan Brown
9fd5190c70 Added md editor ui dropdown options & their back-end storage
Still need to perform actual in-editor functionality for those controls.
2022-11-27 20:30:14 +00:00
Dan Brown
3995b01399 Tightened existing markdown editor styles 2022-11-27 19:52:10 +00:00
Dan Brown
3fdb88c7aa Added callout cycling in markdown editor via shortcut 2022-11-26 23:18:51 +00:00
Dan Brown
8e4bb32b77 Fixed md editor refactoring issues after manual test
Testing was a full manual feature test of each piece of supported logic
defined in the code.
2022-11-26 21:33:39 +00:00
Dan Brown
63d6272282 Refactored markdown editor logic
Split out the markdown editor logic into seperate components to provide
a more orgranised heirachy with feature-specific files.
2022-11-26 16:43:28 +00:00
Dan Brown
40a1377c0b Fixed tests to align with recent changes, Updated php deps 2022-11-23 12:08:55 +00:00
Dan Brown
e20c944350 Fixed OIDC handling when no JWKS 'use' prop exists
Now assume, based on OIDC discovery spec, that keys without 'use' are
'sig' keys. Should not affect existing use-cases since existance of such
keys would have throw exceptions in prev. versions of bookstack.

For #3869
2022-11-23 11:50:59 +00:00
Dan Brown
85b7b10c01 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2022-11-23 00:13:02 +00:00
Dan Brown
35f73bb474 Updated global search component to new format 2022-11-23 00:12:41 +00:00
Dan Brown
ffc9c28ad5 Merge branch 'search_preview' into development 2022-11-23 00:10:21 +00:00
Dan Brown
fcff206853 Adjusted global search preview for dark mode 2022-11-23 00:05:24 +00:00
Dan Brown
0e528986ab Extracted keyboard nav. from dropdowns to share w/ search 2022-11-21 17:35:19 +00:00
Dan Brown
e7e83a4109 Added new endpoint for search suggestions 2022-11-21 10:35:53 +00:00
Dan Brown
891543ff0a Merge pull request #3852 from BookStackApp/php82
PHP8.2 Support
2022-11-20 22:21:52 +00:00
Dan Brown
c617190905 Added global search input debounce and loading indicator 2022-11-20 22:20:31 +00:00
Dan Brown
2c1f20969a Replaced JS logic with CSS focus-within logic 2022-11-20 21:53:53 +00:00
Dan Brown
851ab47f8a Fixed input styles in search preview mode, added animation
Also added JS handlers for hiding the suggestions
2022-11-20 21:50:59 +00:00
Dan Brown
bbf13e9242 Merge pull request #3853 from BookStackApp/component_refactor
Started refactor and alignment of JS component system
2022-11-16 16:05:57 +00:00
Dan Brown
05a24ea355 Updated js dev docs with latest component changes 2022-11-16 16:02:31 +00:00
Dan Brown
be736b3939 Replaced el.components mapping with component service weakmap
Old system was hard to track in terms of usage and it's application of
'components' properties directly to elements was shoddy.
This routes usage via the components service, with element-specific
component usage tracked via a local weakmap.
Updated existing found usages to use the new system.
2022-11-16 15:46:41 +00:00
Dan Brown
25c23a2e5f Removed use of image-manager/entity-selector window globals 2022-11-16 15:21:22 +00:00
Dan Brown
3b8ee3954e Finished updating remainder of JS components to new system 2022-11-16 13:06:08 +00:00
Dan Brown
db79167469 Updated a whole load more js components 2022-11-15 16:04:46 +00:00
Dan Brown
b37e84dc10 Updated another set of components 2022-11-15 12:44:57 +00:00
Dan Brown
4310d34135 Updated a batch of JS components 2022-11-15 11:24:31 +00:00
Dan Brown
09c6a3c240 Started refactor and alignment of component system
- Updates old components to newer format, removes legacy component
support.
- Makes component registration easier and less duplicated.
- Adds base component class to extend for better editor support.
- Aligns global window exposure usage and aligns with other service
  names.
2022-11-14 23:19:02 +00:00
Dan Brown
796f4090b5 Added php8.2 to GH action checks 2022-11-14 18:26:01 +00:00
Dan Brown
19a792bc12 Started on a live-preview on global search input 2022-11-14 10:24:14 +00:00
Dan Brown
a1b1f8138a Updated email confirmation flow so confirmation is done via POST
To avoid non-user GET requests (Such as those from email scanners)
auto-triggering the confirm submission. Made auto-submit the form via
JavaScript in this extra added step with user-link backup to keep
existing user flow experience.

Closes #3797
2022-11-12 15:11:59 +00:00
Dan Brown
0e627a6e05 Merge pull request #3848 from BookStackApp/auth_message_partials
Added login/register message partials for easier use via theme system
2022-11-12 09:03:59 +00:00
Dan Brown
d2cd33e226 Added login/register message partials for easier use via theme system
Related to #608
2022-11-12 09:02:33 +00:00
Dan Brown
2fa5c2581c Added swift support to code blocks and editor
Closes #3847
2022-11-12 08:44:25 +00:00
Dan Brown
d2260b234c Fixed app logo visibility with secure_restricted images
Includes test to cover.
For #3827
2022-11-10 14:15:59 +00:00
Dan Brown
832356d56e Added test to cover books perms. gen with deleted chapter
Closes #3796
2022-11-10 13:48:17 +00:00
Dan Brown
5fd1c07c9d Added dart support to code blocks/editing
For #3808
2022-11-10 13:38:56 +00:00
Dan Brown
4c75358abd Extracted hardcoded english text to language files
Closes #3822
2022-11-10 13:30:48 +00:00
Dan Brown
d520d6cab8 Merge pull request #3830 from BookStackApp/shortcuts
User interface shortcuts system
2022-11-10 10:32:56 +00:00
Dan Brown
737904fa63 Extracted shortcut text to language files 2022-11-10 10:25:28 +00:00
Dan Brown
a3fcc98d6e Aligned user preference endpoints in style and behaviour
Changes their endpoints and remove the user id from the URLs.
Simplifies list changes to share a single endpoint, which aligns it to
the behaviour of the existing sort preference endpoint.
Also added test to ensure user preferences are deleted on user delete.
2022-11-09 19:30:08 +00:00
Dan Brown
24a7e8500d Added tests to cover shortcut endpoints 2022-11-09 18:42:54 +00:00
Dan Brown
9067902267 Added shortcut input controls to make custom shortcuts work 2022-11-09 14:40:44 +00:00
Dan Brown
66c8809799 Started interface user shortcut form interface
Built controller actions and initual UI.
Still needs JS logic for shortcut input handling.
2022-11-08 21:17:45 +00:00
Dan Brown
1fc994177f Improved shortcut overlay with related action highlighting 2022-11-05 13:57:22 +00:00
Dan Brown
78b6450031 Distributed shortcut actions to common ui elements 2022-11-05 13:39:17 +00:00
Dan Brown
b4cb375a02 Started implementation of UI shortcuts system 2022-11-04 15:20:19 +00:00
Dan Brown
33e5c85503 Merge pull request #3821 from BookStackApp/list_reworks
Revision of item list views
2022-11-03 14:52:40 +00:00
Dan Brown
9e8240a736 Addressed additional unsupported array spread operation 2022-11-03 14:40:01 +00:00
Dan Brown
37afd35b6f Fixed use of array unpacking syntax
Since it was using keyed arrays, unpacking is only supported in php8.1+
2022-11-03 14:33:23 +00:00
Dan Brown
6364c541ea Fixed phpstan static usage warning, updated ci flows
CI flow updates to follow deprecation warnings
2022-11-03 14:14:22 +00:00
Dan Brown
8ec6b07690 Updated role permission table to responsive format 2022-11-03 13:28:07 +00:00
Dan Brown
7101ec09ed Updated search term lists to flex layouts 2022-11-03 12:49:05 +00:00
Dan Brown
2c5efddf6c Merge branch 'v22-10' into development 2022-11-02 15:22:53 +00:00
Dan Brown
edb0c6a9e8 Updated version and assets for release v22.10.2 2022-11-02 15:22:13 +00:00
Dan Brown
84049de696 Merge branch 'v22-10' into release 2022-11-02 15:19:33 +00:00
Dan Brown
a37bdffcd9 Updated translator attribution before release v22.10.2 2022-11-02 15:19:13 +00:00
Dan Brown
e95ab36f76 Merged and squashed l10n_development into v22-10 2022-11-02 15:17:54 +00:00
Dan Brown
f809bd3a62 Updated tests to align with recent list changes 2022-11-01 14:53:36 +00:00
Dan Brown
d4e71e431b Revised revision list to responsive layout 2022-10-31 21:26:31 +00:00
Dan Brown
de807f8538 Updated recycle bin list to new responsive layout 2022-10-31 16:45:32 +00:00
Dan Brown
80d2889217 Updated tags list to new responsive format 2022-10-31 11:40:28 +00:00
Dan Brown
9e8516c2df Tweaked list spacings a little to align paddings 2022-10-30 21:06:42 +00:00
Dan Brown
09f2bc28d2 Removed addition detail spacing in audit list 2022-10-30 20:29:21 +00:00
Dan Brown
be320c5501 Adjusted audit log row spacing a tad 2022-10-30 20:27:41 +00:00
Dan Brown
2bbf7b2194 Revised audit log list to new responsive format 2022-10-30 20:24:08 +00:00
Dan Brown
ab184c01d8 Updated API tokens list to new responsive format 2022-10-30 15:37:52 +00:00
Dan Brown
2c114e1a4a Split out user controller preference methods to new controller 2022-10-30 15:25:02 +00:00
Dan Brown
ec4cbbd004 Refactored common list handling operations to new class 2022-10-30 15:16:06 +00:00
Dan Brown
f75091a1c5 Revised webhooks list to new format
Also aligned query naming to start with model in use.
Also added created/updated sort options to roles.
2022-10-30 12:02:06 +00:00
Dan Brown
98b59a1024 Revised role index list to align with user list 2022-10-29 20:52:17 +01:00
Dan Brown
0ef06fd298 Extracted user list item to its own template 2022-10-29 15:25:28 +01:00
Dan Brown
986346a0e9 Redesigned users list to be responsive and aligned 2022-10-29 15:23:21 +01:00
Dan Brown
2a65331573 Worked towards phpstan level 2, 13 errors remain 2022-10-24 12:12:48 +01:00
Dan Brown
45d0860448 Updated npm package versions 2022-10-24 11:40:05 +01:00
Dan Brown
da0531e63b Updated version and assets for release v22.10.1 2022-10-21 21:52:32 +01:00
Dan Brown
421dc75f4e Merge branch 'development' into release 2022-10-21 21:52:16 +01:00
Dan Brown
ea6eacb400 Fixed chapter fetching during joint permission building
Somehow I accidentally deleted previous line 143 in this commit:
3839bf6bf1
which would then break permission generation for content related to, or
containing, chapters in the recycle bin.
Found via user report (subz) & debugging in discord.
2022-10-21 21:49:29 +01:00
Dan Brown
8ae91df038 Updated version and assets for release v22.10 2022-10-21 11:16:45 +01:00
Dan Brown
64b41dd626 Merge branch 'development' into release 2022-10-21 11:16:25 +01:00
Dan Brown
103649887f Updated translator attribution before release v22.10 2022-10-21 11:15:35 +01:00
Dan Brown
7b2fd515da Updated test to align with latest translation 2022-10-21 10:41:55 +01:00
Dan Brown
3f61bfc43c Fixed toggle controls on added content permission role rows 2022-10-21 10:13:11 +01:00
Dan Brown
905d339572 Added greek language option 2022-10-20 12:25:02 +01:00
Dan Brown
5d37a814fd New Crowdin updates (#3737) 2022-10-20 12:18:58 +01:00
Dan Brown
f9c0edbd0c Set fixed cell widths for users list table
To prevent certain cells squashing others.
Related to #3787.
2022-10-19 11:15:17 +01:00
Dan Brown
d084f225a0 Updated page pointer to use a fixed positioning system
Avoids interferance with elements that have their own overflow behaviour
such as table cells.
Related to #3774
2022-10-18 22:40:13 +01:00
Dan Brown
ff3fb2ebb9 Extracted page pointer to its own compontent 2022-10-18 22:02:34 +01:00
Dan Brown
ebd6e4d3a2 Updated version and assets for release v22.09.1 2022-09-20 13:19:34 +01:00
Dan Brown
80374aea5c Merge branch 'development' into release 2022-09-20 13:19:03 +01:00
Dan Brown
2ac9efae7d Updated version and assets for release v22.09 2022-09-08 12:41:09 +01:00
Dan Brown
a11d565ba4 Merge branch 'development' into release 2022-09-08 12:40:57 +01:00
Dan Brown
1fdf854ea7 Updated version and assets for release v22.07.3 2022-08-11 15:17:06 +01:00
Dan Brown
e9c9792cb9 Merge branch 'development' into release 2022-08-11 15:16:34 +01:00
Dan Brown
5ae524c25a Updated version and assets for release v22.07.2 2022-08-09 13:55:52 +01:00
Dan Brown
0d7287fc8b Merge branch 'development' into release 2022-08-09 13:55:40 +01:00
Dan Brown
e77c96f6b7 Updated version and assets for release v22.07.1 2022-08-02 11:47:25 +01:00
Dan Brown
9b8a10dd3a Merge branch 'development' into release 2022-08-02 11:47:08 +01:00
Dan Brown
49200ca5ce Updated version and assets for release v22.07 2022-07-28 14:53:15 +01:00
Dan Brown
34aa4dbf10 Merge branch 'development' into release 2022-07-28 14:53:01 +01:00
Dan Brown
5ee79d16c9 Updated version and assets for release v22.06.2 2022-06-28 11:57:37 +01:00
Dan Brown
a1ea4006e0 Merge branch 'development' into release 2022-06-28 11:57:24 +01:00
Dan Brown
9078188939 Updated version and assets for release v22.06.1 2022-06-25 14:33:07 +01:00
Dan Brown
ed0aad1a7a Merge branch 'development' into release 2022-06-25 14:32:49 +01:00
Dan Brown
5c59cfb020 Updated version and assets for release v22.06 2022-06-24 11:50:56 +01:00
Dan Brown
3ca15ad68a Merge branch 'development' into release 2022-06-24 11:45:29 +01:00
Dan Brown
60014989f5 Updated version and assets for release v22.04.2 2022-05-09 16:10:16 +01:00
Dan Brown
57b10f195e Merge branch 'development' into release 2022-05-09 16:09:54 +01:00
Dan Brown
b1e95eb39f Updated version and assets for release v22.04.1 2022-05-04 21:26:58 +01:00
Dan Brown
b3da77b8f9 Merge branch 'development' into release 2022-05-04 21:26:31 +01:00
Dan Brown
1a345b74bb Updated version and assets for release v22.04 2022-04-29 15:55:32 +01:00
Dan Brown
8ffc3a4abf Merge branch 'development' into release 2022-04-29 15:55:05 +01:00
Dan Brown
7233c1c7b2 Updated version and assets for release v22.03.1 2022-03-30 19:37:07 +01:00
Dan Brown
1309a01131 Merge branch 'development' into release 2022-03-30 19:36:45 +01:00
Dan Brown
0333185b6d Updated version and assets for release v22.03 2022-03-30 13:49:17 +01:00
Dan Brown
83f89f64e8 Merge branch 'development' into release 2022-03-30 13:49:05 +01:00
Dan Brown
11a1a6fb16 Updated version and assets for release v22.02.3 2022-03-07 15:12:22 +00:00
Dan Brown
882c609296 Merge branch 'development' into release 2022-03-07 15:12:09 +00:00
Dan Brown
176a0dcd59 Updated version and assets for release v22.02.2 2022-03-01 22:45:41 +00:00
Dan Brown
94b0f70bfa Merge branch 'development' into release 2022-03-01 22:45:12 +00:00
Dan Brown
08b2a77d41 Updated version and assets for release v22.02.1 2022-02-27 17:46:06 +00:00
Dan Brown
3e8e9a23cf Merge branch 'development' into release 2022-02-27 17:45:49 +00:00
Dan Brown
58b83b64c8 Updated version and assets for release v22.02 2022-02-26 12:01:44 +00:00
Dan Brown
dfe4cde6ee Merge branch 'development' into release 2022-02-26 12:00:46 +00:00
Dan Brown
d11144d9e2 Updated version and assets for release v21.12.5 2022-02-06 15:49:23 +00:00
Dan Brown
f96b0ea5f3 Merge branch 'development' into release 2022-02-06 15:48:55 +00:00
Dan Brown
815f8d79ed Updated version and assets for release v21.12.4 2022-02-01 11:52:24 +00:00
Dan Brown
b62dab32e0 Merge branch 'development' into release 2022-02-01 11:51:48 +00:00
Dan Brown
262f863981 Updated version and assets for release v21.12.3 2022-01-24 22:49:42 +00:00
Dan Brown
a4c94390a1 Merge branch 'master' into release 2022-01-24 22:49:31 +00:00
Dan Brown
53f3cca85d Updated version and assets for release v21.12.2 2022-01-10 18:23:44 +00:00
Dan Brown
ed08bbcecc Merge branch 'master' into release 2022-01-10 18:23:19 +00:00
Dan Brown
de97ebf9b7 Updated version and assets for release v21.12.1 2022-01-06 12:20:37 +00:00
Dan Brown
f492a660a8 Merge branch 'master' into release 2022-01-06 12:20:26 +00:00
Dan Brown
09436836a5 Updated version and assets for release v21.12 2021-12-22 17:04:18 +00:00
Dan Brown
bb455d7788 Merge branch 'master' into release 2021-12-22 17:03:50 +00:00
Dan Brown
009212ab80 Updated version and assets for release v21.11.3 2021-12-15 14:08:37 +00:00
Dan Brown
ba9cb591c8 Merge branch 'master' into release 2021-12-15 14:08:17 +00:00
Dan Brown
d00ac2f34e Updated version and assets for release v21.11.2 2021-11-30 14:30:19 +00:00
Dan Brown
bd4dc6d463 Merge branch 'master' into release 2021-11-30 14:29:53 +00:00
Dan Brown
d91180a909 Updated version and assets for release v21.11.1 2021-11-23 20:44:36 +00:00
Dan Brown
bc2913a5cb Merge branch 'master' into release 2021-11-23 20:44:12 +00:00
Dan Brown
4802394562 Updated version and assets for release v21.11 2021-11-16 13:22:24 +00:00
Dan Brown
1755556468 Merge branch 'master' into release 2021-11-16 13:21:44 +00:00
Dan Brown
01cdbdb7ae Updated version and assets for release v21.10.3 2021-11-01 13:31:10 +00:00
Dan Brown
fc8bbf3eab Merge branch 'master' into release 2021-11-01 13:30:36 +00:00
Dan Brown
3cdab19319 Updated version and assets for release v21.10.2 2021-10-28 15:57:04 +01:00
Dan Brown
5661d20e87 Merge branch 'master' into release 2021-10-28 15:56:49 +01:00
Dan Brown
91f80123e8 Merge branch 'master' into release 2021-10-27 12:35:00 +01:00
Dan Brown
7a0636d0f8 Updated version and assets for release v21.10.1 2021-10-27 12:31:40 +01:00
Dan Brown
0fe5bdfbac Updated version and assets for release v21.10 2021-10-25 15:59:23 +01:00
Dan Brown
f88687e977 Merge branch 'master' into release 2021-10-25 15:58:59 +01:00
Dan Brown
68d437d05b Updated version and assets for release v21.08.6 2021-10-15 14:34:44 +01:00
Dan Brown
1e56aaea04 Merge branch 'master' into release 2021-10-15 14:34:23 +01:00
Dan Brown
dab170a6fe Updated version and assets for release v21.08.5 2021-10-08 22:25:36 +01:00
Dan Brown
a8de717d9b Merge branch 'master' into release 2021-10-08 22:25:05 +01:00
Dan Brown
78fe95b6fc Updated version and assets for release v21.08.4 2021-10-04 16:25:24 +01:00
Dan Brown
e0c24e41aa Merge branch 'master' into release 2021-10-04 16:24:54 +01:00
Dan Brown
fa8553839b Updated version and assets for release v21.08.3 2021-09-12 16:31:02 +01:00
Dan Brown
b8fcefc794 Merge branch 'master' into release 2021-09-12 16:30:35 +01:00
Dan Brown
88bcb68fcb Updated version and assets for release v21.08.2 2021-09-04 15:07:20 +01:00
Dan Brown
7c000553ae Merge branch 'master' into release 2021-09-04 15:06:33 +01:00
Dan Brown
391fa35c80 Updated version and assets for release v21.08.1 2021-09-02 21:13:09 +01:00
Dan Brown
c6773a8c9f Merge branch 'master' into release 2021-09-02 21:12:06 +01:00
Dan Brown
9b226e7d39 Updated version and assets for release v21.08 2021-08-31 22:07:53 +01:00
Dan Brown
9865446267 Merge branch 'master' into release 2021-08-31 22:07:23 +01:00
Dan Brown
926abbe776 Updated version and assets for release v21.05.4 2021-08-04 21:29:10 +01:00
Dan Brown
4fabef3a57 Merge branch 'v21.05.x' into release 2021-08-04 21:28:45 +01:00
Dan Brown
5ef4cd80c3 Updated version and assets for release v21.05.3 2021-07-03 11:59:52 +01:00
Dan Brown
e01f23583f Merge branch 'v21.05.x' into release 2021-07-03 11:59:21 +01:00
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
78886b1e67 Updated version and assets for release v21.04.1 2021-04-19 22:26:19 +01:00
Dan Brown
d9debaf032 Merge branch 'master' into release 2021-04-19 22:25:29 +01:00
Dan Brown
d4360d6347 Updated version and assets for release v21.04 2021-04-09 21:18:32 +01:00
Dan Brown
175b1785c0 Merge branch 'master' into release 2021-04-09 21:18:09 +01:00
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
0e799a3857 Updated version and assets for release v0.31.2 2021-01-10 14:05:16 +00:00
Dan Brown
b91d6e2bfa Merge branch 'master' into release 2021-01-10 14:04:59 +00:00
Dan Brown
ea16ad7e94 Updated version and assets for release v0.31.1 2021-01-04 18:41:55 +00:00
Dan Brown
ba6eb54552 Merge branch 'master' into release 2021-01-04 18:41:26 +00:00
Dan Brown
f705e7683b Updated assets for release v0.31.0 again 2021-01-03 22:33:36 +00:00
Dan Brown
dc996adb20 Merge branch 'master' into release 2021-01-03 22:32:40 +00:00
Dan Brown
a64c638ccc Updated version and assets for release v0.31.0 2021-01-03 21:52:37 +00:00
Dan Brown
359c067279 Merge branch 'master' into release 2021-01-03 21:52:00 +00:00
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
4818192a2a Updated version and assets for release v0.29.1 2020-04-28 12:30:31 +01:00
Dan Brown
965dd97f54 Merge branch 'master' into release 2020-04-28 12:30:09 +01:00
Dan Brown
195b74926c Updated version and assets for release v0.29.0 2020-04-13 16:10:23 +01:00
Dan Brown
2120db12b2 Merge branch 'master' into release 2020-04-13 16:10:11 +01:00
Dan Brown
ed563fef28 Updated version and assets for release v0.28.3 2020-03-14 22:31:42 +00:00
Dan Brown
0d31a8e3f1 Merge branch 'master' into release 2020-03-14 22:31:11 +00:00
Dan Brown
b8354b974b Updated version and assets for release v0.28.2 2020-02-15 22:36:08 +00:00
Dan Brown
034c1e289d Merge branch 'master' into release 2020-02-15 22:35:46 +00:00
Dan Brown
f31605a3de Updated version and assets for release v0.28.1 2020-02-15 22:08:06 +00:00
Dan Brown
e7cc75c74d Merge branch 'master' into release 2020-02-15 22:07:17 +00:00
Dan Brown
4b79d5e4e8 Updated version and assets for release v0.28.0 2020-02-03 22:44:45 +00:00
Dan Brown
34854915b3 Merge branch 'master' into release 2020-02-03 22:43:58 +00:00
Dan Brown
af6f34b529 Updated version and assets for release v0.27.5 2019-10-16 16:35:50 +01:00
Dan Brown
fb82a2b896 Merge branch 'patching-v0.27' into release 2019-10-16 16:35:10 +01:00
Dan Brown
5b464938b6 Updated version and assets for release v0.27.4 2019-09-07 13:30:08 +01:00
Dan Brown
81f954890d Merge branch 'patching-v0.27' into release 2019-09-07 13:29:53 +01:00
Dan Brown
0e2bbcec62 Updated version and assets for release v0.27.3 2019-09-03 21:50:12 +01:00
Dan Brown
fdd339f525 Merge branch 'master' into release 2019-09-03 21:49:46 +01:00
Dan Brown
8cf7d6a83d Updated version and assets for release v0.27.2 2019-09-01 12:12:23 +01:00
Dan Brown
58a5008718 Merge branch 'master' into release 2019-09-01 12:12:10 +01:00
Dan Brown
c44a8df55d Updated version and assets for release v0.27.1 2019-09-01 11:13:50 +01:00
Dan Brown
ff1494c519 Merge branch 'master' into release 2019-09-01 11:13:18 +01:00
Dan Brown
b8ce8fd852 Updated assets for release v0.27 2019-08-31 14:16:14 +01:00
Dan Brown
75e7454a5f Merge branch 'master' into release and set version 2019-08-31 14:15:18 +01:00
Dan Brown
2558ea8931 Updated version for release v0.26.4 2019-08-06 21:42:09 +01:00
Dan Brown
ac0f47a4b2 Merge branch 'v0.26' into release 2019-08-06 21:41:06 +01:00
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
b70a5c0cdb Updated version and assets for release v0.26.1 2019-05-07 23:05:47 +01:00
Dan Brown
9443ae9f40 Merge branch 'master' into release 2019-05-07 23:05:10 +01:00
Dan Brown
220c2a4102 Updated version and assets for release v0.26.0 2019-05-06 18:58:56 +01:00
Dan Brown
e9914eb301 Merge branch 'master' into release 2019-05-06 18:57:58 +01:00
Dan Brown
934512d09c Updated version and assets for release v0.25.5 2019-03-24 19:45:17 +00:00
Dan Brown
9102c90986 Merge branch 'master' into release 2019-03-24 19:45:00 +00:00
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
95b3e78573 Updated version and assets for release v0.25.0 2019-01-12 22:48:53 +00:00
Dan Brown
63a345bc93 Merge branch 'master' into release 2019-01-12 22:47:07 +00:00
Dan Brown
e093a172cb Updated assets and version for release v0.24.3 2018-11-27 21:52:20 +00:00
Dan Brown
4b01f8934b Merge branch 'master' into release 2018-11-27 21:51:32 +00:00
Dan Brown
bc116b45b5 Re-updated assets for release v0.24.2 2018-11-10 16:10:22 +00:00
Dan Brown
a059960b9e Merge branch 'master' into release 2018-11-10 16:09:14 +00:00
Dan Brown
7770966fed Updated assets for release v0.24.2 2018-11-10 16:01:55 +00:00
Dan Brown
d7adcf6c69 Merge branch 'master' into release 2018-11-10 16:01:01 +00:00
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
6ad21ce885 Added built assets for release 2015-11-30 21:59:34 +00:00
531 changed files with 11354 additions and 4617 deletions

View File

@@ -280,3 +280,21 @@ DerLinkman (derlinkman) :: German; German Informal
TurnArabic :: Arabic
Martin Sebek (sebekmartin) :: Czech
Kuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified
digilady :: Greek
Linus (LinusOP) :: Swedish
Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian
RandomUser0815 :: German
Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
구인회 (laskdjlaskdj12) :: Korean
LiZerui (CNLiZerui) :: Chinese Traditional
Fabrice Boyer (FabriceBoyer) :: French
mikael (bitcanon) :: Swedish
Matthias Mai (schnapsidee) :: German
Ufuk Ayyıldız (ufukayyildiz) :: Turkish
Jan Mitrof (jan.kachlik) :: Czech
edwardsmirnov :: Russian
Mr_OSS117 :: French
shotu :: French
Cesar_Lopez_Aguillon :: Spanish
bdewoop :: German
dina davoudi (dina.davoudi) :: Persian

View File

@@ -18,10 +18,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-8.1

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1']
php: ['7.4', '8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1
@@ -21,10 +21,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1']
php: ['7.4', '8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1
@@ -21,10 +21,10 @@ jobs:
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}

6
.gitignore vendored
View File

@@ -5,10 +5,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist
/public/dist/*.map
/public/plugins
/public/css
/public/js
/public/css/*.map
/public/js/*.map
/public/bower
/public/build/
/storage/images

View File

@@ -0,0 +1,30 @@
<?php
namespace BookStack\Actions\Queries;
use BookStack\Actions\Webhook;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
* Get all the webhooks in the system in a paginated format.
*/
class WebhooksAllPaginatedAndSorted
{
public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
$query = Webhook::query()->select(['*'])
->withCount(['trackedEvents'])
->orderBy($listOptions->getSort(), $listOptions->getOrder());
if ($listOptions->getSearch()) {
$term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('endpoint', 'like', $term);
});
}
return $query->paginate($count);
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
use BookStack\Util\SimpleListOptions;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -20,8 +21,14 @@ class TagRepo
/**
* Start a query against all tags in the system.
*/
public function queryWithTotals(string $searchTerm, string $nameFilter): Builder
public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder
{
$searchTerm = $listOptions->getSearch();
$sort = $listOptions->getSort();
if ($sort === 'name' && $nameFilter) {
$sort = 'value';
}
$query = Tag::query()
->select([
'name',
@@ -32,7 +39,7 @@ class TagRepo
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
])
->orderBy($nameFilter ? 'value' : 'name');
->orderBy($sort, $listOptions->getOrder());
if ($nameFilter) {
$query->where('name', '=', $nameFilter);

View File

@@ -4,21 +4,29 @@ namespace BookStack\Api;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ListingResponseBuilder
{
protected $query;
protected $request;
protected $fields;
protected Builder $query;
protected Request $request;
/**
* @var string[]
*/
protected array $fields;
/**
* @var array<callable>
*/
protected $resultModifiers = [];
protected array $resultModifiers = [];
protected $filterOperators = [
/**
* @var array<string, string>
*/
protected array $filterOperators = [
'eq' => '=',
'ne' => '!=',
'gt' => '>',
@@ -62,9 +70,9 @@ class ListingResponseBuilder
/**
* Add a callback to modify each element of the results.
*
* @param (callable(Model)) $modifier
* @param (callable(Model): void) $modifier
*/
public function modifyResults($modifier): void
public function modifyResults(callable $modifier): void
{
$this->resultModifiers[] = $modifier;
}

View File

@@ -67,11 +67,10 @@ class OidcJwtSigningKey
throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
}
if (empty($jwk['use'])) {
throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
}
if ($jwk['use'] !== 'sig') {
// 'use' is optional for a JWK but we assume 'sig' where no value exists since that's what
// the OIDC discovery spec infers since 'sig' MUST be set if encryption keys come into play.
$use = $jwk['use'] ?? 'sig';
if ($use !== 'sig') {
throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
}

View File

@@ -15,40 +15,17 @@ use Psr\Http\Client\ClientInterface;
*/
class OidcProviderSettings
{
/**
* @var string
*/
public $issuer;
/**
* @var string
*/
public $clientId;
/**
* @var string
*/
public $clientSecret;
/**
* @var string
*/
public $redirectUri;
/**
* @var string
*/
public $authorizationEndpoint;
/**
* @var string
*/
public $tokenEndpoint;
public string $issuer;
public string $clientId;
public string $clientSecret;
public ?string $redirectUri;
public ?string $authorizationEndpoint;
public ?string $tokenEndpoint;
/**
* @var string[]|array[]
*/
public $keys = [];
public ?array $keys = [];
public function __construct(array $settings)
{
@@ -164,9 +141,10 @@ class OidcProviderSettings
protected function filterKeys(array $keys): array
{
return array_filter($keys, function (array $key) {
$alg = $key['alg'] ?? null;
$alg = $key['alg'] ?? 'RS256';
$use = $key['use'] ?? 'sig';
return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256');
return $key['kty'] === 'RSA' && $use === 'sig' && $alg === 'RS256';
});
}

View File

@@ -52,7 +52,6 @@ class OidcService
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
return [
'url' => $provider->getAuthorizationUrl(),
'state' => $provider->getState(),

View File

@@ -22,7 +22,7 @@ class JointPermissionBuilder
/**
* @var array<string, array<int, SimpleEntityData>>
*/
protected $entityCache;
protected array $entityCache;
/**
* Re-generate all entity permission from scratch.
@@ -140,6 +140,7 @@ class JointPermissionBuilder
return Book::query()->withTrashed()
->select(['id', 'owned_by'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);
@@ -229,7 +230,7 @@ class JointPermissionBuilder
/**
* Create & Save entity jointPermissions for many entities and roles.
*
* @param Entity[] $entities
* @param Entity[] $originalEntities
* @param Role[] $roles
*/
protected function createManyJointPermissions(array $originalEntities, array $roles)

View File

@@ -0,0 +1,35 @@
<?php
namespace BookStack\Auth\Queries;
use BookStack\Auth\Role;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
* Get all the roles in the system in a paginated format.
*/
class RolesAllPaginatedAndSorted
{
public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
$sort = $listOptions->getSort();
if ($sort === 'created_at') {
$sort = 'users.created_at';
}
$query = Role::query()->select(['*'])
->withCount(['users', 'permissions'])
->orderBy($sort, $listOptions->getOrder());
if ($listOptions->getSearch()) {
$term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('display_name', 'like', $term)
->orWhere('description', 'like', $term);
});
}
return $query->paginate($count);
}
}

View File

@@ -3,6 +3,7 @@
namespace BookStack\Auth\Queries;
use BookStack\Auth\User;
use BookStack\Util\SimpleListOptions;
use Illuminate\Pagination\LengthAwarePaginator;
/**
@@ -11,23 +12,23 @@ use Illuminate\Pagination\LengthAwarePaginator;
* user is assumed to be trusted. (Admin users).
* Email search can be abused to extract email addresses.
*/
class AllUsersPaginatedAndSorted
class UsersAllPaginatedAndSorted
{
/**
* @param array{sort: string, order: string, search: string} $sortData
*/
public function run(int $count, array $sortData): LengthAwarePaginator
public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
{
$sort = $sortData['sort'];
$sort = $listOptions->getSort();
if ($sort === 'created_at') {
$sort = 'users.created_at';
}
$query = User::query()->select(['*'])
->scopes(['withLastActivityAt'])
->with(['roles', 'avatar'])
->withCount('mfaValues')
->orderBy($sort, $sortData['order']);
->orderBy($sort, $listOptions->getOrder());
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
if ($listOptions->getSearch()) {
$term = '%' . $listOptions->getSearch() . '%';
$query->where(function ($query) use ($term) {
$query->where('name', 'like', $term)
->orWhere('email', 'like', $term);

View File

@@ -110,14 +110,6 @@ class Role extends Model implements Loggable
return static::query()->where('system_name', '=', $systemName)->first();
}
/**
* Get all visible roles.
*/
public static function visible(): Collection
{
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
}
/**
* {@inheritdoc}
*/

View File

@@ -158,6 +158,9 @@ class UserRepo
// Delete user profile images
$this->userAvatar->destroyAllForUser($user);
// Delete related activities
setting()->deleteUserSettings($user->id);
if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {

View File

@@ -75,7 +75,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'el', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ka', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
// Application Fallback Locale
'fallback_locale' => 'en',

View File

@@ -26,6 +26,8 @@ return [
// User-level default settings
'user' => [
'ui-shortcuts' => '{}',
'ui-shortcuts-enabled' => false,
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),

View File

@@ -88,8 +88,6 @@ class Page extends BookChild
/**
* Get the current revision for the page if existing.
*
* @return PageRevision|null
*/
public function currentRevision(): HasOne
{

View File

@@ -87,14 +87,14 @@ class BaseRepo
{
if ($coverImage) {
$imageType = $entity->coverImageTypeKey();
$this->imageRepo->destroyImage($entity->cover);
$this->imageRepo->destroyImage($entity->cover()->first());
$image = $this->imageRepo->saveNew($coverImage, $imageType, $entity->id, 512, 512, true);
$entity->cover()->associate($image);
$entity->save();
}
if ($removeImage) {
$this->imageRepo->destroyImage($entity->cover);
$this->imageRepo->destroyImage($entity->cover()->first());
$entity->image_id = 0;
$entity->save();
}

View File

@@ -181,7 +181,7 @@ class BookContents
$model->changeBook($newBook->id);
}
if ($chapterChanged) {
if ($model instanceof Page && $chapterChanged) {
$model->chapter_id = $newChapter->id ?? 0;
}
@@ -235,7 +235,7 @@ class BookContents
}
$hasPageEditPermission = userCan('page-update', $model);
$newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id);
$newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));
$newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
$hasNewParentPermission = userCan($newParentPermission, $newParent);

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\ChapterRepo;
@@ -109,9 +110,11 @@ class Cloner
$inputData['tags'] = $this->entityTagsToInputArray($entity);
// Add a cover to the data if existing on the original entity
if ($entity->cover instanceof Image) {
$uploadedFile = $this->imageToUploadedFile($entity->cover);
$inputData['image'] = $uploadedFile;
if ($entity instanceof HasCoverImage) {
$cover = $entity->cover()->first();
if ($cover) {
$inputData['image'] = $this->imageToUploadedFile($cover);
}
}
return $inputData;

View File

@@ -3,6 +3,8 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityType;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -13,10 +15,15 @@ class AuditLogController extends Controller
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->get('order', 'desc'),
$sort = $request->get('sort', 'activity_date');
$order = $request->get('order', 'desc');
$listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
'created_at' => trans('settings.audit_table_date'),
'type' => trans('settings.audit_table_event'),
]);
$filters = [
'event' => $request->get('event', ''),
'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'user' => $request->get('user', ''),
@@ -25,39 +32,38 @@ class AuditLogController extends Controller
$query = Activity::query()
->with([
'entity' => function ($query) {
$query->withTrashed();
},
'entity' => fn ($query) => $query->withTrashed(),
'user',
])
->orderBy($listDetails['sort'], $listDetails['order']);
->orderBy($listOptions->getSort(), $listOptions->getOrder());
if ($listDetails['event']) {
$query->where('type', '=', $listDetails['event']);
if ($filters['event']) {
$query->where('type', '=', $filters['event']);
}
if ($listDetails['user']) {
$query->where('user_id', '=', $listDetails['user']);
if ($filters['user']) {
$query->where('user_id', '=', $filters['user']);
}
if ($listDetails['date_from']) {
$query->where('created_at', '>=', $listDetails['date_from']);
if ($filters['date_from']) {
$query->where('created_at', '>=', $filters['date_from']);
}
if ($listDetails['date_to']) {
$query->where('created_at', '<=', $listDetails['date_to']);
if ($filters['date_to']) {
$query->where('created_at', '<=', $filters['date_to']);
}
if ($listDetails['ip']) {
$query->where('ip', 'like', $listDetails['ip'] . '%');
if ($filters['ip']) {
$query->where('ip', 'like', $filters['ip'] . '%');
}
$activities = $query->paginate(100);
$activities->appends($listDetails);
$activities->appends($request->all());
$types = DB::table('activities')->select('type')->distinct()->pluck('type');
$types = ActivityType::all();
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
'listDetails' => $listDetails,
'filters' => $filters,
'listOptions' => $listOptions,
'activityTypes' => $types,
]);
}

View File

@@ -51,14 +51,28 @@ class ConfirmEmailController extends Controller
return view('auth.user-unconfirmed', ['user' => $user]);
}
/**
* Show the form for a user to provide their positive confirmation of their email.
*/
public function showAcceptForm(string $token)
{
return view('auth.register-confirm-accept', ['token' => $token]);
}
/**
* Confirms an email via a token and logs the user into the system.
*
* @throws ConfirmationEmailException
* @throws Exception
*/
public function confirm(string $token)
public function confirm(Request $request)
{
$validated = $this->validate($request, [
'token' => ['required', 'string']
]);
$token = $validated['token'];
try {
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
} catch (UserTokenNotFoundException $exception) {

View File

@@ -15,6 +15,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -35,13 +36,16 @@ class BookController extends Controller
/**
* Display a listing of the book.
*/
public function index()
public function index(Request $request)
{
$view = setting()->getForCurrentUser('books_view_type');
$sort = setting()->getForCurrentUser('books_sort', 'name');
$order = setting()->getForCurrentUser('books_sort_order', 'asc');
$listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$books = $this->bookRepo->getAllPaginated(18, $sort, $order);
$books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
$popular = $this->bookRepo->getPopular(4);
$new = $this->bookRepo->getRecentlyCreated(4);
@@ -56,8 +60,7 @@ class BookController extends Controller
'popular' => $popular,
'new' => $new,
'view' => $view,
'sort' => $sort,
'order' => $order,
'listOptions' => $listOptions,
]);
}

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -30,18 +31,16 @@ class BookshelfController extends Controller
/**
* Display a listing of the book.
*/
public function index()
public function index(Request $request)
{
$view = setting()->getForCurrentUser('bookshelves_view_type');
$sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
$order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
$listOptions = SimpleListOptions::fromRequest($request, 'bookshelves')->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
]);
$shelves = $this->shelfRepo->getAllPaginated(18, $sort, $order);
$shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
$recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
$popular = $this->shelfRepo->getPopular(4);
$new = $this->shelfRepo->getRecentlyCreated(4);
@@ -55,9 +54,7 @@ class BookshelfController extends Controller
'popular' => $popular,
'new' => $new,
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
'listOptions' => $listOptions,
]);
}
@@ -100,16 +97,21 @@ class BookshelfController extends Controller
*
* @throws NotFoundException
*/
public function show(ActivityQueries $activities, string $slug)
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$shelf = $this->shelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf);
$sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
$order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
'default' => trans('common.sort_default'),
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$sort = $listOptions->getSort();
$sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $order === 'desc')
->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $listOptions->getOrder() === 'desc')
->values()
->all();
@@ -124,8 +126,7 @@ class BookshelfController extends Controller
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
'activity' => $activities->entityActivity($shelf, 20, 1),
'order' => $order,
'sort' => $sort,
'listOptions' => $listOptions,
'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
]);
}

View File

@@ -10,13 +10,15 @@ use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
class HomeController extends Controller
{
/**
* Display the homepage.
*/
public function index(ActivityQueries $activities)
public function index(Request $request, ActivityQueries $activities)
{
$activity = $activities->latest(10);
$draftPages = [];
@@ -61,33 +63,27 @@ class HomeController extends Controller
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getForCurrentUser($key . '_view_type');
$sort = setting()->getForCurrentUser($key . '_sort', 'name');
$order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
$listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
]);
$commonData = array_merge($commonData, [
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
'listOptions' => $listOptions,
]);
}
if ($homepageOption === 'bookshelves') {
$shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('home.shelves', $data);
}
if ($homepageOption === 'books') {
$bookRepo = app(BookRepo::class);
$books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
$books = app(BookRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
$data = array_merge($commonData, ['books' => $books]);
return view('home.books', $data);

View File

@@ -3,10 +3,13 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\PageRevision;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Ssddanbrown\HtmlDiff\Diff;
class PageRevisionController extends Controller
@@ -23,22 +26,29 @@ class PageRevisionController extends Controller
*
* @throws NotFoundException
*/
public function index(string $bookSlug, string $pageSlug)
public function index(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
'id' => trans('entities.pages_revisions_sort_number')
]);
$revisions = $page->revisions()->select([
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
'type', 'revision_number', 'summary',
])
'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
'type', 'revision_number', 'summary',
])
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
->get();
->reorder('id', $listOptions->getOrder())
->reorder('created_at', $listOptions->getOrder())
->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
return view('pages.revisions', [
'revisions' => $revisions,
'page' => $page,
'revisions' => $revisions,
'page' => $page,
'listOptions' => $listOptions,
]);
}
@@ -50,6 +60,7 @@ class PageRevisionController extends Controller
public function show(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
throw new NotFoundException();
@@ -78,6 +89,7 @@ class PageRevisionController extends Controller
public function changes(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
/** @var ?PageRevision $revision */
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
throw new NotFoundException();

View File

@@ -3,19 +3,18 @@
namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Queries\RolesAllPaginatedAndSorted;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class RoleController extends Controller
{
protected $permissionsRepo;
protected PermissionsRepo $permissionsRepo;
/**
* PermissionController constructor.
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
@@ -24,14 +23,27 @@ class RoleController extends Controller
/**
* Show a listing of the roles in the system.
*/
public function index()
public function index(Request $request)
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
$listOptions = SimpleListOptions::fromRequest($request, 'roles')->withSortOptions([
'display_name' => trans('common.sort_name'),
'users_count' => trans('settings.roles_assigned_users'),
'permissions_count' => trans('settings.roles_permissions_provided'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
]);
$roles = (new RolesAllPaginatedAndSorted())->run(20, $listOptions);
$roles->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.roles'));
return view('settings.roles.index', ['roles' => $roles]);
return view('settings.roles.index', [
'roles' => $roles,
'listOptions' => $listOptions,
]);
}
/**
@@ -75,16 +87,11 @@ class RoleController extends Controller
/**
* Show the form for editing a user role.
*
* @throws PermissionsException
*/
public function edit(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
$this->setPageTitle(trans('settings.role_edit'));

View File

@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
class SearchController extends Controller
{
protected $searchRunner;
protected SearchRunner $searchRunner;
public function __construct(SearchRunner $searchRunner)
{
@@ -69,7 +69,7 @@ class SearchController extends Controller
* Search for a list of entities and return a partial HTML response of matching entities.
* Returns the most popular entities if no search is provided.
*/
public function searchEntitiesAjax(Request $request)
public function searchForSelector(Request $request)
{
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false);
@@ -83,7 +83,25 @@ class SearchController extends Controller
$entities = (new Popular())->run(20, 0, $entityTypes);
}
return view('search.parts.entity-ajax-list', ['entities' => $entities, 'permission' => $permission]);
return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities
* to be used as a result preview suggestion list for global system searches.
*/
public function searchSuggestions(Request $request)
{
$searchTerm = $request->get('term', '');
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
foreach ($entities as $entity) {
$entity->setAttribute('preview_content', '');
}
return view('search.parts.entity-suggestion-list', [
'entities' => $entities->slice(0, 5)
]);
}
/**

View File

@@ -3,6 +3,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\TagRepo;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
class TagController extends Controller
@@ -19,22 +20,25 @@ class TagController extends Controller
*/
public function index(Request $request)
{
$search = $request->get('search', '');
$listOptions = SimpleListOptions::fromRequest($request, 'tags')->withSortOptions([
'name' => trans('common.sort_name'),
'usages' => trans('entities.tags_usages'),
]);
$nameFilter = $request->get('name', '');
$tags = $this->tagRepo
->queryWithTotals($search, $nameFilter)
->queryWithTotals($listOptions, $nameFilter)
->paginate(50)
->appends(array_filter([
'search' => $search,
->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
'name' => $nameFilter,
]));
])));
$this->setPageTitle(trans('entities.tags'));
return view('tags.index', [
'tags' => $tags,
'search' => $search,
'nameFilter' => $nameFilter,
'tags' => $tags,
'nameFilter' => $nameFilter,
'listOptions' => $listOptions,
]);
}

View File

@@ -3,13 +3,13 @@
namespace BookStack\Http\Controllers;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\Queries\AllUsersPaginatedAndSorted;
use BookStack\Auth\Queries\UsersAllPaginatedAndSorted;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use BookStack\Util\SimpleListOptions;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
@@ -21,9 +21,6 @@ class UserController extends Controller
protected UserRepo $userRepo;
protected ImageRepo $imageRepo;
/**
* UserController constructor.
*/
public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
{
$this->userRepo = $userRepo;
@@ -36,20 +33,23 @@ class UserController extends Controller
public function index(Request $request)
{
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->get('order', 'asc'),
'search' => $request->get('search', ''),
'sort' => $request->get('sort', 'name'),
];
$users = (new AllUsersPaginatedAndSorted())->run(20, $listDetails);
$listOptions = SimpleListOptions::fromRequest($request, 'users')->withSortOptions([
'name' => trans('common.sort_name'),
'email' => trans('auth.email'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
'last_activity_at' => trans('settings.users_latest_activity'),
]);
$users = (new UsersAllPaginatedAndSorted())->run(20, $listOptions);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
$users->appends($listOptions->getPaginationAppends());
return view('users.index', [
'users' => $users,
'listDetails' => $listDetails,
'listOptions' => $listOptions,
]);
}
@@ -107,9 +107,8 @@ class UserController extends Controller
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
/** @var User $user */
$user = User::query()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
$user = $this->userRepo->getById($id);
$user->load(['apiTokens', 'mfaValues']);
$authMethod = ($user->system_name) ? 'system' : config('auth.method');
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
@@ -202,137 +201,4 @@ class UserController extends Controller
return redirect('/settings/users');
}
/**
* Update the user's preferred book-list display setting.
*/
public function switchBooksView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'books');
}
/**
* Update the user's preferred shelf-list display setting.
*/
public function switchShelvesView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* Update the user's preferred shelf-view book list display setting.
*/
public function switchShelfView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelf');
}
/**
* For a type of list, switch with stored view type for a user.
*/
protected function switchViewType(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
return redirect()->back(302, [], "/settings/users/$userId");
}
/**
* Change the stored sort type for a particular view.
*/
public function changeSort(Request $request, string $id, string $type)
{
$validSortTypes = ['books', 'bookshelves', 'shelf_books'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
return $this->changeListSort($id, $request, $type);
}
/**
* Toggle dark mode for the current user.
*/
public function toggleDarkMode()
{
$enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
return redirect()->back();
}
/**
* Update the stored section expansion preference for the given user.
*/
public function updateExpansionPreference(Request $request, string $id, string $key)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
return response('Invalid key', 500);
}
$newState = $request->get('expand', 'false');
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
return response('', 204);
}
public function updateCodeLanguageFavourite(Request $request)
{
$validated = $this->validate($request, [
'language' => ['required', 'string', 'max:20'],
'active' => ['required', 'bool'],
]);
$currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
$currentFavorites = array_filter(explode(',', $currentFavoritesStr));
$isFav = in_array($validated['language'], $currentFavorites);
if (!$isFav && $validated['active']) {
$currentFavorites[] = $validated['language'];
} elseif ($isFav && !$validated['active']) {
$index = array_search($validated['language'], $currentFavorites);
array_splice($currentFavorites, $index, 1);
}
setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
}
/**
* Changed the stored preference for a list sort order.
*/
protected function changeListSort(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$sort = $request->get('sort');
if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default'])) {
$sort = 'name';
}
$order = $request->get('order');
if (!in_array($order, ['asc', 'desc'])) {
$order = 'asc';
}
$user = $this->userRepo->getById($userId);
$sortKey = $listName . '_sort';
$orderKey = $listName . '_sort_order';
setting()->putUser($user, $sortKey, $sort);
setting()->putUser($user, $orderKey, $order);
return redirect()->back(302, [], "/settings/users/$userId");
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Auth\UserRepo;
use BookStack\Settings\UserShortcutMap;
use Illuminate\Http\Request;
class UserPreferencesController extends Controller
{
protected UserRepo $userRepo;
public function __construct(UserRepo $userRepo)
{
$this->userRepo = $userRepo;
}
/**
* Show the user-specific interface shortcuts.
*/
public function showShortcuts()
{
$shortcuts = UserShortcutMap::fromUserPreferences();
$enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
return view('users.preferences.shortcuts', [
'shortcuts' => $shortcuts,
'enabled' => $enabled,
]);
}
/**
* Update the user-specific interface shortcuts.
*/
public function updateShortcuts(Request $request)
{
$enabled = $request->get('enabled') === 'true';
$providedShortcuts = $request->get('shortcut', []);
$shortcuts = new UserShortcutMap($providedShortcuts);
setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);
$this->showSuccessNotification(trans('preferences.shortcuts_update_success'));
return redirect('/preferences/shortcuts');
}
/**
* Update the preferred view format for a list view of the given type.
*/
public function changeView(Request $request, string $type)
{
$valueViewTypes = ['books', 'bookshelves', 'bookshelf'];
if (!in_array($type, $valueViewTypes)) {
return redirect()->back(500);
}
$view = $request->get('view');
if (!in_array($view, ['grid', 'list'])) {
$view = 'list';
}
$key = $type . '_view_type';
setting()->putForCurrentUser($key, $view);
return redirect()->back(302, [], "/");
}
/**
* Change the stored sort type for a particular view.
*/
public function changeSort(Request $request, string $type)
{
$validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
$sort = substr($request->get('sort') ?: 'name', 0, 50);
$order = $request->get('order') === 'desc' ? 'desc' : 'asc';
$sortKey = $type . '_sort';
$orderKey = $type . '_sort_order';
setting()->putForCurrentUser($sortKey, $sort);
setting()->putForCurrentUser($orderKey, $order);
return redirect()->back(302, [], "/");
}
/**
* Toggle dark mode for the current user.
*/
public function toggleDarkMode()
{
$enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
setting()->putForCurrentUser('dark-mode-enabled', $enabled ? 'false' : 'true');
return redirect()->back();
}
/**
* Update the stored section expansion preference for the given user.
*/
public function changeExpansion(Request $request, string $type)
{
$typeWhitelist = ['home-details'];
if (!in_array($type, $typeWhitelist)) {
return response('Invalid key', 500);
}
$newState = $request->get('expand', 'false');
setting()->putForCurrentUser('section_expansion#' . $type, $newState);
return response('', 204);
}
/**
* Update the favorite status for a code language.
*/
public function updateCodeLanguageFavourite(Request $request)
{
$validated = $this->validate($request, [
'language' => ['required', 'string', 'max:20'],
'active' => ['required', 'bool'],
]);
$currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
$currentFavorites = array_filter(explode(',', $currentFavoritesStr));
$isFav = in_array($validated['language'], $currentFavorites);
if (!$isFav && $validated['active']) {
$currentFavorites[] = $validated['language'];
} elseif ($isFav && !$validated['active']) {
$index = array_search($validated['language'], $currentFavorites);
array_splice($currentFavorites, $index, 1);
}
setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
return response('', 204);
}
}

View File

@@ -3,7 +3,9 @@
namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Actions\Queries\WebhooksAllPaginatedAndSorted;
use BookStack\Actions\Webhook;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
class WebhookController extends Controller
@@ -18,16 +20,25 @@ class WebhookController extends Controller
/**
* Show all webhooks configured in the system.
*/
public function index()
public function index(Request $request)
{
$webhooks = Webhook::query()
->orderBy('name', 'desc')
->with('trackedEvents')
->get();
$listOptions = SimpleListOptions::fromRequest($request, 'webhooks')->withSortOptions([
'name' => trans('common.sort_name'),
'endpoint' => trans('settings.webhooks_endpoint'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
'active' => trans('common.status'),
]);
$webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listOptions);
$webhooks->appends($listOptions->getPaginationAppends());
$this->setPageTitle(trans('settings.webhooks'));
return view('settings.webhooks.index', ['webhooks' => $webhooks]);
return view('settings.webhooks.index', [
'webhooks' => $webhooks,
'listOptions' => $listOptions,
]);
}
/**

View File

@@ -19,14 +19,6 @@ class RouteServiceProvider extends ServiceProvider
*/
public const HOME = '/';
/**
* This namespace is applied to the controller routes in your routes file.
*
* In addition, it is set as the URL generator's root namespace.
*
* @var string
*/
/**
* Define your route model bindings, pattern filters, etc.
*

View File

@@ -50,7 +50,7 @@ class SearchRunner
* The provided count is for each entity to search,
* Total returned could be larger and not guaranteed.
*
* @return array{total: int, count: int, has_more: bool, results: Entity[]}
* @return array{total: int, count: int, has_more: bool, results: Collection<Entity>}
*/
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array
{

View File

@@ -194,6 +194,8 @@ class SettingService
/**
* Put a user-specific setting into the database.
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/
public function putUser(User $user, string $key, string $value): bool
{
@@ -206,6 +208,16 @@ class SettingService
return $this->put($this->userKey($user->id, $key), $value);
}
/**
* Put a user-specific setting into the database for the current access user.
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/
public function putForCurrentUser(string $key, string $value)
{
return $this->putUser(user(), $key, $value);
}
/**
* Convert a setting key into a user-specific key.
*/

View File

@@ -0,0 +1,82 @@
<?php
namespace BookStack\Settings;
class UserShortcutMap
{
protected const DEFAULTS = [
// Header actions
"home_view" => "1",
"shelves_view" => "2",
"books_view" => "3",
"settings_view" => "4",
"favourites_view" => "5",
"profile_view" => "6",
"global_search" => "/",
"logout" => "0",
// Common actions
"edit" => "e",
"new" => "n",
"copy" => "c",
"delete" => "d",
"favourite" => "f",
"export" => "x",
"sort" => "s",
"permissions" => "p",
"move" => "m",
"revisions" => "r",
// Navigation
"next" => "ArrowRight",
"previous" => "ArrowLeft",
];
/**
* @var array<string, string>
*/
protected array $mapping;
public function __construct(array $map)
{
$this->mapping = static::DEFAULTS;
$this->merge($map);
}
/**
* Merge the given map into the current shortcut mapping.
*/
protected function merge(array $map): void
{
foreach ($map as $key => $value) {
if (is_string($value) && isset($this->mapping[$key])) {
$this->mapping[$key] = $value;
}
}
}
/**
* Get the shortcut defined for the given ID.
*/
public function getShortcut(string $id): string
{
return $this->mapping[$id] ?? '';
}
/**
* Convert this mapping to JSON.
*/
public function toJson(): string
{
return json_encode($this->mapping);
}
/**
* Create a new instance from the current user's preferences.
*/
public static function fromUserPreferences(): self
{
$userKeyMap = setting()->getForCurrentUser('ui-shortcuts');
return new self(json_decode($userKeyMap, true) ?: []);
}
}

View File

@@ -88,16 +88,17 @@ class ImageService
protected function getStorageDiskName(string $imageType): string
{
$storageType = config('filesystems.images');
$localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');
// Ensure system images (App logo) are uploaded to a public space
if ($imageType === 'system' && $storageType === 'local_secure') {
$storageType = 'local';
if ($imageType === 'system' && $localSecureInUse) {
return 'local';
}
// Rename local_secure options to get our image specific storage driver which
// is scoped to the relevant image directories.
if ($storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
$storageType = 'local_secure_images';
if ($localSecureInUse) {
return 'local_secure_images';
}
return $storageType;

View File

@@ -28,6 +28,7 @@ class LanguageManager
'de' => ['iso' => 'de_DE', 'windows' => 'German'],
'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'],
'en' => ['iso' => 'en_GB', 'windows' => 'English'],
'el' => ['iso' => 'el_GR', 'windows' => 'Greek'],
'es' => ['iso' => 'es_ES', 'windows' => 'Spanish'],
'es_AR' => ['iso' => 'es_AR', 'windows' => 'Spanish'],
'et' => ['iso' => 'et_EE', 'windows' => 'Estonian'],

View File

@@ -0,0 +1,104 @@
<?php
namespace BookStack\Util;
use Illuminate\Http\Request;
/**
* Handled options commonly used for item lists within the system, providing a standard
* model for handling and validating sort, order and search options.
*/
class SimpleListOptions
{
protected string $typeKey;
protected string $sort;
protected string $order;
protected string $search;
protected array $sortOptions = [];
public function __construct(string $typeKey, string $sort, string $order, string $search = '')
{
$this->typeKey = $typeKey;
$this->sort = $sort;
$this->order = $order;
$this->search = $search;
}
/**
* Create a new instance from the given request.
* Takes the item type (plural) that's used as a key for storing sort preferences.
*/
public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self
{
$search = $request->get('search', '');
$sort = setting()->getForCurrentUser($typeKey . '_sort', '');
$order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
return new self($typeKey, $sort, $order, $search);
}
/**
* Configure the valid sort options for this set of list options.
* Provided sort options must be an array, keyed by search properties
* with values being user-visible option labels.
* Returns current options for easy fluent usage during creation.
*/
public function withSortOptions(array $sortOptions): self
{
$this->sortOptions = array_merge($this->sortOptions, $sortOptions);
return $this;
}
/**
* Get the current order option.
*/
public function getOrder(): string
{
return strtolower($this->order) === 'desc' ? 'desc' : 'asc';
}
/**
* Get the current sort option.
*/
public function getSort(): string
{
$default = array_key_first($this->sortOptions) ?? 'name';
$sort = $this->sort ?: $default;
if (empty($this->sortOptions) || array_key_exists($sort, $this->sortOptions)) {
return $sort;
}
return $default;
}
/**
* Get the set search term.
*/
public function getSearch(): string
{
return $this->search;
}
/**
* Get the data to append for pagination.
*/
public function getPaginationAppends(): array
{
return ['search' => $this->search];
}
/**
* Get the data required by the sort control view.
*/
public function getSortControlData(): array
{
return [
'options' => $this->sortOptions,
'order' => $this->getOrder(),
'sort' => $this->getSort(),
'type' => $this->typeKey,
];
}
}

470
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ class Dropdown {
All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.
Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file. You'll need to import the component class then add it to `componentMapping` object, following the pattern of other components.
Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file by defining an additional export, following the pattern of other components.
### Using a Component in HTML
@@ -80,9 +80,9 @@ Will result with `this.$opts` being:
}
```
#### Component Properties
#### Component Properties & Methods
A component has the below shown properties available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
A component has the below shown properties & methods available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
```javascript
// The root element that the compontent has been applied to.
@@ -98,6 +98,15 @@ this.$manyRefs
// Options defined for the compontent.
this.$opts
// The registered name of the component, usually kebab-case.
this.$name
// Emit a custom event from this component.
// Will be bubbled up from the dom element this is registered on,
// as a custom event with the name `<elementName>-<eventName>`,
// with the provided data in the event detail.
this.$emit(eventName, data = {})
```
## Global JavaScript Helpers
@@ -132,7 +141,16 @@ window.trans_plural(translationString, count, replacements);
// Component System
// Parse and initialise any components from the given root el down.
window.components.init(rootEl);
// Get the first active component of the given name
window.components.first(name);
window.$components.init(rootEl);
// Register component models to be used by the component system.
// Takes a mapping of classes/constructors keyed by component names.
// Names will be converted to kebab-case.
window.$components.register(mapping);
// Get the first active component of the given name.
window.$components.first(name);
// Get all the active components of the given name.
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);
```

1116
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,11 @@
},
"devDependencies": {
"chokidar-cli": "^3.0",
"esbuild": "0.14.42",
"esbuild": "^0.15.12",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.52.1"
"sass": "^1.55.0"
},
"dependencies": {
"clipboard": "^2.0.11",

62
public/dist/app.js vendored Normal file

File diff suppressed because one or more lines are too long

32
public/dist/code.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/dist/export-styles.css vendored Normal file

File diff suppressed because one or more lines are too long

1
public/dist/print-styles.css vendored Normal file
View File

@@ -0,0 +1 @@
:root{--color-primary: #206ea7;--color-primary-light: rgba(32,110,167,0.15);--color-page: #206ea7;--color-page-draft: #7e50b1;--color-chapter: #af4d0d;--color-book: #077b70;--color-bookshelf: #a94747;--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E")}:root.dark-mode{--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");color-scheme:only dark}:root:not(.dark-mode){color-scheme:only light}header{display:none}html,body{font-size:12px;background-color:#fff}.page-content{margin:0 auto}.print-hidden{display:none !important}.tri-layout-container{grid-template-columns:1fr;grid-template-areas:"b";margin-inline-start:0;margin-inline-end:0;display:block}.card{box-shadow:none}.content-wrap.card{padding-inline-start:0;padding-inline-end:0}/*# sourceMappingURL=print-styles.css.map */

1
public/dist/styles.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>

Before

Width:  |  Height:  |  Size: 246 B

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm8 7H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1s-.45 1-1 1zm1-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@@ -27,5 +27,8 @@ window.trans_choice = translator.getPlural.bind(translator);
window.trans_plural = translator.parsePlural.bind(translator);
// Load Components
import components from "./components"
components();
import * as components from "./services/components"
import * as componentMap from "./components";
components.register(componentMap);
window.$components = components;
components.init();

View File

@@ -4,6 +4,7 @@ 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';
@@ -27,6 +28,7 @@ import 'codemirror/mode/rust/rust';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/sql/sql';
import 'codemirror/mode/stex/stex';
import 'codemirror/mode/swift/swift';
import 'codemirror/mode/toml/toml';
import 'codemirror/mode/vb/vb';
import 'codemirror/mode/vbscript/vbscript';
@@ -49,6 +51,7 @@ const modeMap = {
'c++': 'text/x-c++src',
'c#': 'text/x-csharp',
csharp: 'text/x-csharp',
dart: 'application/dart',
diff: 'diff',
for: 'fortran',
fortran: 'fortran',
@@ -91,11 +94,12 @@ const modeMap = {
rs: 'rust',
shell: 'shell',
sh: 'shell',
sql: 'text/x-sql',
stext: 'text/x-stex',
swift: 'text/x-swift',
toml: 'toml',
ts: 'text/typescript',
typescript: 'text/typescript',
sql: 'text/x-sql',
vbs: 'vbscript',
vbscript: 'vbscript',
'vb.net': 'text/x-vb',

View File

@@ -1,13 +1,13 @@
import {onChildEvent} from "../services/dom";
import {uniqueId} from "../services/util";
import {Component} from "./component";
/**
* AddRemoveRows
* Allows easy row add/remove controls onto a table.
* Needs a model row to use when adding a new row.
* @extends {Component}
*/
class AddRemoveRows {
export class AddRemoveRows extends Component {
setup() {
this.modelRow = this.$refs.model;
this.addButton = this.$refs.add;
@@ -31,7 +31,7 @@ class AddRemoveRows {
clone.classList.remove('hidden');
this.setClonedInputNames(clone);
this.modelRow.parentNode.insertBefore(clone, this.modelRow);
window.components.init(clone);
window.$components.init(clone);
}
/**
@@ -49,6 +49,4 @@ class AddRemoveRows {
elem.name = elem.name.split('randrowid').join(rowId);
}
}
}
export default AddRemoveRows;
}

View File

@@ -1,10 +1,7 @@
/**
* AjaxDelete
* @extends {Component}
*/
import {onSelect} from "../services/dom";
import {Component} from "./component";
class AjaxDeleteRow {
export class AjaxDeleteRow extends Component {
setup() {
this.row = this.$el;
this.url = this.$opts.url;
@@ -27,6 +24,4 @@ class AjaxDeleteRow {
this.row.style.pointerEvents = null;
});
}
}
export default AjaxDeleteRow;
}

View File

@@ -1,4 +1,5 @@
import {onEnterPress, onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Ajax Form
@@ -8,10 +9,8 @@ import {onEnterPress, onSelect} from "../services/dom";
*
* Will handle a real form if that's what the component is added to
* otherwise will act as a fake form element.
*
* @extends {Component}
*/
class AjaxForm {
export class AjaxForm extends Component {
setup() {
this.container = this.$el;
this.responseContainer = this.container;
@@ -72,11 +71,9 @@ class AjaxForm {
this.responseContainer.innerHTML = err.data;
}
window.components.init(this.responseContainer);
window.$components.init(this.responseContainer);
this.responseContainer.style.opacity = null;
this.responseContainer.style.pointerEvents = null;
}
}
export default AjaxForm;
}

View File

@@ -1,10 +1,11 @@
import {Component} from "./component";
/**
* Attachments List
* Adds '?open=true' query to file attachment links
* when ctrl/cmd is pressed down.
* @extends {Component}
*/
class AttachmentsList {
export class AttachmentsList extends Component {
setup() {
this.container = this.$el;
@@ -42,6 +43,4 @@ class AttachmentsList {
link.removeAttribute('target');
}
}
}
export default AttachmentsList;
}

View File

@@ -1,10 +1,7 @@
/**
* Attachments
* @extends {Component}
*/
import {showLoading} from "../services/dom";
import {Component} from "./component";
class Attachments {
export class Attachments extends Component {
setup() {
this.container = this.$el;
@@ -46,10 +43,12 @@ class Attachments {
reloadList() {
this.stopEdit();
this.mainTabs.components.tabs.show('items');
/** @var {Tabs} */
const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
tabs.show('items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.components.init(this.list);
window.$components.init(this.list);
});
}
@@ -66,7 +65,7 @@ class Attachments {
showLoading(this.editContainer);
const resp = await window.$http.get(`/attachments/edit/${id}`);
this.editContainer.innerHTML = resp.data;
window.components.init(this.editContainer);
window.$components.init(this.editContainer);
}
stopEdit() {
@@ -74,6 +73,4 @@ class Attachments {
this.listContainer.classList.remove('hidden');
}
}
export default Attachments;
}

View File

@@ -0,0 +1,11 @@
import {Component} from "./component";
export class AutoSubmit extends Component {
setup() {
this.form = this.$el;
this.form.submit();
}
}

View File

@@ -1,13 +1,13 @@
import {escapeHtml} from "../services/util";
import {onChildEvent} from "../services/dom";
import {Component} from "./component";
const ajaxCache = {};
/**
* AutoSuggest
* @extends {Component}
*/
class AutoSuggest {
export class AutoSuggest extends Component {
setup() {
this.parent = this.$el.parentElement;
this.container = this.$el;
@@ -148,6 +148,4 @@ class AutoSuggest {
this.hideSuggestions();
}
}
}
export default AutoSuggest;
}

View File

@@ -1,34 +1,35 @@
import {Component} from "./component";
class BackToTop {
export class BackToTop extends Component {
constructor(elem) {
this.elem = elem;
setup() {
this.button = this.$el;
this.targetElem = document.getElementById('header');
this.showing = false;
this.breakPoint = 1200;
if (document.body.classList.contains('flexbox')) {
this.elem.style.display = 'none';
this.button.style.display = 'none';
return;
}
this.elem.addEventListener('click', this.scrollToTop.bind(this));
this.button.addEventListener('click', this.scrollToTop.bind(this));
window.addEventListener('scroll', this.onPageScroll.bind(this));
}
onPageScroll() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!this.showing && scrollTopPos > this.breakPoint) {
this.elem.style.display = 'block';
this.button.style.display = 'block';
this.showing = true;
setTimeout(() => {
this.elem.style.opacity = 0.4;
this.button.style.opacity = 0.4;
}, 1);
} else if (this.showing && scrollTopPos < this.breakPoint) {
this.elem.style.opacity = 0;
this.button.style.opacity = 0;
this.showing = false;
setTimeout(() => {
this.elem.style.display = 'none';
this.button.style.display = 'none';
}, 500);
}
}
@@ -54,6 +55,4 @@ class BackToTop {
requestAnimationFrame(setPos.bind(this));
}
}
export default BackToTop;
}

View File

@@ -1,4 +1,6 @@
import Sortable from "sortablejs";
import {Component} from "./component";
import {htmlToDom} from "../services/dom";
// Auto sort control
const sortOperations = {
@@ -35,14 +37,14 @@ const sortOperations = {
},
};
class BookSort {
export class BookSort extends Component {
constructor(elem) {
this.elem = elem;
this.sortContainer = elem.querySelector('[book-sort-boxes]');
this.input = elem.querySelector('[book-sort-input]');
setup() {
this.container = this.$el;
this.sortContainer = this.$refs.sortContainer;
this.input = this.$refs.input;
const initialSortBox = elem.querySelector('.sort-box');
const initialSortBox = this.container.querySelector('.sort-box');
this.setupBookSortable(initialSortBox);
this.setupSortPresets();
@@ -90,14 +92,12 @@ class BookSort {
* @param {Object} entityInfo
*/
bookSelect(entityInfo) {
const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
if (alreadyAdded) return;
const entitySortItemUrl = entityInfo.link + '/sort-item';
window.$http.get(entitySortItemUrl).then(resp => {
const wrap = document.createElement('div');
wrap.innerHTML = resp.data;
const newBookContainer = wrap.children[0];
const newBookContainer = htmlToDom(resp.data);
this.sortContainer.append(newBookContainer);
this.setupBookSortable(newBookContainer);
});
@@ -155,7 +155,7 @@ class BookSort {
*/
buildEntityMap() {
const entityMap = [];
const lists = this.elem.querySelectorAll('.sort-list');
const lists = this.container.querySelectorAll('.sort-list');
for (let list of lists) {
const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
@@ -202,6 +202,4 @@ class BookSort {
}
}
}
export default BookSort;
}

View File

@@ -1,9 +1,7 @@
import {slideUp, slideDown} from "../services/animations";
import {Component} from "./component";
/**
* @extends {Component}
*/
class ChapterContents {
export class ChapterContents extends Component {
setup() {
this.list = this.$refs.list;
@@ -31,7 +29,4 @@ class ChapterContents {
event.preventDefault();
this.isOpen ? this.close() : this.open();
}
}
export default ChapterContents;

View File

@@ -1,10 +1,8 @@
import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Code Editor
* @extends {Component}
*/
class CodeEditor {
export class CodeEditor extends Component {
setup() {
this.container = this.$refs.container;
@@ -73,7 +71,7 @@ class CodeEditor {
isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
window.$http.patch('/settings/users/update-code-language-favourite', {
window.$http.patch('/preferences/update-code-language-favourite', {
language: language,
active: isFavorite
});
@@ -128,7 +126,7 @@ class CodeEditor {
}
this.loadHistory();
this.popup.components.popup.show(() => {
this.getPopup().show(() => {
Code.updateLayout(this.editor);
this.editor.focus();
}, () => {
@@ -137,10 +135,17 @@ class CodeEditor {
}
hide() {
this.popup.components.popup.hide();
this.getPopup().hide();
this.addHistory();
}
/**
* @returns {Popup}
*/
getPopup() {
return window.$components.firstOnElement(this.popup, 'popup');
}
async updateEditorMode(language) {
const Code = await window.importVersioned('code');
Code.setMode(this.editor, language, this.editor.getValue());
@@ -184,6 +189,4 @@ class CodeEditor {
window.sessionStorage.setItem(this.historyKey, historyString);
}
}
export default CodeEditor;
}

View File

@@ -1,14 +1,16 @@
class CodeHighlighter {
import {Component} from "./component";
constructor(elem) {
const codeBlocks = elem.querySelectorAll('pre');
export class CodeHighlighter extends Component{
setup() {
const container = this.$el;
const codeBlocks = container.querySelectorAll('pre');
if (codeBlocks.length > 0) {
window.importVersioned('code').then(Code => {
Code.highlightWithin(elem);
Code.highlightWithin(container);
});
}
}
}
export default CodeHighlighter;
}

View File

@@ -1,9 +1,10 @@
/**
* A simple component to render a code editor within the textarea
* this exists upon.
* @extends {Component}
*/
class CodeTextarea {
import {Component} from "./component";
export class CodeTextarea extends Component {
async setup() {
const mode = this.$opts.mode;
@@ -11,6 +12,4 @@ class CodeTextarea {
Code.inlineEditor(this.$el, mode);
}
}
export default CodeTextarea;
}

View File

@@ -1,35 +1,37 @@
import {slideDown, slideUp} from "../services/animations";
import {Component} from "./component";
/**
* Collapsible
* Provides some simple logic to allow collapsible sections.
*/
class Collapsible {
export class Collapsible extends Component {
constructor(elem) {
this.elem = elem;
this.trigger = elem.querySelector('[collapsible-trigger]');
this.content = elem.querySelector('[collapsible-content]');
setup() {
this.container = this.$el;
this.trigger = this.$refs.trigger;
this.content = this.$refs.content;
if (!this.trigger) return;
this.trigger.addEventListener('click', this.toggle.bind(this));
this.openIfContainsError();
if (this.trigger) {
this.trigger.addEventListener('click', this.toggle.bind(this));
this.openIfContainsError();
}
}
open() {
this.elem.classList.add('open');
this.container.classList.add('open');
this.trigger.setAttribute('aria-expanded', 'true');
slideDown(this.content, 300);
}
close() {
this.elem.classList.remove('open');
this.container.classList.remove('open');
this.trigger.setAttribute('aria-expanded', 'false');
slideUp(this.content, 300);
}
toggle() {
if (this.elem.classList.contains('open')) {
if (this.container.classList.contains('open')) {
this.close();
} else {
this.open();
@@ -43,6 +45,4 @@ class Collapsible {
}
}
}
export default Collapsible;
}

View File

@@ -0,0 +1,58 @@
export class Component {
/**
* The registered name of the component.
* @type {string}
*/
$name = '';
/**
* The element that the component is registered upon.
* @type {Element}
*/
$el = null;
/**
* Mapping of referenced elements within the component.
* @type {Object<string, Element>}
*/
$refs = {};
/**
* Mapping of arrays of referenced elements within the component so multiple
* references, sharing the same name, can be fetched.
* @type {Object<string, Element[]>}
*/
$manyRefs = {};
/**
* Options passed into this component.
* @type {Object<String, String>}
*/
$opts = {};
/**
* Component-specific setup methods.
* Use this to assign local variables and run any initial setup or actions.
*/
setup() {
//
}
/**
* Emit an event from this component.
* Will be bubbled up from the dom element this is registered on, as a custom event
* with the name `<elementName>-<eventName>`, with the provided data in the event detail.
* @param {String} eventName
* @param {Object} data
*/
$emit(eventName, data = {}) {
data.from = this;
const componentName = this.$name;
const event = new CustomEvent(`${componentName}-${eventName}`, {
bubbles: true,
detail: data
});
this.$el.dispatchEvent(event);
}
}

View File

@@ -1,12 +1,12 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Custom equivalent of window.confirm() using our popup component.
* Is promise based so can be used like so:
* `const result = await dialog.show()`
* @extends {Component}
*/
class ConfirmDialog {
export class ConfirmDialog extends Component {
setup() {
this.container = this.$el;
@@ -34,7 +34,7 @@ class ConfirmDialog {
* @returns {Popup}
*/
getPopup() {
return this.container.components.popup;
return window.$components.firstOnElement(this.container, 'popup');
}
/**
@@ -47,6 +47,4 @@ class ConfirmDialog {
}
}
}
export default ConfirmDialog;
}

View File

@@ -1,18 +1,19 @@
import {Component} from "./component";
class CustomCheckbox {
export class CustomCheckbox extends Component {
constructor(elem) {
this.elem = elem;
this.checkbox = elem.querySelector('input[type=checkbox]');
this.display = elem.querySelector('[role="checkbox"]');
setup() {
this.container = this.$el;
this.checkbox = this.container.querySelector('input[type=checkbox]');
this.display = this.container.querySelector('[role="checkbox"]');
this.checkbox.addEventListener('change', this.stateChange.bind(this));
this.elem.addEventListener('keydown', this.onKeyDown.bind(this));
this.container.addEventListener('keydown', this.onKeyDown.bind(this));
}
onKeyDown(event) {
const isEnterOrPress = event.keyCode === 32 || event.keyCode === 13;
if (isEnterOrPress) {
const isEnterOrSpace = event.key === ' ' || event.key === 'Enter';
if (isEnterOrSpace) {
event.preventDefault();
this.toggle();
}
@@ -29,6 +30,4 @@ class CustomCheckbox {
this.display.setAttribute('aria-checked', checked);
}
}
export default CustomCheckbox;
}

View File

@@ -1,21 +1,22 @@
class DetailsHighlighter {
import {Component} from "./component";
constructor(elem) {
this.elem = elem;
export class DetailsHighlighter extends Component {
setup() {
this.container = this.$el;
this.dealtWith = false;
elem.addEventListener('toggle', this.onToggle.bind(this));
this.container.addEventListener('toggle', this.onToggle.bind(this));
}
onToggle() {
if (this.dealtWith) return;
if (this.elem.querySelector('pre')) {
if (this.container.querySelector('pre')) {
window.importVersioned('code').then(Code => {
Code.highlightWithin(this.elem);
Code.highlightWithin(this.container);
});
}
this.dealtWith = true;
}
}
export default DetailsHighlighter;
}

View File

@@ -1,7 +1,8 @@
import {debounce} from "../services/util";
import {transitionHeight} from "../services/animations";
import {Component} from "./component";
class DropdownSearch {
export class DropdownSearch extends Component {
setup() {
this.elem = this.$el;
@@ -78,6 +79,4 @@ class DropdownSearch {
this.loadingElem.style.display = show ? 'block' : 'none';
}
}
export default DropdownSearch;
}

View File

@@ -1,11 +1,12 @@
import {onSelect} from "../services/dom";
import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
import {Component} from "./component";
/**
* Dropdown
* Provides some simple logic to create simple dropdown menus.
* @extends {Component}
*/
class DropDown {
export class Dropdown extends Component {
setup() {
this.container = this.$el;
@@ -17,8 +18,9 @@ class DropDown {
this.direction = (document.dir === 'rtl') ? 'right' : 'left';
this.body = document.body;
this.showing = false;
this.setupListeners();
this.hide = this.hide.bind(this);
this.setupListeners();
}
show(event = null) {
@@ -52,7 +54,7 @@ class DropDown {
}
// Set listener to hide on mouse leave or window click
this.menu.addEventListener('mouseleave', this.hide.bind(this));
this.menu.addEventListener('mouseleave', this.hide);
window.addEventListener('click', event => {
if (!this.menu.contains(event.target)) {
this.hide();
@@ -74,7 +76,7 @@ class DropDown {
}
hideAll() {
for (let dropdown of window.components.dropdown) {
for (let dropdown of window.$components.get('dropdown')) {
dropdown.hide();
}
}
@@ -97,33 +99,25 @@ class DropDown {
this.showing = false;
}
getFocusable() {
return Array.from(this.menu.querySelectorAll('[tabindex]:not([tabindex="-1"]),[href],button,input:not([type=hidden])'));
}
focusNext() {
const focusable = this.getFocusable();
const currentIndex = focusable.indexOf(document.activeElement);
let newIndex = currentIndex + 1;
if (newIndex >= focusable.length) {
newIndex = 0;
}
focusable[newIndex].focus();
}
focusPrevious() {
const focusable = this.getFocusable();
const currentIndex = focusable.indexOf(document.activeElement);
let newIndex = currentIndex - 1;
if (newIndex < 0) {
newIndex = focusable.length - 1;
}
focusable[newIndex].focus();
}
setupListeners() {
const keyboardNavHandler = new KeyboardNavigationHandler(this.container, (event) => {
this.hide();
this.toggle.focus();
if (!this.bubbleEscapes) {
event.stopPropagation();
}
}, (event) => {
if (event.target.nodeName === 'INPUT') {
event.preventDefault();
event.stopPropagation();
}
this.hide();
});
if (this.moveMenu) {
keyboardNavHandler.shareHandlingToEl(this.menu);
}
// Hide menu on option click
this.container.addEventListener('click', event => {
const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
@@ -136,41 +130,9 @@ class DropDown {
event.stopPropagation();
this.show(event);
if (event instanceof KeyboardEvent) {
this.focusNext();
}
});
// Keyboard navigation
const keyboardNavigation = event => {
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
this.focusNext();
event.preventDefault();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
this.focusPrevious();
event.preventDefault();
} else if (event.key === 'Escape') {
this.hide();
this.toggle.focus();
if (!this.bubbleEscapes) {
event.stopPropagation();
}
}
};
this.container.addEventListener('keydown', keyboardNavigation);
if (this.moveMenu) {
this.menu.addEventListener('keydown', keyboardNavigation);
}
// Hide menu on enter press or escape
this.menu.addEventListener('keydown ', event => {
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
this.hide();
keyboardNavHandler.focusNext();
}
});
}
}
export default DropDown;

View File

@@ -1,11 +1,8 @@
import DropZoneLib from "dropzone";
import {fadeOut} from "../services/animations";
import {Component} from "./component";
/**
* Dropzone
* @extends {Component}
*/
class Dropzone {
export class Dropzone extends Component {
setup() {
this.container = this.$el;
this.url = this.$opts.url;
@@ -73,6 +70,4 @@ class Dropzone {
removeAll() {
this.dz.removeAllFiles(true);
}
}
export default Dropzone;
}

View File

@@ -1,51 +1,58 @@
class EditorToolbox {
import {Component} from "./component";
constructor(elem) {
export class EditorToolbox extends Component {
setup() {
// Elements
this.elem = elem;
this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
this.toggleButton = elem.querySelector('[toolbox-toggle]');
this.container = this.$el;
this.buttons = this.$manyRefs.tabButton;
this.contentElements = this.$manyRefs.tabContent;
this.toggleButton = this.$refs.toggle;
// Toolbox toggle button click
this.toggleButton.addEventListener('click', this.toggle.bind(this));
// Tab button click
this.elem.addEventListener('click', event => {
let button = event.target.closest('[toolbox-tab-button]');
if (button === null) return;
let name = button.getAttribute('toolbox-tab-button');
this.setActiveTab(name, true);
});
this.setupListeners();
// Set the first tab as active on load
this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
this.setActiveTab(this.contentElements[0].dataset.tabContent);
}
setupListeners() {
// Toolbox toggle button click
this.toggleButton.addEventListener('click', () => this.toggle());
// Tab button click
this.container.addEventListener('click', event => {
const button = event.target.closest('button');
if (this.buttons.includes(button)) {
const name = button.dataset.tab;
this.setActiveTab(name, true);
}
});
}
toggle() {
this.elem.classList.toggle('open');
const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
this.container.classList.toggle('open');
const expanded = this.container.classList.contains('open') ? 'true' : 'false';
this.toggleButton.setAttribute('aria-expanded', expanded);
}
setActiveTab(tabName, openToolbox = false) {
// Set button visibility
for (let i = 0, len = this.buttons.length; i < len; i++) {
this.buttons[i].classList.remove('active');
let bName = this.buttons[i].getAttribute('toolbox-tab-button');
if (bName === tabName) this.buttons[i].classList.add('active');
}
// Set content visibility
for (let i = 0, len = this.contentElements.length; i < len; i++) {
this.contentElements[i].style.display = 'none';
let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
if (cName === tabName) this.contentElements[i].style.display = 'block';
for (const button of this.buttons) {
button.classList.remove('active');
const bName = button.dataset.tab;
if (bName === tabName) button.classList.add('active');
}
if (openToolbox && !this.elem.classList.contains('open')) {
// Set content visibility
for (const contentEl of this.contentElements) {
contentEl.style.display = 'none';
const cName = contentEl.dataset.tabContent;
if (cName === tabName) contentEl.style.display = 'block';
}
if (openToolbox && !this.container.classList.contains('open')) {
this.toggle();
}
}
}
export default EditorToolbox;
}

View File

@@ -1,7 +1,7 @@
/**
* @extends {Component}
*/
class EntityPermissions {
import {htmlToDom} from "../services/dom";
import {Component} from "./component";
export class EntityPermissions extends Component {
setup() {
this.container = this.$el;
@@ -53,17 +53,14 @@ class EntityPermissions {
// Get and insert new row
const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`);
const wrap = document.createElement('div');
wrap.innerHTML = resp.data;
const row = wrap.children[0];
const row = htmlToDom(resp.data);
this.roleContainer.append(row);
window.components.init(row);
this.roleSelect.disabled = false;
}
removeRowOnButtonClick(button) {
const row = button.closest('.content-permissions-row');
const row = button.closest('.item-list-row');
const roleId = button.dataset.roleId;
const roleName = button.dataset.roleName;
@@ -75,6 +72,4 @@ class EntityPermissions {
row.remove();
}
}
export default EntityPermissions;
}

View File

@@ -1,10 +1,7 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Class EntitySearch
* @extends {Component}
*/
class EntitySearch {
export class EntitySearch extends Component {
setup() {
this.entityId = this.$opts.entityId;
this.entityType = this.$opts.entityType;
@@ -54,6 +51,4 @@ class EntitySearch {
this.loadingBlock.classList.add('hidden');
this.searchInput.value = '';
}
}
export default EntitySearch;
}

View File

@@ -1,14 +1,10 @@
/**
* Entity Selector Popup
* @extends {Component}
*/
class EntitySelectorPopup {
import {Component} from "./component";
export class EntitySelectorPopup extends Component {
setup() {
this.elem = this.$el;
this.container = this.$el;
this.selectButton = this.$refs.select;
window.EntitySelectorPopup = this;
this.selectorEl = this.$refs.selector;
this.callback = null;
@@ -21,16 +17,26 @@ class EntitySelectorPopup {
show(callback) {
this.callback = callback;
this.elem.components.popup.show();
this.getPopup().show();
this.getSelector().focusSearch();
}
hide() {
this.elem.components.popup.hide();
this.getPopup().hide();
}
/**
* @returns {Popup}
*/
getPopup() {
return window.$components.firstOnElement(this.container, 'popup');
}
/**
* @returns {EntitySelector}
*/
getSelector() {
return this.selectorEl.components['entity-selector'];
return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
}
onSelectButtonClick() {
@@ -51,6 +57,4 @@ class EntitySelectorPopup {
this.getSelector().reset();
if (this.callback && entity) this.callback(entity);
}
}
export default EntitySelectorPopup;
}

View File

@@ -1,10 +1,10 @@
import {onChildEvent} from "../services/dom";
import {Component} from "./component";
/**
* Entity Selector
* @extends {Component}
*/
class EntitySelector {
export class EntitySelector extends Component {
setup() {
this.elem = this.$el;
@@ -115,7 +115,7 @@ class EntitySelector {
}
searchUrl() {
return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
return `/search/entity-selector?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
}
searchEntities(searchTerm) {
@@ -185,6 +185,4 @@ class EntitySelector {
this.selectedItemData = null;
}
}
export default EntitySelector;
}

View File

@@ -1,4 +1,5 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
/**
* EventEmitSelect
@@ -10,10 +11,8 @@ import {onSelect} from "../services/dom";
*
* All options will be set as the "detail" of the event with
* their values included.
*
* @extends {Component}
*/
class EventEmitSelect {
export class EventEmitSelect extends Component{
setup() {
this.container = this.$el;
this.name = this.$opts.name;
@@ -24,6 +23,4 @@ class EventEmitSelect {
});
}
}
export default EventEmitSelect;
}

View File

@@ -1,17 +1,15 @@
import {slideUp, slideDown} from "../services/animations";
import {Component} from "./component";
class ExpandToggle {
export class ExpandToggle extends Component {
constructor(elem) {
this.elem = elem;
// Component state
this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
this.selector = elem.getAttribute('expand-toggle');
setup(elem) {
this.targetSelector = this.$opts.targetSelector;
this.isOpen = this.$opts.isOpen === 'true';
this.updateEndpoint = this.$opts.updateEndpoint;
// Listener setup
elem.addEventListener('click', this.click.bind(this));
this.$el.addEventListener('click', this.click.bind(this));
}
open(elemToToggle) {
@@ -25,7 +23,7 @@ class ExpandToggle {
click(event) {
event.preventDefault();
const matchingElems = document.querySelectorAll(this.selector);
const matchingElems = document.querySelectorAll(this.targetSelector);
for (let match of matchingElems) {
this.isOpen ? this.close(match) : this.open(match);
}
@@ -40,6 +38,4 @@ class ExpandToggle {
});
}
}
export default ExpandToggle;
}

View File

@@ -0,0 +1,82 @@
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.
* Mainly to show live results preview.
*/
export class GlobalSearch extends Component {
setup() {
this.container = this.$el;
this.input = this.$refs.input;
this.suggestions = this.$refs.suggestions;
this.suggestionResultsWrap = this.$refs.suggestionResults;
this.loadingWrap = this.$refs.loading;
this.button = this.$refs.button;
this.setupListeners();
}
setupListeners() {
const updateSuggestionsDebounced = debounce(this.updateSuggestions.bind(this), 200, false);
// Handle search input changes
this.input.addEventListener('input', () => {
const value = this.input.value;
if (value.length > 0) {
this.loadingWrap.style.display = 'block';
this.suggestionResultsWrap.style.opacity = '0.5';
updateSuggestionsDebounced(value);
} else {
this.hideSuggestions();
}
});
// Allow double click to show auto-click suggestions
this.input.addEventListener('dblclick', () => {
this.input.setAttribute('autocomplete', 'on');
this.button.focus();
this.input.focus();
});
new KeyboardNavigationHandler(this.container, () => {
this.hideSuggestions();
});
}
/**
* @param {String} search
*/
async updateSuggestions(search) {
const {data: results} = await window.$http.get('/search/suggest', {term: search});
if (!this.input.value) {
return;
}
const resultDom = htmlToDom(results);
this.suggestionResultsWrap.innerHTML = '';
this.suggestionResultsWrap.style.opacity = '1';
this.loadingWrap.style.display = 'none';
this.suggestionResultsWrap.append(resultDom);
if (!this.container.classList.contains('search-active')) {
this.showSuggestions();
}
}
showSuggestions() {
this.container.classList.add('search-active');
window.requestAnimationFrame(() => {
this.suggestions.classList.add('search-suggestions-animation');
})
}
hideSuggestions() {
this.container.classList.remove('search-active');
this.suggestions.classList.remove('search-suggestions-animation');
this.suggestionResultsWrap.innerHTML = '';
}
}

View File

@@ -1,5 +1,6 @@
import {Component} from "./component";
class HeaderMobileToggle {
export class HeaderMobileToggle extends Component {
setup() {
this.elem = this.$el;
@@ -36,6 +37,4 @@ class HeaderMobileToggle {
this.onToggle(event);
}
}
export default HeaderMobileToggle;
}

View File

@@ -1,13 +1,9 @@
import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
import {Component} from "./component";
/**
* ImageManager
* @extends {Component}
*/
class ImageManager {
export class ImageManager extends Component {
setup() {
// Options
this.uploadedTo = this.$opts.uploadedTo;
@@ -36,8 +32,6 @@ class ImageManager {
this.resetState();
this.setupListeners();
window.ImageManager = this;
}
setupListeners() {
@@ -100,7 +94,7 @@ class ImageManager {
this.callback = callback;
this.type = type;
this.popupEl.components.popup.show();
this.getPopup().show();
this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
if (!this.hasData) {
@@ -110,7 +104,14 @@ class ImageManager {
}
hide() {
this.popupEl.components.popup.hide();
this.getPopup().hide();
}
/**
* @returns {Popup}
*/
getPopup() {
return window.$components.firstOnElement(this.popupEl, 'popup');
}
async loadGallery() {
@@ -132,7 +133,7 @@ class ImageManager {
addReturnedHtmlElementsToList(html) {
const el = document.createElement('div');
el.innerHTML = html;
window.components.init(el);
window.$components.init(el);
for (const child of [...el.children]) {
this.listContainer.appendChild(child);
}
@@ -207,9 +208,7 @@ class ImageManager {
const params = requestDelete ? {delete: true} : {};
const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
this.formContainer.innerHTML = formHtml;
window.components.init(this.formContainer);
window.$components.init(this.formContainer);
}
}
export default ImageManager;
}

View File

@@ -1,21 +1,25 @@
import {Component} from "./component";
class ImagePicker {
export class ImagePicker extends Component {
constructor(elem) {
this.elem = elem;
this.imageElem = elem.querySelector('img');
this.imageInput = elem.querySelector('input[type=file]');
this.resetInput = elem.querySelector('input[data-reset-input]');
this.removeInput = elem.querySelector('input[data-remove-input]');
setup() {
this.imageElem = this.$refs.image;
this.imageInput = this.$refs.imageInput;
this.resetInput = this.$refs.resetInput;
this.removeInput = this.$refs.removeInput;
this.resetButton = this.$refs.resetButton;
this.removeButton = this.$refs.removeButton || null;
this.defaultImage = elem.getAttribute('data-default-image');
this.defaultImage = this.$opts.defaultImage;
const resetButton = elem.querySelector('button[data-action="reset-image"]');
resetButton.addEventListener('click', this.reset.bind(this));
this.setupListeners();
}
const removeButton = elem.querySelector('button[data-action="remove-image"]');
if (removeButton) {
removeButton.addEventListener('click', this.removeImage.bind(this));
setupListeners() {
this.resetButton.addEventListener('click', this.reset.bind(this));
if (this.removeButton) {
this.removeButton.addEventListener('click', this.removeImage.bind(this));
}
this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
@@ -50,6 +54,4 @@ class ImagePicker {
this.resetInput.setAttribute('disabled', 'disabled');
}
}
export default ImagePicker;
}

View File

@@ -1,274 +1,59 @@
import addRemoveRows from "./add-remove-rows.js"
import ajaxDeleteRow from "./ajax-delete-row.js"
import ajaxForm from "./ajax-form.js"
import attachments from "./attachments.js"
import attachmentsList from "./attachments-list.js"
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
import chapterContents from "./chapter-contents.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import codeTextarea from "./code-textarea.js"
import collapsible from "./collapsible.js"
import confirmDialog from "./confirm-dialog"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
import dropdownSearch from "./dropdown-search.js"
import dropzone from "./dropzone.js"
import editorToolbox from "./editor-toolbox.js"
import entityPermissions from "./entity-permissions";
import entitySearch from "./entity-search.js"
import entitySelector from "./entity-selector.js"
import entitySelectorPopup from "./entity-selector-popup.js"
import eventEmitSelect from "./event-emit-select.js"
import expandToggle from "./expand-toggle.js"
import headerMobileToggle from "./header-mobile-toggle.js"
import homepageControl from "./homepage-control.js"
import imageManager from "./image-manager.js"
import imagePicker from "./image-picker.js"
import listSortControl from "./list-sort-control.js"
import markdownEditor from "./markdown-editor.js"
import newUserPassword from "./new-user-password.js"
import notification from "./notification.js"
import optionalInput from "./optional-input.js"
import pageComments from "./page-comments.js"
import pageDisplay from "./page-display.js"
import pageEditor from "./page-editor.js"
import pagePicker from "./page-picker.js"
import permissionsTable from "./permissions-table.js"
import popup from "./popup.js"
import settingAppColorPicker from "./setting-app-color-picker.js"
import settingColorPicker from "./setting-color-picker.js"
import shelfSort from "./shelf-sort.js"
import sidebar from "./sidebar.js"
import sortableList from "./sortable-list.js"
import submitOnChange from "./submit-on-change.js"
import tabs from "./tabs.js"
import tagManager from "./tag-manager.js"
import templateManager from "./template-manager.js"
import toggleSwitch from "./toggle-switch.js"
import triLayout from "./tri-layout.js"
import userSelect from "./user-select.js"
import webhookEvents from "./webhook-events";
import wysiwygEditor from "./wysiwyg-editor.js"
const componentMapping = {
"add-remove-rows": addRemoveRows,
"ajax-delete-row": ajaxDeleteRow,
"ajax-form": ajaxForm,
"attachments": attachments,
"attachments-list": attachmentsList,
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,
"chapter-contents": chapterContents,
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"code-textarea": codeTextarea,
"collapsible": collapsible,
"confirm-dialog": confirmDialog,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
"dropdown-search": dropdownSearch,
"dropzone": dropzone,
"editor-toolbox": editorToolbox,
"entity-permissions": entityPermissions,
"entity-search": entitySearch,
"entity-selector": entitySelector,
"entity-selector-popup": entitySelectorPopup,
"event-emit-select": eventEmitSelect,
"expand-toggle": expandToggle,
"header-mobile-toggle": headerMobileToggle,
"homepage-control": homepageControl,
"image-manager": imageManager,
"image-picker": imagePicker,
"list-sort-control": listSortControl,
"markdown-editor": markdownEditor,
"new-user-password": newUserPassword,
"notification": notification,
"optional-input": optionalInput,
"page-comments": pageComments,
"page-display": pageDisplay,
"page-editor": pageEditor,
"page-picker": pagePicker,
"permissions-table": permissionsTable,
"popup": popup,
"setting-app-color-picker": settingAppColorPicker,
"setting-color-picker": settingColorPicker,
"shelf-sort": shelfSort,
"sidebar": sidebar,
"sortable-list": sortableList,
"submit-on-change": submitOnChange,
"tabs": tabs,
"tag-manager": tagManager,
"template-manager": templateManager,
"toggle-switch": toggleSwitch,
"tri-layout": triLayout,
"user-select": userSelect,
"webhook-events": webhookEvents,
"wysiwyg-editor": wysiwygEditor,
};
window.components = {};
/**
* Initialize components of the given name within the given element.
* @param {String} componentName
* @param {HTMLElement|Document} parentElement
*/
function searchForComponentInParent(componentName, parentElement) {
const elems = parentElement.querySelectorAll(`[${componentName}]`);
for (let j = 0, jLen = elems.length; j < jLen; j++) {
initComponent(componentName, elems[j]);
}
}
/**
* Initialize a component instance on the given dom element.
* @param {String} name
* @param {Element} element
*/
function initComponent(name, element) {
const componentModel = componentMapping[name];
if (componentModel === undefined) return;
// Create our component instance
let instance;
try {
instance = new componentModel(element);
instance.$el = element;
const allRefs = parseRefs(name, element);
instance.$refs = allRefs.refs;
instance.$manyRefs = allRefs.manyRefs;
instance.$opts = parseOpts(name, element);
instance.$emit = (eventName, data = {}) => {
data.from = instance;
const event = new CustomEvent(`${name}-${eventName}`, {
bubbles: true,
detail: data
});
instance.$el.dispatchEvent(event);
};
if (typeof instance.setup === 'function') {
instance.setup();
}
} catch (e) {
console.error('Failed to create component', e, name, element);
}
// Add to global listing
if (typeof window.components[name] === "undefined") {
window.components[name] = [];
}
window.components[name].push(instance);
// Add to element listing
if (typeof element.components === 'undefined') {
element.components = {};
}
element.components[name] = instance;
}
/**
* Parse out the element references within the given element
* for the given component name.
* @param {String} name
* @param {Element} element
*/
function parseRefs(name, element) {
const refs = {};
const manyRefs = {};
const prefix = `${name}@`
const selector = `[refs*="${prefix}"]`;
const refElems = [...element.querySelectorAll(selector)];
if (element.matches(selector)) {
refElems.push(element);
}
for (const el of refElems) {
const refNames = el.getAttribute('refs')
.split(' ')
.filter(str => str.startsWith(prefix))
.map(str => str.replace(prefix, ''))
.map(kebabToCamel);
for (const ref of refNames) {
refs[ref] = el;
if (typeof manyRefs[ref] === 'undefined') {
manyRefs[ref] = [];
}
manyRefs[ref].push(el);
}
}
return {refs, manyRefs};
}
/**
* Parse out the element component options.
* @param {String} name
* @param {Element} element
* @return {Object<String, String>}
*/
function parseOpts(name, element) {
const opts = {};
const prefix = `option:${name}:`;
for (const {name, value} of element.attributes) {
if (name.startsWith(prefix)) {
const optName = name.replace(prefix, '');
opts[kebabToCamel(optName)] = value || '';
}
}
return opts;
}
/**
* Convert a kebab-case string to camelCase
* @param {String} kebab
* @returns {string}
*/
function kebabToCamel(kebab) {
const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
const words = kebab.split('-');
return words[0] + words.slice(1).map(ucFirst).join('');
}
/**
* Initialize all components found within the given element.
* @param parentElement
*/
function initAll(parentElement) {
if (typeof parentElement === 'undefined') parentElement = document;
// Old attribute system
for (const componentName of Object.keys(componentMapping)) {
searchForComponentInParent(componentName, parentElement);
}
// New component system
const componentElems = parentElement.querySelectorAll(`[component],[components]`);
for (const el of componentElems) {
const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
for (const name of componentNames) {
initComponent(name, el);
}
}
}
window.components.init = initAll;
window.components.first = (name) => (window.components[name] || [null])[0];
export default initAll;
/**
* @typedef Component
* @property {HTMLElement} $el
* @property {Object<String, HTMLElement>} $refs
* @property {Object<String, HTMLElement[]>} $manyRefs
* @property {Object<String, String>} $opts
* @property {function(string, Object)} $emit
*/
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 {SettingAppColorPicker} from "./setting-app-color-picker.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"

View File

@@ -2,16 +2,22 @@
* ListSortControl
* Manages the logic for the control which provides list sorting options.
*/
class ListSortControl {
import {Component} from "./component";
constructor(elem) {
this.elem = elem;
this.menu = elem.querySelector('ul');
export class ListSortControl extends Component {
this.sortInput = elem.querySelector('[name="sort"]');
this.orderInput = elem.querySelector('[name="order"]');
this.form = elem.querySelector('form');
setup() {
this.elem = this.$el;
this.menu = this.$refs.menu;
this.sortInput = this.$refs.sort;
this.orderInput = this.$refs.order;
this.form = this.$refs.form;
this.setupListeners();
}
setupListeners() {
this.menu.addEventListener('click', event => {
if (event.target.closest('[data-sort-value]') !== null) {
this.sortOptionClick(event);
@@ -34,12 +40,9 @@ class ListSortControl {
sortDirectionClick(event) {
const currentDir = this.orderInput.value;
const newDir = (currentDir === 'asc') ? 'desc' : 'asc';
this.orderInput.value = newDir;
this.orderInput.value = (currentDir === 'asc') ? 'desc' : 'asc';
event.preventDefault();
this.form.submit();
}
}
export default ListSortControl;
}

View File

@@ -1,11 +1,8 @@
import MarkdownIt from "markdown-it";
import mdTasksLists from 'markdown-it-task-lists';
import Clipboard from "../services/clipboard";
import {debounce} from "../services/util";
import {patchDomFromHtmlString} from "../services/vdom";
import DrawIO from "../services/drawio";
import {Component} from "./component";
import {init as initEditor} from "../markdown/editor";
class MarkdownEditor {
export class MarkdownEditor extends Component {
setup() {
this.elem = this.$el;
@@ -15,80 +12,59 @@ class MarkdownEditor {
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
this.display = this.$refs.display;
this.input = this.$refs.input;
this.divider = this.$refs.divider;
this.displayWrap = this.$refs.displayWrap;
this.display = this.elem.querySelector('.markdown-display');
const settingContainer = this.$refs.settingContainer;
const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
this.displayStylesLoaded = false;
this.input = this.elem.querySelector('textarea');
this.cm = null;
this.Code = null;
const cmLoadPromise = window.importVersioned('code').then(Code => {
this.cm = Code.markdownEditor(this.input);
this.Code = Code;
return this.cm;
});
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
const displayLoad = () => {
this.displayDoc = this.display.contentDocument;
this.init(cmLoadPromise);
};
if (this.display.contentDocument.readyState === 'complete') {
displayLoad();
} else {
this.display.addEventListener('load', displayLoad.bind(this));
}
window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
markdownIt: this.markdown,
this.editor = null;
initEditor({
pageId: this.pageId,
container: this.elem,
displayEl: this.display,
codeMirrorInstance: this.cm,
inputEl: this.input,
drawioUrl: this.getDrawioUrl(),
settingInputs: Array.from(settingInputs),
text: {
serverUploadLimit: this.serverUploadLimitText,
imageUploadError: this.imageUploadErrorText,
},
}).then(editor => {
this.editor = editor;
this.setupListeners();
this.emitEditorEvents();
this.scrollToTextIfNeeded();
this.editor.actions.updateAndRender();
});
}
init(cmLoadPromise) {
let lastClick = 0;
// Prevent markdown display link click redirect
this.displayDoc.addEventListener('click', event => {
let isDblClick = Date.now() - lastClick < 300;
let link = event.target.closest('a');
if (link !== null) {
event.preventDefault();
window.open(link.getAttribute('href'));
return;
}
let drawing = event.target.closest('[drawio-diagram]');
if (drawing !== null && isDblClick) {
this.actionEditDrawing(drawing);
return;
}
lastClick = Date.now();
emitEditorEvents() {
window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
markdownIt: this.editor.markdown.getRenderer(),
displayEl: this.display,
codeMirrorInstance: this.editor.cm,
});
}
setupListeners() {
// Button actions
this.elem.addEventListener('click', event => {
let button = event.target.closest('button[data-action]');
if (button === null) return;
let action = button.getAttribute('data-action');
if (action === 'insertImage') this.actionInsertImage();
if (action === 'insertLink') this.actionShowLinkSelector();
const action = button.getAttribute('data-action');
if (action === 'insertImage') this.editor.actions.insertImage();
if (action === 'insertLink') this.editor.actions.showLinkSelector();
if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
this.actionShowImageManager();
this.editor.actions.showImageManager();
return;
}
if (action === 'insertDrawing') this.actionStartDrawing();
if (action === 'fullscreen') this.actionFullScreen();
if (action === 'insertDrawing') this.editor.actions.startDrawing();
if (action === 'fullscreen') this.editor.actions.fullScreen();
});
// Mobile section toggling
@@ -97,527 +73,68 @@ class MarkdownEditor {
if (!toolbarLabel) return;
const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
for (let activeElem of currentActiveSections) {
for (const activeElem of currentActiveSections) {
activeElem.classList.remove('active');
}
toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
});
cmLoadPromise.then(cm => {
this.codeMirrorSetup(cm);
// Refresh CodeMirror on container resize
const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false);
const observer = new ResizeObserver(resizeDebounced);
observer.observe(this.elem);
// Refresh CodeMirror on container resize
const resizeDebounced = debounce(() => this.Code.updateLayout(cm), 100, false);
const observer = new ResizeObserver(resizeDebounced);
observer.observe(this.elem);
this.handleDividerDrag();
}
handleDividerDrag() {
this.divider.addEventListener('pointerdown', event => {
const wrapRect = this.elem.getBoundingClientRect();
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.editor.settings.set('editorWidth', xPct);
};
const upListener = (event) => {
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';
document.body.style.userSelect = 'none';
window.addEventListener('pointermove', moveListener);
window.addEventListener('pointerup', upListener);
});
const widthSetting = this.editor.settings.get('editorWidth');
if (widthSetting) {
this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
}
}
this.listenForBookStackEditorEvents();
// Scroll to text if needed.
scrollToTextIfNeeded() {
const queryParams = (new URL(window.location)).searchParams;
const scrollText = queryParams.get('content-text');
if (scrollText) {
this.scrollToText(scrollText);
this.editor.actions.scrollToText(scrollText);
}
}
// Update the input content and render the display.
updateAndRender() {
const content = this.cm.getValue();
this.input.value = content;
const html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
// Set body content
const target = this.getDisplayTarget();
this.displayDoc.body.className = 'page-content';
patchDomFromHtmlString(target, html);
// Copy styles from page head and set custom styles for editor
this.loadStylesIntoDisplay();
}
getDisplayTarget() {
const body = this.displayDoc.body;
if (body.children.length === 0) {
const wrap = document.createElement('div');
this.displayDoc.body.append(wrap);
}
return body.children[0];
}
loadStylesIntoDisplay() {
if (this.displayStylesLoaded) return;
this.displayDoc.documentElement.classList.add('markdown-editor-display');
// Set display to be dark mode if parent is
if (document.documentElement.classList.contains('dark-mode')) {
this.displayDoc.documentElement.style.backgroundColor = '#222';
this.displayDoc.documentElement.classList.add('dark-mode');
}
this.displayDoc.head.innerHTML = '';
const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
for (let style of styles) {
const copy = style.cloneNode(true);
this.displayDoc.head.appendChild(copy);
}
this.displayStylesLoaded = true;
}
onMarkdownScroll(lineCount) {
const elems = this.displayDoc.body.children;
if (elems.length <= lineCount) return;
const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
}
codeMirrorSetup(cm) {
const context = this;
// Text direction
// cm.setOption('direction', this.textDirection);
cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
// Custom key commands
let metaKey = this.Code.getMetaKey();
const extraKeys = {};
// Insert Image shortcut
extraKeys[`${metaKey}-Alt-I`] = function(cm) {
let selectedText = cm.getSelection();
let newText = `![${selectedText}](http://)`;
let cursorPos = cm.getCursor('from');
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
};
// Save draft
extraKeys[`${metaKey}-S`] = cm => {window.$events.emit('editor-save-draft')};
// Save page
extraKeys[`${metaKey}-Enter`] = cm => {window.$events.emit('editor-save-page')};
// Show link selector
extraKeys[`Shift-${metaKey}-K`] = cm => {this.actionShowLinkSelector()};
// Insert Link
extraKeys[`${metaKey}-K`] = cm => {insertLink()};
// FormatShortcuts
extraKeys[`${metaKey}-1`] = cm => {replaceLineStart('##');};
extraKeys[`${metaKey}-2`] = cm => {replaceLineStart('###');};
extraKeys[`${metaKey}-3`] = cm => {replaceLineStart('####');};
extraKeys[`${metaKey}-4`] = cm => {replaceLineStart('#####');};
extraKeys[`${metaKey}-5`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-D`] = cm => {replaceLineStart('');};
extraKeys[`${metaKey}-6`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-Q`] = cm => {replaceLineStart('>');};
extraKeys[`${metaKey}-7`] = cm => {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = cm => {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = cm => {wrapSelection('`', '`');};
extraKeys[`${metaKey}-9`] = cm => {wrapSelection('<p class="callout info">', '</p>');};
extraKeys[`${metaKey}-P`] = cm => {replaceLineStart('-')}
extraKeys[`${metaKey}-O`] = cm => {replaceLineStartForOrderedList()}
cm.setOption('extraKeys', extraKeys);
// Update data on content change
cm.on('change', (instance, changeObj) => {
this.updateAndRender();
});
const onScrollDebounced = debounce((instance) => {
// Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
let scroll = instance.getScrollInfo();
let atEnd = scroll.top + scroll.clientHeight === scroll.height;
if (atEnd) {
this.onMarkdownScroll(-1);
return;
}
let lineNum = instance.lineAtHeight(scroll.top, 'local');
let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
let parser = new DOMParser();
let doc = parser.parseFromString(this.markdown.render(range), 'text/html');
let totalLines = doc.documentElement.querySelectorAll('body > *');
this.onMarkdownScroll(totalLines.length);
}, 100);
// Handle scroll to sync display view
cm.on('scroll', instance => {
onScrollDebounced(instance);
});
// 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 images = clipboard.getImages();
for (const image of images) {
uploadImage(image);
}
});
// Handle image & content drag n drop
cm.on('drop', (cm, event) => {
const templateId = event.dataTransfer.getData('bookstack/template');
if (templateId) {
const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
event.preventDefault();
window.$http.get(`/templates/${templateId}`).then(resp => {
const content = resp.data.markdown || resp.data.html;
cm.replaceSelection(content);
});
}
const clipboard = new Clipboard(event.dataTransfer);
if (clipboard.hasItems() && clipboard.getImages().length > 0) {
const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
event.stopPropagation();
event.preventDefault();
const images = clipboard.getImages();
for (const image of images) {
uploadImage(image);
}
}
});
// Helper to replace editor content
function replaceContent(search, replace) {
let text = cm.getValue();
let cursor = cm.listSelections();
cm.setValue(text.replace(search, replace));
cm.setSelections(cursor);
}
// Helper to replace the start of the line
function replaceLineStart(newStart) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let lineLen = lineContent.length;
let lineStart = lineContent.split(' ')[0];
// Remove symbol if already set
if (lineStart === newStart) {
lineContent = lineContent.replace(`${newStart} `, '');
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
return;
}
let alreadySymbol = /^[#>`]/.test(lineStart);
let posDif = 0;
if (alreadySymbol) {
posDif = newStart.length - lineStart.length;
lineContent = lineContent.replace(lineStart, newStart).trim();
} else if (newStart !== '') {
posDif = newStart.length + 1;
lineContent = newStart + ' ' + lineContent;
}
cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
}
function wrapLine(start, end) {
let cursor = cm.getCursor();
let lineContent = cm.getLine(cursor.line);
let 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}`;
}
cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
}
function wrapSelection(start, end) {
let selection = cm.getSelection();
if (selection === '') return wrapLine(start, end);
let newSelection = selection;
let frontDiff = 0;
let endDiff = 0;
if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
newSelection = selection.slice(start.length, selection.length - end.length);
endDiff = -(end.length + start.length);
} else {
newSelection = `${start}${selection}${end}`;
endDiff = start.length + end.length;
}
let selections = cm.listSelections()[0];
cm.replaceSelection(newSelection);
let headFirst = selections.head.ch <= selections.anchor.ch;
selections.head.ch += headFirst ? frontDiff : endDiff;
selections.anchor.ch += headFirst ? endDiff : frontDiff;
cm.setSelections([selections]);
}
function replaceLineStartForOrderedList() {
const cursor = cm.getCursor();
const prevLineContent = cm.getLine(cursor.line - 1) || '';
const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || [];
const number = (Number(listMatch[2]) || 0) + 1;
const whiteSpace = listMatch[1] || '';
const listMark = listMatch[3] || '.'
const prefix = `${whiteSpace}${number}${listMark}`;
return replaceLineStart(prefix);
}
// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file === null || file.type.indexOf('image') !== 0) return;
let ext = 'png';
if (file.name) {
let 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 placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
const selectedText = cm.getSelection();
const placeHolderText = `![${selectedText}](${placeholderImage})`;
const cursor = cm.getCursor();
cm.replaceSelection(placeHolderText);
cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
const remoteFilename = "image-" + Date.now() + "." + ext;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', context.pageId);
window.$http.post('/images/gallery', formData).then(resp => {
const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
replaceContent(placeHolderText, newContent);
}).catch(err => {
window.$events.emit('error', context.imageUploadErrorText);
replaceContent(placeHolderText, selectedText);
console.log(err);
});
}
function insertLink() {
let cursorPos = cm.getCursor('from');
let selectedText = cm.getSelection() || '';
let newText = `[${selectedText}]()`;
cm.focus();
cm.replaceSelection(newText);
let cursorPosDiff = (selectedText === '') ? -3 : -1;
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
}
this.updateAndRender();
}
actionInsertImage() {
const cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
const imageUrl = image.thumbs.display || image.url;
let selectedText = this.cm.getSelection();
let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
}, 'gallery');
}
actionShowImageManager() {
const cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
this.insertDrawing(image, cursorPos);
}, 'drawio');
}
// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
const cursorPos = this.cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = this.cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
/**
* Get the URL for the configured drawio instance.
* @returns {String}
*/
getDrawioUrl() {
const drawioUrlElem = document.querySelector('[drawio-url]');
return drawioUrlElem ? drawioUrlElem.getAttribute('drawio-url') : false;
}
// Show draw.io if enabled and handle save.
actionStartDrawing() {
const url = this.getDrawioUrl();
if (!url) return;
const cursorPos = this.cm.getCursor('from');
DrawIO.show(url,() => {
return Promise.resolve('');
}, (pngData) => {
const data = {
image: pngData,
uploaded_to: Number(this.pageId),
};
window.$http.post("/images/drawio", data).then(resp => {
this.insertDrawing(resp.data, cursorPos);
DrawIO.close();
}).catch(err => {
this.handleDrawingUploadError(err);
});
});
}
insertDrawing(image, originalCursor) {
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
}
// Show draw.io if enabled and handle save.
actionEditDrawing(imgContainer) {
const drawioUrl = this.getDrawioUrl();
if (!drawioUrl) {
return;
const drawioAttrEl = document.querySelector('[drawio-url]');
if (!drawioAttrEl) {
return '';
}
const cursorPos = this.cm.getCursor('from');
const drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(drawioUrl, () => {
return DrawIO.load(drawingId);
}, (pngData) => {
let data = {
image: pngData,
uploaded_to: Number(this.pageId),
};
window.$http.post("/images/drawio", data).then(resp => {
let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
let newContent = this.cm.getValue().split('\n').map(line => {
if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
return newText;
}
return line;
}).join('\n');
this.cm.setValue(newContent);
this.cm.setCursor(cursorPos);
this.cm.focus();
DrawIO.close();
}).catch(err => {
this.handleDrawingUploadError(err);
});
});
return drawioAttrEl.getAttribute('drawio-url') || '';
}
handleDrawingUploadError(error) {
if (error.status === 413) {
window.$events.emit('error', this.serverUploadLimitText);
} else {
window.$events.emit('error', this.imageUploadErrorText);
}
console.log(error);
}
// Make the editor full screen
actionFullScreen() {
const alreadyFullscreen = this.elem.classList.contains('fullscreen');
this.elem.classList.toggle('fullscreen', !alreadyFullscreen);
document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
}
// Scroll to a specified text
scrollToText(searchText) {
if (!searchText) {
return;
}
const content = this.cm.getValue();
const lines = content.split(/\r?\n/);
let lineNumber = lines.findIndex(line => {
return line && line.indexOf(searchText) !== -1;
});
if (lineNumber === -1) {
return;
}
this.cm.scrollIntoView({
line: lineNumber,
}, 200);
this.cm.focus();
// set the cursor location.
this.cm.setCursor({
line: lineNumber,
char: lines[lineNumber].length
})
}
listenForBookStackEditorEvents() {
function getContentToInsert({html, markdown}) {
return markdown || html;
}
// Replace editor content
window.$events.listen('editor::replace', (eventContent) => {
const markdown = getContentToInsert(eventContent);
this.cm.setValue(markdown);
});
// Append editor content
window.$events.listen('editor::append', (eventContent) => {
const cursorPos = this.cm.getCursor('from');
const markdown = getContentToInsert(eventContent);
const content = this.cm.getValue() + '\n' + markdown;
this.cm.setValue(content);
this.cm.setCursor(cursorPos.line, cursorPos.ch);
});
// Prepend editor content
window.$events.listen('editor::prepend', (eventContent) => {
const cursorPos = this.cm.getCursor('from');
const markdown = getContentToInsert(eventContent);
const content = markdown + '\n' + this.cm.getValue();
this.cm.setValue(content);
const prependLineCount = markdown.split('\n').length;
this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
});
// Insert editor content at the current location
window.$events.listen('editor::insert', (eventContent) => {
const markdown = getContentToInsert(eventContent);
this.cm.replaceSelection(markdown);
});
// Focus on editor
window.$events.listen('editor::focus', () => {
this.cm.focus();
});
}
}
export default MarkdownEditor ;

View File

@@ -1,9 +1,11 @@
import {Component} from "./component";
class NewUserPassword {
export class NewUserPassword extends Component {
constructor(elem) {
this.elem = elem;
this.inviteOption = elem.querySelector('input[name=send_invite]');
setup() {
this.container = this.$el;
this.inputContainer = this.$refs.inputContainer;
this.inviteOption = this.container.querySelector('input[name=send_invite]');
if (this.inviteOption) {
this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
@@ -13,16 +15,12 @@ class NewUserPassword {
inviteOptionChange() {
const inviting = (this.inviteOption.value === 'true');
const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
const passwordBoxes = this.container.querySelectorAll('input[type=password]');
for (const input of passwordBoxes) {
input.disabled = inviting;
}
const container = this.elem.querySelector('#password-input-container');
if (container) {
container.style.display = inviting ? 'none' : 'block';
}
this.inputContainer.style.display = inviting ? 'none' : 'block';
}
}
export default NewUserPassword;
}

View File

@@ -1,19 +1,21 @@
import {Component} from "./component";
class Notification {
export class Notification extends Component {
constructor(elem) {
this.elem = elem;
this.type = elem.getAttribute('notification');
this.textElem = elem.querySelector('span');
this.autohide = this.elem.hasAttribute('data-autohide');
this.elem.style.display = 'grid';
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.container.style.display = 'grid';
window.$events.listen(this.type, text => {
this.show(text);
});
elem.addEventListener('click', this.hide.bind(this));
this.container.addEventListener('click', this.hide.bind(this));
if (elem.hasAttribute('data-show')) {
if (this.initialShow) {
setTimeout(() => this.show(this.textElem.textContent), 100);
}
@@ -21,14 +23,14 @@ class Notification {
}
show(textToShow = '') {
this.elem.removeEventListener('transitionend', this.hideCleanup);
this.container.removeEventListener('transitionend', this.hideCleanup);
this.textElem.textContent = textToShow;
this.elem.style.display = 'grid';
this.container.style.display = 'grid';
setTimeout(() => {
this.elem.classList.add('showing');
this.container.classList.add('showing');
}, 1);
if (this.autohide) {
if (this.autoHide) {
const words = textToShow.split(' ').length;
const timeToShow = Math.max(2000, 1000 + (250 * words));
setTimeout(this.hide.bind(this), timeToShow);
@@ -36,15 +38,13 @@ class Notification {
}
hide() {
this.elem.classList.remove('showing');
this.elem.addEventListener('transitionend', this.hideCleanup);
this.container.classList.remove('showing');
this.container.addEventListener('transitionend', this.hideCleanup);
}
hideCleanup() {
this.elem.style.display = 'none';
this.elem.removeEventListener('transitionend', this.hideCleanup);
this.container.style.display = 'none';
this.container.removeEventListener('transitionend', this.hideCleanup);
}
}
export default Notification;
}

View File

@@ -1,6 +1,7 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
class OptionalInput {
export class OptionalInput extends Component {
setup() {
this.removeButton = this.$refs.remove;
this.showButton = this.$refs.show;
@@ -23,6 +24,4 @@ class OptionalInput {
});
}
}
export default OptionalInput;
}

View File

@@ -1,9 +1,8 @@
import {scrollAndHighlightElement} from "../services/util";
import {Component} from "./component";
import {htmlToDom} from "../services/dom";
/**
* @extends {Component}
*/
class PageComments {
export class PageComments extends Component {
setup() {
this.elem = this.$el;
@@ -90,7 +89,7 @@ class PageComments {
newComment.innerHTML = resp.data;
this.editingComment.innerHTML = newComment.children[0].innerHTML;
window.$events.success(this.updatedText);
window.components.init(this.editingComment);
window.$components.init(this.editingComment);
this.closeUpdateForm();
this.editingComment = null;
}).catch(window.$events.showValidationErrors).then(() => {
@@ -119,11 +118,9 @@ class PageComments {
};
this.showLoading(this.form);
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
let newElem = newComment.children[0];
const newElem = htmlToDom(resp.data);
this.container.appendChild(newElem);
window.components.init(newElem);
window.$components.init(newElem);
window.$events.success(this.createdText);
this.resetForm();
this.updateCount();
@@ -199,6 +196,4 @@ class PageComments {
formElem.querySelector('.form-group.loading').style.display = 'none';
}
}
export default PageComments;
}

View File

@@ -1,21 +1,20 @@
import Clipboard from "clipboard/dist/clipboard.min";
import * as DOM from "../services/dom";
import {scrollAndHighlightElement} from "../services/util";
import {Component} from "./component";
class PageDisplay {
export class PageDisplay extends Component {
constructor(elem) {
this.elem = elem;
this.pageId = elem.getAttribute('page-display');
setup() {
this.container = this.$el;
this.pageId = this.$opts.pageId;
window.importVersioned('code').then(Code => Code.highlight());
this.setupPointer();
this.setupNavHighlighting();
this.setupDetailsCodeBlockRefresh();
// Check the hash on load
if (window.location.hash) {
let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
const text = window.location.hash.replace(/%20/g, ' ').substring(1);
this.goToText(text);
}
@@ -24,7 +23,7 @@ class PageDisplay {
if (sidebarPageNav) {
DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
event.preventDefault();
window.components['tri-layout'][0].showContent();
window.$components.first('tri-layout').showContent();
const contentId = child.getAttribute('href').substr(1);
this.goToText(contentId);
window.history.pushState(null, null, '#' + contentId);
@@ -50,120 +49,11 @@ class PageDisplay {
}
}
setupPointer() {
let pointer = document.getElementById('pointer');
if (!pointer) {
return;
}
// Set up pointer
pointer = pointer.parentNode.removeChild(pointer);
const pointerInner = pointer.querySelector('div.pointer');
// Instance variables
let pointerShowing = false;
let isSelection = false;
let pointerModeLink = true;
let pointerSectionId = '';
// Select all contents on input click
DOM.onChildEvent(pointer, 'input', 'click', (event, input) => {
input.select();
event.stopPropagation();
});
// Prevent closing pointer when clicked or focused
DOM.onEvents(pointer, ['click', 'focus'], event => {
event.stopPropagation();
});
// Pointer mode toggle
DOM.onChildEvent(pointer, 'span.icon', 'click', (event, icon) => {
event.stopPropagation();
pointerModeLink = !pointerModeLink;
icon.querySelector('[data-icon="include"]').style.display = (!pointerModeLink) ? 'inline' : 'none';
icon.querySelector('[data-icon="link"]').style.display = (pointerModeLink) ? 'inline' : 'none';
updatePointerContent();
});
// Set up clipboard
new Clipboard(pointer.querySelector('button'));
// Hide pointer when clicking away
DOM.onEvents(document.body, ['click', 'focus'], event => {
if (!pointerShowing || isSelection) return;
pointer = pointer.parentElement.removeChild(pointer);
pointerShowing = false;
});
let updatePointerContent = (element) => {
let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
if (pointerModeLink && !inputText.startsWith('http')) {
inputText = window.location.protocol + "//" + window.location.host + inputText;
}
pointer.querySelector('input').value = inputText;
// Update anchor if present
const editAnchor = pointer.querySelector('#pointer-edit');
if (editAnchor && element) {
const editHref = editAnchor.dataset.editHref;
const elementId = element.id;
// get the first 50 characters.
const queryContent = element.textContent && element.textContent.substring(0, 50);
editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
}
};
// Show pointer when selecting a single block of tagged content
DOM.forEach('.page-content [id^="bkmrk"]', bookMarkElem => {
DOM.onEvents(bookMarkElem, ['mouseup', 'keyup'], event => {
event.stopPropagation();
let selection = window.getSelection();
if (selection.toString().length === 0) return;
// Show pointer and set link
pointerSectionId = bookMarkElem.id;
updatePointerContent(bookMarkElem);
bookMarkElem.parentNode.insertBefore(pointer, bookMarkElem);
pointer.style.display = 'block';
pointerShowing = true;
isSelection = true;
// Set pointer to sit near mouse-up position
requestAnimationFrame(() => {
const bookMarkBounds = bookMarkElem.getBoundingClientRect();
let pointerLeftOffset = (event.pageX - bookMarkBounds.left - 164);
if (pointerLeftOffset < 0) {
pointerLeftOffset = 0
}
const pointerLeftOffsetPercent = (pointerLeftOffset / bookMarkBounds.width) * 100;
pointerInner.style.left = pointerLeftOffsetPercent + '%';
setTimeout(() => {
isSelection = false;
}, 100);
});
});
});
}
setupNavHighlighting() {
// Check if support is present for IntersectionObserver
if (!('IntersectionObserver' in window) ||
!('IntersectionObserverEntry' in window) ||
!('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
return;
}
let pageNav = document.querySelector('.sidebar-page-nav');
const pageNav = document.querySelector('.sidebar-page-nav');
// fetch all the headings.
let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
// if headings are present, add observers.
if (headings.length > 0 && pageNav !== null) {
addNavObserver(headings);
@@ -171,21 +61,21 @@ class PageDisplay {
function addNavObserver(headings) {
// Setup the intersection observer.
let intersectOpts = {
const intersectOpts = {
rootMargin: '0px 0px 0px 0px',
threshold: 1.0
};
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
for (let heading of headings) {
for (const heading of headings) {
pageNavObserver.observe(heading);
}
}
function headingVisibilityChange(entries, observer) {
for (let entry of entries) {
let isVisible = (entry.intersectionRatio === 1);
for (const entry of entries) {
const isVisible = (entry.intersectionRatio === 1);
toggleAnchorHighlighting(entry.target.id, isVisible);
}
}
@@ -203,9 +93,7 @@ class PageDisplay {
codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
};
const details = [...this.elem.querySelectorAll('details')];
const details = [...this.container.querySelectorAll('details')];
details.forEach(detail => detail.addEventListener('toggle', onToggle));
}
}
export default PageDisplay;
}

View File

@@ -1,11 +1,9 @@
import * as Dates from "../services/dates";
import {onSelect} from "../services/dom";
import {debounce} from "../services/util";
import {Component} from "./component";
/**
* Page Editor
* @extends {Component}
*/
class PageEditor {
export class PageEditor extends Component {
setup() {
// Options
this.draftsEnabled = this.$opts.draftsEnabled === 'true';
@@ -69,7 +67,8 @@ class PageEditor {
});
// Changelog controls
this.changelogInput.addEventListener('change', this.updateChangelogDisplay.bind(this));
const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
this.changelogInput.addEventListener('input', updateChangelogDebounced);
// Draft Controls
onSelect(this.saveDraftButton, this.saveDraft.bind(this));
@@ -199,7 +198,8 @@ class PageEditor {
event.preventDefault();
const link = event.target.closest('a').href;
const dialog = this.switchDialogContainer.components['confirm-dialog'];
/** @var {ConfirmDialog} **/
const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
if (saved && confirmed) {
@@ -208,5 +208,3 @@ class PageEditor {
}
}
export default PageEditor;

View File

@@ -1,14 +1,14 @@
import {Component} from "./component";
class PagePicker {
export class PagePicker extends Component {
constructor(elem) {
this.elem = elem;
this.input = elem.querySelector('input');
this.resetButton = elem.querySelector('[page-picker-reset]');
this.selectButton = elem.querySelector('[page-picker-select]');
this.display = elem.querySelector('[page-picker-display]');
this.defaultDisplay = elem.querySelector('[page-picker-default]');
this.buttonSep = elem.querySelector('span.sep');
setup() {
this.input = this.$refs.input;
this.resetButton = this.$refs.resetButton;
this.selectButton = this.$refs.selectButton;
this.display = this.$refs.display;
this.defaultDisplay = this.$refs.defaultDisplay;
this.buttonSep = this.$refs.buttonSeperator;
this.value = this.input.value;
this.setupListeners();
@@ -24,7 +24,9 @@ class PagePicker {
}
showPopup() {
window.EntitySelectorPopup.show(entity => {
/** @type {EntitySelectorPopup} **/
const selectorPopup = window.$components.first('entity-selector-popup');
selectorPopup.show(entity => {
this.setValue(entity.id, entity.name);
});
}
@@ -36,7 +38,7 @@ class PagePicker {
}
controlView(name) {
let hasValue = this.value && this.value !== 0;
const hasValue = this.value && this.value !== 0;
toggleElem(this.resetButton, hasValue);
toggleElem(this.buttonSep, hasValue);
toggleElem(this.defaultDisplay, !hasValue);
@@ -55,8 +57,5 @@ class PagePicker {
}
function toggleElem(elem, show) {
let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
elem.style.display = show ? display : 'none';
}
export default PagePicker;
elem.style.display = show ? null : 'none';
}

View File

@@ -1,8 +1,11 @@
import {Component} from "./component";
class PermissionsTable {
export class PermissionsTable extends Component {
setup() {
this.container = this.$el;
this.cellSelector = this.$opts.cellSelector || 'td,th';
this.rowSelector = this.$opts.rowSelector || 'tr';
// Handle toggle all event
for (const toggleAllElem of (this.$manyRefs.toggleAll || [])) {
@@ -27,15 +30,15 @@ class PermissionsTable {
toggleRowClick(event) {
event.preventDefault();
this.toggleAllInElement(event.target.closest('tr'));
this.toggleAllInElement(event.target.closest(this.rowSelector));
}
toggleColumnClick(event) {
event.preventDefault();
const tableCell = event.target.closest('th,td');
const tableCell = event.target.closest(this.cellSelector);
const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);
const tableRows = tableCell.closest('table').querySelectorAll('tr');
const tableRows = this.container.querySelectorAll(this.rowSelector);
const inputsToToggle = [];
for (let row of tableRows) {
@@ -60,6 +63,4 @@ class PermissionsTable {
}
}
}
export default PermissionsTable;
}

View File

@@ -0,0 +1,128 @@
import * as DOM from "../services/dom";
import Clipboard from "clipboard/dist/clipboard.min";
import {Component} from "./component";
export class Pointer extends Component {
setup() {
this.container = this.$el;
this.pageId = this.$opts.pageId;
// Instance variables
this.showing = false;
this.isSelection = false;
this.pointerModeLink = true;
this.pointerSectionId = '';
this.setupListeners();
// Set up clipboard
new Clipboard(this.container.querySelector('button'));
}
setupListeners() {
// Select all contents on input click
DOM.onChildEvent(this.container, 'input', 'click', (event, input) => {
input.select();
event.stopPropagation();
});
// Prevent closing pointer when clicked or focused
DOM.onEvents(this.container, ['click', 'focus'], event => {
event.stopPropagation();
});
// Pointer mode toggle
DOM.onChildEvent(this.container, 'span.icon', 'click', (event, icon) => {
event.stopPropagation();
this.pointerModeLink = !this.pointerModeLink;
icon.querySelector('[data-icon="include"]').style.display = (!this.pointerModeLink) ? 'inline' : 'none';
icon.querySelector('[data-icon="link"]').style.display = (this.pointerModeLink) ? 'inline' : 'none';
this.updateForTarget();
});
// Hide pointer when clicking away
DOM.onEvents(document.body, ['click', 'focus'], event => {
if (!this.showing || this.isSelection) return;
this.hidePointer();
});
// Show pointer when selecting a single block of tagged content
const pageContent = document.querySelector('.page-content');
DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => {
event.stopPropagation();
const targetEl = event.target.closest('[id^="bkmrk"]');
if (targetEl) {
this.showPointerAtTarget(targetEl, event.pageX);
}
});
}
hidePointer() {
this.container.style.display = null;
this.showing = false;
}
/**
* Move and display the pointer at the given element, targeting the given screen x-position if possible.
* @param {Element} element
* @param {Number} xPosition
*/
showPointerAtTarget(element, xPosition) {
const selection = window.getSelection();
if (selection.toString().length === 0) return;
// Show pointer and set link
this.pointerSectionId = element.id;
this.updateForTarget(element);
this.container.style.display = 'block';
const targetBounds = element.getBoundingClientRect();
const pointerBounds = this.container.getBoundingClientRect();
const xTarget = Math.min(Math.max(xPosition, targetBounds.left), targetBounds.right);
const xOffset = xTarget - (pointerBounds.width / 2);
const yOffset = (targetBounds.top - pointerBounds.height) - 16;
this.container.style.left = `${xOffset}px`;
this.container.style.top = `${yOffset}px`;
this.showing = true;
this.isSelection = true;
setTimeout(() => {
this.isSelection = false;
}, 100);
const scrollListener = () => {
this.hidePointer();
window.removeEventListener('scroll', scrollListener, {passive: true});
};
window.addEventListener('scroll', scrollListener, {passive: true});
}
/**
* Update the pointer inputs/content for the given target element.
* @param {?Element} element
*/
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;
}
this.container.querySelector('input').value = inputText;
// Update anchor if present
const editAnchor = this.container.querySelector('#pointer-edit');
if (editAnchor && element) {
const editHref = editAnchor.dataset.editHref;
const elementId = element.id;
// get the first 50 characters.
const queryContent = element.textContent && element.textContent.substring(0, 50);
editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
}
}
}

View File

@@ -1,13 +1,13 @@
import {fadeIn, fadeOut} from "../services/animations";
import {onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Popup window that will contain other content.
* This component provides the show/hide functionality
* with the ability for popup@hide child references to close this.
* @extends {Component}
*/
class Popup {
export class Popup extends Component {
setup() {
this.container = this.$el;
@@ -56,6 +56,4 @@ class Popup {
this.onHide = onHide;
}
}
export default Popup;
}

View File

@@ -1,23 +1,13 @@
import {Component} from "./component";
class SettingAppColorPicker {
export class SettingAppColorPicker extends Component {
constructor(elem) {
this.elem = elem;
this.colorInput = elem.querySelector('input[type=color]');
this.lightColorInput = elem.querySelector('input[name="setting-app-color-light"]');
this.resetButton = elem.querySelector('[setting-app-color-picker-reset]');
this.defaultButton = elem.querySelector('[setting-app-color-picker-default]');
setup() {
this.colorInput = this.$refs.input;
this.lightColorInput = this.$refs.lightInput;
this.colorInput.addEventListener('change', this.updateColor.bind(this));
this.colorInput.addEventListener('input', this.updateColor.bind(this));
this.resetButton.addEventListener('click', event => {
this.colorInput.value = this.colorInput.dataset.current;
this.updateColor();
});
this.defaultButton.addEventListener('click', event => {
this.colorInput.value = this.colorInput.dataset.default;
this.updateColor();
});
}
/**
@@ -44,8 +34,8 @@ class SettingAppColorPicker {
/**
* Covert a hex color code to rgb components.
* @attribution https://stackoverflow.com/a/5624139
* @param hex
* @returns {*}
* @param {String} hex
* @returns {{r: Number, g: Number, b: Number}}
*/
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -57,5 +47,3 @@ class SettingAppColorPicker {
}
}
export default SettingAppColorPicker;

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