Compare commits

...

165 Commits

Author SHA1 Message Date
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
03eb63ec77 Made it possible to pre-fill login via url
Allows email to be passed to email field.
Also allows password only if in demo mode (Due to security concerns).
2017-12-10 13:56:25 +00:00
Dan Brown
3ed5426315 Moved book cover image input into collapsible section
Prevent extra friction when creating a new book and makes it easier to
skip if grid view is not in use
2017-12-10 13:46:50 +00:00
Dan Brown
90bf13c1ab Updated okta config keys, made SVG fully vector
Also added some additional error handling to login.
2017-12-09 13:32:45 +00:00
Dan Brown
d17eb0f54c Merge branch 'master' of git://github.com/lommes/BookStack into lommes-master 2017-12-09 12:48:08 +00:00
Dan Brown
ac7e3977de Fixed WYSIWYG fullscreen mode on firefox
Prevented overlapping sidebar and collapsed content.
Fixes #605
2017-12-09 12:38:30 +00:00
Dan Brown
d7edc389a6 Enabled custom HTML head content to work within editors
Closes #562
2017-12-08 11:52:43 +00:00
Dan Brown
56d5af1336 Made it possible to configure proxies via env
In reference to #146
2017-12-07 19:46:47 +00:00
Dan Brown
06cf175b08 Prevented page navigation highlighting erroring
This was when no page nav was on the page
2017-12-07 19:27:54 +00:00
Dan Brown
b65abd25e0 Made small var name and formatting tweaks 2017-12-07 19:19:25 +00:00
Dan Brown
a5e49f642b Merge branch 'disable-comments' of git://github.com/Abijeet/BookStack into Abijeet-disable-comments 2017-12-07 19:15:26 +00:00
Dan Brown
91444e83fd Cleaned up some page-show JS 2017-12-07 19:10:31 +00:00
Dan Brown
6063ac4a11 Merge branch 'master' of git://github.com/Abijeet/BookStack 2017-12-07 18:47:07 +00:00
Dan Brown
02fd1c48ed Added meta+enter shortcut for page save
Closes #604
2017-12-07 18:44:20 +00:00
Dan Brown
6ee35f55cc Refactored image picker to js component
Also adjusted default cover image size
2017-12-06 17:32:29 +00:00
Dan Brown
261e57fc4e Converted books view setting to user setting
Also cleaned up/moved new CSS and removed redundant new book methods.
2017-12-06 16:34:26 +00:00
Dan Brown
bc1302a8d8 Merge branch 'BookStackApp-master' of git://github.com/OsmosysSoftware/BookStack into OsmosysSoftware-BookStackApp-master 2017-12-06 15:52:54 +00:00
Dan Brown
eeb2b8cbe5 Prevented finding of check script in lang tests 2017-12-06 11:17:34 +00:00
Dan Brown
b167ae795e Added script to check translation files
Closes #373
2017-12-04 20:25:04 +00:00
Dan Brown
6ebe8bf619 Fixed conflicting PDF facade namespace and corrected php version
Updated composer to have the correct config to install dependancies that
work for 7.0
2017-12-04 17:59:53 +00:00
Timo Bartholomes
009af9736e Add socialite authentication for okta 2017-11-26 16:41:29 +01:00
Dan Brown
7668a999a2 Fixed heavy init breakages made in last commit 2017-11-19 18:31:24 +00:00
Dan Brown
ed88c623d6 Made some further laravel 5.5 cleanup
Removed old bootstrap files that are not needed and
amended composer to laravel upgrade guide
2017-11-19 17:59:12 +00:00
Dan Brown
873b1099f8 Updated to laravel 5.5
Closes #590
2017-11-19 15:56:19 +00:00
Abijeet
6a54733f2b Adding testcases for comments disable / enable setting. 2017-11-16 23:32:36 +05:30
Abijeet
7a5bd23909 Added language translation for the new settings icons. 2017-11-16 00:22:22 +05:30
Abijeet
6bb7b5465f Added code in the settings to disable comments. Based on that hiding the comments section on the page display. 2017-11-16 00:05:24 +05:30
Abijeet Patro
0b967d84ad Merge pull request #2 from BookStackApp/master
Getting the latest code.
2017-11-15 08:05:20 +05:30
Abijeet
2261308415 Removed invalid comments, and formatted the code. 2017-11-15 00:04:35 +05:30
Abijeet
7b5edb4d62 Merge branch 'master' of https://github.com/Abijeet/BookStack 2017-11-15 00:01:48 +05:30
Abijeet
8378f06889 Highlights all headings currently visible. Also fixes extra scrollbar appearing in Firefox. 2017-11-14 23:54:25 +05:30
Abijeet Patro
10dc851697 Merge pull request #1 from BookStackApp/master
Getting the latest changes.
2017-11-13 23:22:45 +05:30
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
65579214e2 Allowed custom session expiry time
Closes #570
2017-11-11 18:30:55 +00:00
Dan Brown
d89440d198 Fixed required email confirmation with domain restriction
Added test to cover scenario.

Closes #573
2017-11-11 18:09:48 +00:00
Dan Brown
08e58bab79 Fixed vue component error 2017-11-11 17:10:15 +00:00
Dan Brown
d29b177c84 Merge pull request #563 from 10bass/master
Fix ajax tag suggestion for subdir installs
2017-11-11 17:03:36 +00:00
Dan Brown
151d72e42c Merge pull request #558 from lbguilherme/ptbr
Update pt_BR translations
2017-11-11 16:30:16 +00:00
Dan Brown
711ba258f1 Prevented mulitple hypens incorrectly in slug
Added test to check slug format.
Fixes #589
2017-11-11 16:27:29 +00:00
Dan Brown
df4d4f30f1 Added package-lock file for npm version locking 2017-11-11 16:19:24 +00:00
Dan Brown
f094837709 Added test to cover multi-byte slugs
Also removed check for 'mb_' functions since mbstring is a dependancy
2017-11-11 16:15:08 +00:00
Dan Brown
e27cbb9dce Merge branch 'wowkaster-patch-1' 2017-11-11 16:07:23 +00:00
Abijeet
bdba25b6f2 Refactored all functionality into one function. Changed margin-top. 2017-11-05 20:23:16 +05:30
Vladimir
6b2581de63 Russian slug and Multibyte String 2017-11-03 14:00:07 +02:00
Abijeet
1031c61d0c Fixes #466. Adds support for header highlighting using intersection observer. 2017-11-02 01:14:06 +05:30
10bass
46fc0e5026 Fix ajax tag suggestion for subdir installs
tag suggest URLs were hardcoded to /ajax in blade template. Wrapped them in baseUrl()
2017-10-16 18:24:47 -04:00
Guilherme Bernal
332f678ed0 Update pt_BR translations 2017-10-15 16:15:53 -03: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
0d5d77d8ab Updated search test to fit with new tokenization 2017-10-15 19:24:06 +01:00
Dan Brown
db51cee2d8 Prevented custom homepage being deleted
Fixes #546
2017-10-15 19:14:46 +01:00
Dan Brown
a988438946 Expanded list of indexing split chars
Expands on #531
2017-10-15 19:14:31 +01:00
Dan Brown
3bf7cac030 Prevented flexbox contains overflowing page
Fixes #552
2017-10-15 18:34:37 +01:00
Dan Brown
79c3a07e9a Fixed include syntax erroring within vue
Fixes #553
2017-10-15 18:20:13 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
9758872baf Updated image fetching in exporting
Added domain check to see if possibly local even when whole url found.
Changed image fetch from file_get_contents to curl for external
resources.

Hopeful solution to #392
2017-10-06 20:49:25 +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
b711bc6816 Prevented 'Discard draft' option showing after saving a draft page 2017-10-01 18:11:24 +01:00
Dan Brown
247e6dba85 Fixed some design issues around cards
Reverted drop shadow change.
Fixed header line-height when linked.
Fixed overflowing paragraph text. Fixes #533.
2017-10-01 17:59:51 +01:00
Dan Brown
2b3d6e4e4a Updated search-regen command description 2017-10-01 17:51:59 +01:00
Dan Brown
6b1980c4f3 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-10-01 13:19:41 +01:00
Dan Brown
9ba29770e1 Added azureAD social auth option
Closes #509
2017-10-01 13:19:17 +01:00
Dan Brown
3d375fae55 Merge pull request #529 from cipi1965/master
Updated italian translation
2017-10-01 11:44:45 +01:00
Dan Brown
c99a50de2c Merge pull request #528 from turbotankist/master
russian lang fixes
2017-10-01 11:43:31 +01:00
Dan Brown
1a32b25b5e Merge pull request #523 from sanderdw/master
Update dutch translations
2017-10-01 11:33:22 +01:00
Dan Brown
481aa5b5b0 Added 'last_commented' sort option to search
Closes #440
2017-10-01 11:24:33 +01:00
Dan Brown
c943eb4d0d Removed empty string null middleware as was causing issues 2017-09-30 14:44:52 +01:00
Dan Brown
aca6de49b0 Added missing middleware to trim input 2017-09-30 14:31:27 +01:00
Dan Brown
5fd04fa470 Updated search indexer to split words better
Will now split up words based on more chars than just spaces.
Not takes into account newlines, tabs, periods & commas.

Fixed #531
2017-09-30 14:14:23 +01:00
Dan Brown
87339e4cd0 Added missing codemirror theme class
Fixes #535
2017-09-30 13:48:38 +01:00
Dan Brown
a9eb058dad Updated issue template 2017-09-30 13:41:06 +01:00
Dan Brown
61fad6a665 Finished migration of last angular code 2017-09-30 13:27:08 +01:00
Matteo Piccina
fa4bee2d98 Updated italian translation 2017-09-27 10:44:08 +02:00
alexey
ce63260fa6 russian lang fixes 2017-09-27 11:17:56 +03:00
Dan Brown
a3557d5bb2 Tweaked shadows on cards 2017-09-24 18:47:34 +01:00
Dan Brown
9ca22976c3 Migrated editor toolbox, No more directives! 2017-09-24 18:30:21 +01:00
Dan Brown
9e2934fe17 Migrated editor inputs to non-angular JS 2017-09-23 12:24:06 +01:00
sanderdw
2259263214 Update entities.php 2017-09-23 00:52:08 +02:00
sanderdw
762cf5f183 Update components.php 2017-09-23 00:47:02 +02:00
sanderdw
07175f2b3e Update dutch translations 2017-09-23 00:28:25 +02: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
0c4ddf16a5 Moved details card above book nav 2017-09-20 21:32:19 +01:00
Dan Brown
74a5e3113e Fixed page includes erroring on save
Closes #514
2017-09-20 21:03:40 +01:00
Dan Brown
df0a982433 Merge branch 'master' of github.com:BookStackApp/BookStack 2017-09-20 20:27:50 +01:00
Dan Brown
212f924ffa Refactored WYSIWYG editor image upload code
Added sketchy timeout to fix images being pasted at end of page.
Fixes #489
2017-09-20 20:27:00 +01:00
Dan Brown
09936566dd Upgrade tinymce version 2017-09-20 20:26:34 +01:00
Dan Brown
9469c04eab Merge pull request #517 from leomartinez/master
Added 'Spanish Argentina' translation
2017-09-20 20:17:39 +01:00
Leonardo Martinez
941bb73a68 Added missing colon 2017-09-19 14:25:44 -03:00
Leonardo Martinez
8e652f5d8f Added 'Spanish Argentina' translation. 2017-09-19 13:43:17 -03:00
Leonardo Martinez
eec1b21928 Added 'Spanish Argentina' translation. 2017-09-19 13:42:39 -03:00
Dan Brown
1baeb7bec9 Merge pull request #510 from sanderdw/patch-1
Update entities.php
2017-09-17 11:09:12 +01:00
Dan Brown
61475ca0a3 Merge pull request #506 from turbotankist/master
Russian lang added
2017-09-14 20:44:19 +01:00
sanderdw
244c5a3ebb Update entities.php 2017-09-14 21:43:47 +02:00
Dan Brown
39e7ac1c15 Updated social login to redirect to intended page.
Closes #508.
2017-09-14 20:20:47 +01:00
alexey
ab7f5def04 Russian lang added 2017-09-12 16:42:04 +03:00
Bharadwaja G
5034f21394 Added migration file. 2017-09-05 19:53:29 +05:30
Bharadwaja G
e02fcbe983 Added Book cover image description in all languages. 2017-09-05 12:46:31 +05:30
Bharadwaja G
1c88d21abf Fixed books cover image ratio. 2017-09-04 20:50:24 +05:30
Bharadwaja G
c1a1bc0135 Books grid view 2017-09-04 20:27:52 +05:30
Bharadwaja G
6200948eec Merge branch 'master' of git://github.com/BookStackApp/BookStack into BookStackApp-master
Conflicts:
	app/Http/Controllers/BookController.php
	resources/lang/en/common.php
	resources/views/books/create.blade.php
	resources/views/books/form.blade.php
	resources/views/books/index.blade.php
	resources/views/users/edit.blade.php
	tests/Entity/EntityTest.php
2017-08-29 12:19:00 +05:30
Bharadwaja G
7f902e41c7 Resolved conflicts 2017-08-24 12:21:43 +05:30
Nilesh Deepak
3079a9f4de Reverted required changes. 2017-07-15 19:07:32 +05:30
Nilesh Deepak
a7d2cfdee2 Resolving test cases 2017-07-15 19:03:02 +05:30
Nilesh Deepak
a149e87ca7 Resolving test cases 2017-07-15 19:00:23 +05:30
Nilesh Deepak
854fd52a27 Resolving test cases 2017-07-15 18:57:09 +05:30
Nilesh Deepak
3d808ac75f Test for cover image 2017-07-15 18:39:13 +05:30
Nilesh Deepak
39b924f158 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 18:37:55 +05:30
Nilesh Deepak
a488ef6b00 Test for cover image. 2017-07-15 18:36:49 +05:30
abijeetp
6d66c38c12 Fixes issues with the test case, now creating a user with the required profile setting. 2017-07-15 18:00:39 +05:30
Nilesh Deepak
922964ecf2 Changes grid container size 2017-07-15 17:50:09 +05:30
Nilesh Deepak
0c70416b5c Test books display options. 2017-07-15 16:33:52 +05:30
Nilesh Deepak
770f30c3a8 Test books display options. 2017-07-15 16:29:42 +05:30
Nilesh Deepak
b4044e6c3a Resolves heading issues in grid view 2017-07-15 16:22:29 +05:30
Nilesh Deepak
9872767f20 Test for cover image upload 2017-07-15 16:19:35 +05:30
Nilesh Deepak
dd4d2f4696 Resolves book heading issues in grid view. 2017-07-15 16:15:45 +05:30
Nilesh Deepak
e5dc0e6bb8 Merge branch 'master' of https://github.com/OsmosysSoftware/BookStack 2017-07-15 16:13:48 +05:30
Nilesh Deepak
85fbe820c4 Adding getHeadingExcerpt to get heading. 2017-07-15 16:11:10 +05:30
abijeetp
832f8eaa94 Fixes the test case related to UserProfileTest. 2017-07-15 15:50:42 +05:30
Abijeet
3435dcc91e Merge pull request #10 from OsmosysSoftware/test-issue-181
Tests for issue 181
2017-07-15 14:29:38 +05:30
Nilesh Deepak
1ed74b8598 Test for grid and list layout selection. 2017-07-15 13:19:49 +05:30
Nilesh Deepak
fd36978c13 Test for layout selection. 2017-07-15 12:26:57 +05:30
Nilesh Deepak
1278a0b818 Test for layout selection. 2017-07-15 11:40:51 +05:30
Nilesh Deepak
6a6516ddd5 Test for layout selection. 2017-07-15 11:31:43 +05:30
Nilesh Deepak
1fe8f13503 Cover image test case 2017-07-14 18:36:50 +05:30
Nilesh Deepak
8f3adcda5d Cover image test case 2017-07-14 18:02:45 +05:30
Abijeet
21a8df78ee Merge pull request #9 from OsmosysSoftware/feature-181
Feature 181
2017-07-13 15:50:43 +05:30
Nilesh Deepak
7f8351e044 Removed avatar class from form.blade.php 2017-07-13 15:20:53 +05:30
Nilesh Deepak
afc1ecafe9 4. Changed the border color of the gallery item to #ccc 2017-07-13 12:27:14 +05:30
Nilesh Deepak
ab6ff5fda2 3. New default.png 2017-07-13 12:26:01 +05:30
Nilesh Deepak
b0ba1a43a9 2. Added classed col-xs-6 col-sm-4 col-md-4 col-lg-3 in grid-item.blade.php
5. Added <div class="row"> in index.blade.php
2017-07-13 12:24:47 +05:30
Nilesh Deepak
e919cab3d1 1. Thumbnail size when creating or editing book. 2017-07-13 12:22:43 +05:30
Abijeet
f37509062e Merge pull request #8 from OsmosysSoftware/feature-181
Issue 181
2017-07-12 18:41:35 +05:30
Nilesh Deepak
24ee78ccd8 Update. 2017-07-12 18:04:06 +05:30
Nilesh Deepak
d37b398e79 Updates styles. 2017-07-12 13:52:21 +05:30
Nilesh Deepak
7a724f9134 Updated modifications. 2017-07-12 13:44:37 +05:30
Abijeet
f3b2e0fb91 Merge pull request #7 from OsmosysSoftware/revert-3-revert-1-issue-181
Revert "Revert "Bookstack grid view.""
2017-07-12 11:41:01 +05:30
Abijeet
844976c85b Revert "Revert "Bookstack grid view."" 2017-07-12 11:40:50 +05:30
Abijeet
f0d914abbf Merge pull request #5 from BookStackApp/master
Getting latest changes
2017-07-12 11:33:58 +05:30
Abijeet
0ed3023b42 Merge pull request #3 from OsmosysSoftware/revert-1-issue-181
Revert "Bookstack grid view."
2017-07-07 17:28:47 +05:30
Abijeet
3fd61a3600 Revert "Bookstack grid view." 2017-07-07 17:28:34 +05:30
Nilesh Deepak
a663fc8aa8 Merge pull request #1 from OsmosysSoftware/issue-181
Bookstack grid view issue 181.
2017-07-07 17:08:19 +05:30
Nilesh Deepak
d84315fff8 Indentation correction. 2017-07-07 17:06:08 +05:30
Nilesh Deepak
144a6e469d Updated cover image upload and delete function. 2017-07-07 16:29:38 +05:30
Nilesh Deepak
c5f11e4516 Fixed pagination on change of display type. 2017-07-06 10:05:11 +05:30
Nilesh Deepak
16a09e8ff6 Deletion of image file on book deletion. 2017-07-06 10:03:40 +05:30
Nilesh Deepak
f51db4b9f6 Resolved responsiveness issues 2017-07-05 19:58:52 +05:30
Nilesh Deepak
6ad24a6bee Changed public getImageURL function to private. 2017-07-05 18:32:38 +05:30
Nilesh Deepak
5b736c3b36 Updated views to support different languages. 2017-07-05 16:12:29 +05:30
Nilesh Deepak
cc553cc93d Added labels for 'Thumbnail toggle' and 'Cover image' in different languages. 2017-07-05 16:11:15 +05:30
Nilesh Deepak
e88a06291e Updated toggle thumbnails function. 2017-07-05 16:09:20 +05:30
Nilesh Deepak
6eccb3d5b9 Adding new migration. 2017-07-05 16:08:04 +05:30
Nilesh Deepak
026de8c5ca Thumbnail toggle function. 2017-07-05 12:48:41 +05:30
Nilesh Deepak
e10d4b91cf styles.scss 2017-07-05 12:36:26 +05:30
Nilesh Deepak
d089eaf754 Changes in User edit profile page. 2017-07-05 12:32:39 +05:30
Nilesh Deepak
bb2d85965f Removed duplicated styles. 2017-07-05 12:29:16 +05:30
Nilesh Deepak
d99fd1fd65 Applied required changes 2017-07-05 12:26:02 +05:30
Nilesh Deepak
947c58f227 Applied required changes in BookStack. 2017-07-05 12:09:01 +05:30
Nilesh Deepak
bce5fdd5cd Merge branch 'master' into issue-181 2017-07-04 15:16:46 +05:30
Nilesh Deepak
fdf139edb2 Changing column size for responsiveness 2017-07-04 15:04:57 +05:30
Nilesh Deepak
af72f0d490 Bookstack grid view. 2017-06-29 18:54:04 +05:30
Nilesh Deepak
8924618d12 test 2017-06-28 18:56:17 +05:30
Nilesh Deepak
6557fbb666 commit 2017-06-28 18:51:32 +05:30
175 changed files with 13069 additions and 3833 deletions

View File

@@ -46,6 +46,9 @@ GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
OKTA_BASE_URL=false
OKTA_KEY=false
OKTA_SECRET=false
# External services such as Gravatar
DISABLE_EXTERNAL_SERVICES=false

View File

@@ -4,10 +4,18 @@ Desired Feature:
### For Bug Reports
* BookStack Version:
* BookStack Version *(Found in settings, Please don't put 'latest')*:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Actual Behavior
##### Current Behavior
##### Steps to Reproduce

7
.gitignore vendored
View File

@@ -2,8 +2,10 @@
/node_modules
Homestead.yaml
.env
/public/dist
.idea
npm-debug.log
yarn-error.log
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
@@ -18,5 +20,4 @@ yarn.lock
nbproject
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs
.settings/

View File

@@ -2,7 +2,8 @@ dist: trusty
sudo: false
language: php
php:
- 7.0.7
- 7.0.20
- 7.1.9
cache:
directories:
@@ -14,7 +15,6 @@ before_script:
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- mysql -u root -e "FLUSH PRIVILEGES;"
- phpenv config-rm xdebug.ini
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- php artisan clear-compiled -n
- php artisan optimize -n

View File

@@ -3,7 +3,7 @@
class Book extends Entity
{
protected $fillable = ['name', 'description'];
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the url for this book.
@@ -18,6 +18,33 @@ class Book extends Entity
return baseUrl('/books/' . urlencode($this->slug));
}
/**
* Returns book cover image, if book cover not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) return $default;
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/*
* Get the edit url for this book.
* @return string

View File

@@ -19,7 +19,7 @@ class RegenerateSearch extends Command
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Re-index all content for searching';
protected $searchService;

View File

@@ -11,12 +11,7 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
Commands\ClearViews::class,
Commands\ClearActivity::class,
Commands\ClearRevisions::class,
Commands\RegeneratePermissions::class,
Commands\RegenerateSearch::class,
Commands\UpgradeDatabaseEncoding::class
//
];
/**
@@ -29,4 +24,14 @@ class Kernel extends ConsoleKernel
{
//
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
}
}

View File

@@ -26,10 +26,10 @@ class Handler extends ExceptionHandler
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $e
* @return mixed
*/
public function report(Exception $e)
{
@@ -103,4 +103,16 @@ class Handler extends ExceptionHandler
return redirect()->guest('login');
}
/**
* Convert a validation exception into a JSON response.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Validation\ValidationException $exception
* @return \Illuminate\Http\JsonResponse
*/
protected function invalidJson($request, ValidationException $exception)
{
return response()->json($exception->errors(), $exception->status);
}
}

View File

@@ -72,13 +72,13 @@ class LoginController extends Controller
// Explicitly log them out for now if they do no exist.
if (!$user->exists) auth()->logout($user);
if (!$user->exists && $user->email === null && !$request->has('email')) {
if (!$user->exists && $user->email === null && !$request->filled('email')) {
$request->flash();
session()->flash('request-email', true);
return redirect('/login');
}
if (!$user->exists && $user->email === null && $request->has('email')) {
if (!$user->exists && $user->email === null && $request->filled('email')) {
$user->email = $request->get('email');
}
@@ -102,12 +102,21 @@ class LoginController extends Controller
/**
* Show the application login form.
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function getLogin()
public function getLogin(Request $request)
{
$socialDrivers = $this->socialAuthService->getActiveDrivers();
$authMethod = config('auth.method');
if ($request->has('email')) {
session()->flashInput([
'email' => $request->get('email'),
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
]);
}
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}

View File

@@ -53,7 +53,7 @@ class RegisterController extends Controller
*/
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
$this->socialAuthService = $socialAuthService;
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
@@ -250,15 +250,27 @@ class RegisterController extends Controller
/**
* The callback for social login services.
* @param $socialDriver
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws ConfirmationEmailException
*/
public function socialCallback($socialDriver)
public function socialCallback($socialDriver, Request $request)
{
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
}
// Check request for error information
if ($request->has('error') && $request->has('error_description')) {
throw new SocialSignInException(trans('errors.social_login_bad_response', [
'socialAccount' => $socialDriver,
'error' => $request->get('error_description'),
]), '/login');
}
$action = session()->pull('social-callback');
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
@@ -279,7 +291,9 @@ class RegisterController extends Controller
* Register a new user after a registration callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws ConfirmationEmailException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
protected function socialRegisterCallback($socialDriver)
{

View File

@@ -40,12 +40,14 @@ class BookController extends Controller
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$this->setPageTitle('Books');
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', 'list');
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new
'new' => $new,
'booksViewType' => $booksViewType
]);
}
@@ -125,9 +127,9 @@ class BookController extends Controller
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
/**
@@ -183,7 +185,7 @@ class BookController extends Controller
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->has('sort-tree')) {
if (!$request->filled('sort-tree')) {
return redirect($book->getUrl());
}

View File

@@ -54,6 +54,7 @@ class HomeController extends Controller
/**
* Get a js representation of the current translations
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
* @throws \Exception
*/
public function getTranslations() {
$locale = app()->getLocale();
@@ -86,4 +87,13 @@ class HomeController extends Controller
]);
}
/**
* Get custom head HTML, Used in ajax calls to show in editor.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function customHeadContent()
{
return view('partials/custom-head-content');
}
}

View File

@@ -107,7 +107,7 @@ class ImageController extends Controller
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
$uploadedTo = $request->filled('uploaded_to') ? $request->get('uploaded_to') : 0;
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
@@ -162,7 +162,7 @@ class ImageController extends Controller
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
$isForced = in_array($request->get('force', ''), [true, 'true']);
if (!$isForced) {
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {

View File

@@ -158,17 +158,25 @@ class PageController extends Controller
$this->checkOwnablePermission('page-view', $page);
$pageContent = $this->entityRepo->renderPage($page);
$page->html = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($pageContent);
$page->load(['comments.createdBy']);
$pageNav = $this->entityRepo->getPageNav($page->html);
// check if the comment's are enabled
$commentsEnabled = !setting('app-disable-comments');
if ($commentsEnabled) {
$page->load(['comments.createdBy']);
}
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav]);
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
]);
}
/**
@@ -324,9 +332,10 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$book = $page->book;
$this->checkOwnablePermission('page-delete', $page);
$this->entityRepo->destroyPage($page);
Activity::addMessage('page_delete', $book->id, $page->name);
session()->flash('success', trans('entities.pages_delete_success'));
$this->entityRepo->destroyPage($page);
return redirect($book->getUrl());
}
@@ -441,6 +450,7 @@ class PageController extends Controller
public function exportPdf($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
@@ -457,6 +467,7 @@ class PageController extends Controller
public function exportHtml($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',

View File

@@ -36,7 +36,7 @@ class SearchController extends Controller
$searchTerm = $request->get('term');
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
@@ -88,8 +88,8 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = $request->get('term', false);
// Search for entities otherwise show most popular
if ($searchTerm !== false) {

View File

@@ -37,7 +37,7 @@ class TagController extends Controller
*/
public function getNameSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$searchTerm = $request->get('search', false);
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
return response()->json($suggestions);
}
@@ -49,8 +49,8 @@ class TagController extends Controller
*/
public function getValueSuggestions(Request $request)
{
$searchTerm = $request->has('search') ? $request->get('search') : false;
$tagName = $request->has('name') ? $request->get('name') : false;
$searchTerm = $request->get('search', false);
$tagName = $request->get('name', false);
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}

View File

@@ -34,9 +34,9 @@ class UserController extends Controller
{
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->has('order') ? $request->get('order') : 'asc',
'search' => $request->has('search') ? $request->get('search') : '',
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
'order' => $request->get('order', 'asc'),
'search' => $request->get('search', ''),
'sort' => $request->get('sort', 'name'),
];
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
@@ -88,7 +88,7 @@ class UserController extends Controller
$user->save();
if ($request->has('roles')) {
if ($request->filled('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
@@ -155,24 +155,24 @@ class UserController extends Controller
$user->fill($request->all());
// Role updates
if (userCan('users-manage') && $request->has('roles')) {
if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles');
$user->roles()->sync($roles);
}
// Password updates
if ($request->has('password') && $request->get('password') != '') {
if ($request->filled('password')) {
$password = $request->get('password');
$user->password = bcrypt($password);
}
// External auth id updates
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
if ($request->has('setting')) {
if ($request->filled('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}

View File

@@ -13,8 +13,9 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\BookStack\Http\Middleware\TrimStrings::class,
\BookStack\Http\Middleware\TrustProxies::class,
];
/**
@@ -26,6 +27,8 @@ class Kernel extends HttpKernel
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
@@ -42,10 +45,11 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth' => \BookStack\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
];
}

View File

@@ -30,8 +30,11 @@ class Authenticate
*/
public function handle($request, Closure $next)
{
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
return redirect(baseUrl('/register/confirm/awaiting'));
if ($this->auth->check()) {
$requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict'));
if ($requireConfirmation && !$this->auth->user()->email_confirmed) {
return redirect('/register/confirm/awaiting');
}
}
if ($this->auth->guest() && !setting('app-public')) {

View File

@@ -2,9 +2,9 @@
namespace BookStack\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends BaseEncrypter
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.

View File

@@ -0,0 +1,19 @@
<?php
namespace BookStack\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array
*/
protected $except = [
'password',
'password_confirmation',
'password-confirm',
];
}

View File

@@ -0,0 +1,47 @@
<?php
namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Fideloper\Proxy\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array
*/
protected $proxies;
/**
* The current proxy header mappings.
*
* @var array
*/
protected $headers = [
Request::HEADER_FORWARDED => 'FORWARDED',
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
];
/**
* Handle the request, Set the correct user-configured proxy information.
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$setProxies = config('app.proxies');
if ($setProxies !== '**' && $setProxies !== '*' && $setProxies !== '') {
$setProxies = explode(',', $setProxies);
}
$this->proxies = $setProxies;
return parent::handle($request, $next);
}
}

View File

@@ -2,9 +2,9 @@
namespace BookStack\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends BaseVerifier
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.

View File

@@ -16,6 +16,8 @@ class EventServiceProvider extends ServiceProvider
protected $listen = [
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
'SocialiteProviders\Azure\AzureExtendSocialite@handle',
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
],
];

View File

@@ -4,6 +4,7 @@ use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Page;
use BookStack\PageRevision;
use BookStack\Services\AttachmentService;
@@ -441,9 +442,10 @@ class EntityRepo
*/
public function updateEntityPermissionsFromRequest($request, Entity $entity)
{
$entity->restricted = $request->has('restricted') && $request->get('restricted') === 'true';
$entity->restricted = $request->get('restricted', '') === 'true';
$entity->permissions()->delete();
if ($request->has('restrictions')) {
if ($request->filled('restrictions')) {
foreach ($request->get('restrictions') as $roleId => $restrictions) {
foreach ($restrictions as $action => $value) {
$entity->permissions()->create([
@@ -453,6 +455,7 @@ class EntityRepo
}
}
}
$entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
}
@@ -552,8 +555,9 @@ class EntityRepo
*/
protected function nameToSlug($name)
{
$slug = str_replace(' ', '-', strtolower($name));
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', $slug);
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5);
return $slug;
}
@@ -716,7 +720,6 @@ class EntityRepo
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
$page->setAttribute('renderedHTML', $content);
return $content;
}
@@ -1074,6 +1077,7 @@ class EntityRepo
/**
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
*/
public function destroyPage(Page $page)
{
@@ -1085,6 +1089,12 @@ class EntityRepo
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {

View File

@@ -127,7 +127,7 @@ class ExportService
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
$pdf = \DomPDF::loadHTML($containedHtml);
}
return $pdf->output();
}
@@ -136,6 +136,7 @@ class ExportService
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @return mixed|string
* @throws \Exception
*/
protected function containHtml($htmlContent)
{
@@ -153,9 +154,27 @@ class ExportService
} else {
$pathString = $srcString;
}
// Attempt to find local files even if url not absolute
$base = baseUrl('/');
if (strpos($srcString, $base) === 0) {
$isLocal = true;
$relString = str_replace($base, '', $srcString);
$pathString = public_path(trim($relString, '/'));
}
if ($isLocal && !file_exists($pathString)) continue;
try {
$imageContent = file_get_contents($pathString);
if ($isLocal) {
$imageContent = file_get_contents($pathString);
} else {
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageContent = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) throw new \Exception("Image fetch failed, Received error: " . $err);
}
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {

View File

@@ -205,12 +205,17 @@ class PermissionService
}
$entities[] = $entity->book;
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity->isA('chapter')) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities));
}

View File

@@ -382,11 +382,13 @@ class SearchService
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitText = explode(' ', $text);
foreach ($splitText as $token) {
if ($token === '') continue;
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
@@ -479,4 +481,23 @@ class SearchService
});
}
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$functionName = camel_case('sort_by_' . $input);
if (method_exists($this, $functionName)) $this->$functionName($query, $model);
}
/**
* Sorting filter options
*/
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
{
$commentsTable = $this->db->getTablePrefix() . 'comments';
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
$commentQuery = $this->db->raw('(SELECT c1.entity_id, c1.entity_type, c1.created_at as last_commented FROM '.$commentsTable.' c1 LEFT JOIN '.$commentsTable.' c2 ON (c1.entity_id = c2.entity_id AND c1.entity_type = c2.entity_type AND c1.created_at < c2.created_at) WHERE c1.entity_type = \''. $morphClass .'\' AND c2.created_at IS NULL) as comments');
$query->join($commentQuery, $model->getTable() . '.id', '=', 'comments.entity_id')->orderBy('last_commented', 'desc');
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Http\Requests\Request;
use GuzzleHttp\Exception\ClientException;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
@@ -14,7 +16,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta'];
/**
* SocialAuthService constructor.
@@ -91,7 +93,6 @@ class SocialAuthService
public function handleLoginCallback($socialDriver)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
$socialId = $socialUser->getId();
@@ -104,7 +105,8 @@ class SocialAuthService
// When a user is not logged in and a matching SocialAccount exists,
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
return $this->logUserIn($socialAccount->user);
auth()->login($socialAccount->user);
return redirect()->intended('/');
}
// When a user is logged in but the social account does not exist,
@@ -134,14 +136,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message . '.', '/login');
}
private function logUserIn($user)
{
auth()->login($user);
return redirect('/');
throw new SocialSignInException($message, '/login');
}
/**

16
artisan
View File

@@ -1,19 +1,19 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
| Initialize The App
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
| We need to get things going before we start up the app.
| The init file loads everything in, in the correct order.
|
*/
require __DIR__.'/bootstrap/autoload.php';
require __DIR__.'/bootstrap/init.php';
$app = require_once __DIR__.'/bootstrap/app.php';
@@ -40,7 +40,7 @@ $status = $kernel->handle(
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running. We will fire off the shutdown events
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
@@ -48,4 +48,4 @@ $status = $kernel->handle(
$kernel->terminate($input, $status);
exit($status);
exit($status);

View File

@@ -1,6 +1,15 @@
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Load Our Own Helpers
|--------------------------------------------------------------------------
|
| This custom function loads any helpers, before the Laravel Framework
| is built so we can override any helpers as we please.
|
*/
require __DIR__.'/../app/helpers.php';
/*
|--------------------------------------------------------------------------
@@ -13,23 +22,4 @@ define('LARAVEL_START', microtime(true));
| loading of any our classes "manually". Feels great to relax.
|
*/
require __DIR__.'/../app/helpers.php';
require __DIR__.'/../vendor/autoload.php';
/*
|--------------------------------------------------------------------------
| Include The Compiled Class File
|--------------------------------------------------------------------------
|
| To dramatically increase your application's performance, you may use a
| compiled class file which contains all of the classes commonly used
| by a request. The Artisan "optimize" is used to create this file.
|
*/
$compiledPath = __DIR__.'/cache/compiled.php';
if (file_exists($compiledPath)) {
require $compiledPath;
}
require __DIR__.'/../vendor/autoload.php';

View File

@@ -1,31 +1,35 @@
{
"name": "ssddanbrown/bookstack",
"name": "bookstackapp/bookstack",
"description": "BookStack documentation platform",
"keywords": ["BookStack", "Documentation"],
"license": "MIT",
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "5.4.*",
"php": ">=7.0.0",
"laravel/framework": "5.5.*",
"fideloper/proxy": "~3.3",
"ext-tidy": "*",
"intervention/image": "^2.3",
"intervention/image": "^2.4",
"laravel/socialite": "^3.0",
"barryvdh/laravel-ide-helper": "^2.2.3",
"barryvdh/laravel-debugbar": "^2.3.2",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0",
"socialiteproviders/slack": "^3.0"
"barryvdh/laravel-snappy": "^0.4.0",
"socialiteproviders/slack": "^3.0",
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0"
},
"require-dev": {
"filp/whoops": "~2.0",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "0.9.*",
"phpunit/phpunit": "~5.0",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0",
"symfony/css-selector": "3.1.*",
"symfony/dom-crawler": "3.1.*"
"symfony/dom-crawler": "3.1.*",
"laravel/browser-kit-testing": "^2.0",
"barryvdh/laravel-ide-helper": "^2.4.1",
"barryvdh/laravel-debugbar": "^3.1.0"
},
"autoload": {
"classmap": [
@@ -56,14 +60,12 @@
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize",
"php artisan cache:clear",
"php artisan view:clear"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
],
"refresh-test-database": [
"php artisan migrate:refresh --database=mysql_testing",
@@ -71,6 +73,10 @@
]
},
"config": {
"preferred-install": "dist"
"optimize-autoloader": true,
"preferred-install": "dist",
"platform": {
"php": "7.0"
}
}
}

5864
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it'],
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl', 'it', 'ru'],
/*
|--------------------------------------------------------------------------
@@ -219,7 +219,7 @@ return [
*/
'ImageTool' => Intervention\Image\Facades\Image::class,
'PDF' => Barryvdh\DomPDF\Facade::class,
'DomPDF' => Barryvdh\DomPDF\Facade::class,
'SnappyPDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
@@ -234,4 +234,6 @@ return [
],
'proxies' => env('APP_PROXIES', ''),
];

View File

@@ -72,6 +72,22 @@ return [
'name' => 'Twitter',
],
'azure' => [
'client_id' => env('AZURE_APP_ID', false),
'client_secret' => env('AZURE_APP_SECRET', false),
'tenant' => env('AZURE_TENANT', false),
'redirect' => env('APP_URL') . '/login/service/azure/callback',
'name' => 'Microsoft Azure',
],
'okta' => [
'client_id' => env('OKTA_APP_ID'),
'client_secret' => env('OKTA_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/okta/callback',
'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),
'dn' => env('LDAP_DN', false),

View File

@@ -29,7 +29,7 @@ return [
|
*/
'lifetime' => 120,
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddCoverImageDisplay extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('books', function (Blueprint $table) {
$table->integer('image_id')->nullable()->default(null);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('books', function (Blueprint $table) {
$table->dropColumn('image_id');
});
}
}

View File

@@ -11,25 +11,31 @@ class DummyContentSeeder extends Seeder
*/
public function run()
{
$user = factory(\BookStack\User::class)->create();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
// Create an editor user
$editorUser = factory(\BookStack\User::class)->create();
$editorRole = \BookStack\Role::getRole('editor');
$editorUser->attachRole($editorRole);
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
// Create a viewer user
$viewerUser = factory(\BookStack\User::class)->create();
$role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role);
factory(\BookStack\Book::class, 20)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($book) use ($editorUser) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($chapter) use ($editorUser, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $user->id, 'updated_by' => $user->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();

5977
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -25,11 +25,6 @@
"yargs": "^7.1.0"
},
"dependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.24.1",

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
bootstrap="bootstrap/init.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,22 +4,22 @@
* Laravel - A PHP Framework For Web Artisans
*
* @package Laravel
* @author Taylor Otwell <taylorotwell@gmail.com>
* @author Taylor Otwell <taylor@laravel.com>
*/
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
| Initialize The App
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader for
| our application. We just need to utilize it! We'll simply require it
| into the script here so that we don't have to worry about manual
| loading any of our classes later on. It feels nice to relax.
| We need to get things going before we start up the app.
| The init file loads everything in, in the correct order.
|
*/
require __DIR__.'/../bootstrap/autoload.php';
require __DIR__.'/../bootstrap/init.php';
/*
|--------------------------------------------------------------------------
@@ -55,4 +55,4 @@ $response = $kernel->handle(
$response->send();
$kernel->terminate($request, $response);
$kernel->terminate($request, $response);

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("advlist",function(a){function c(b){return a.$.contains(a.getBody(),b)}function d(a){return a&&/^(OL|UL|DL)$/.test(a.nodeName)&&c(a)}function e(a,c){var d=[];return c&&b.each(c.split(/[ ,]/),function(a){d.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),d}function f(c,d){a.undoManager.transact(function(){var e,f=a.dom,g=a.selection;if(e=f.getParent(g.getNode(),"ol,ul"),!e||e.nodeName!=c||d===!1){var h={"list-style-type":d?d:""};a.execCommand("UL"==c?"InsertUnorderedList":"InsertOrderedList",!1,h)}e=f.getParent(g.getNode(),"ol,ul"),e&&b.each(f.select("ol,ul",e).concat([e]),function(a){a.nodeName!==c&&d!==!1&&(a=f.rename(a,c)),f.setStyle(a,"listStyleType",d?d:null),a.removeAttribute("data-mce-style")}),a.focus()})}function g(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var h,i,j=function(a,c){var d=a.settings.plugins?a.settings.plugins:"";return b.inArray(d.split(/[ ,]/),c)!==-1};h=e("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),i=e("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square"));var k=function(c){return function(){var e=this;a.on("NodeChange",function(a){var f=b.grep(a.parents,d);e.active(f.length>0&&f[0].nodeName===c)})}};j(a,"lists")&&(a.addCommand("ApplyUnorderedListStyle",function(a,b){f("UL",b["list-style-type"])}),a.addCommand("ApplyOrderedListStyle",function(a,b){f("OL",b["list-style-type"])}),a.addButton("numlist",{type:h.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:h,onPostRender:k("OL"),onshow:g,onselect:function(a){f("OL",a.control.settings.data)},onclick:function(){f("OL",!1)}}),a.addButton("bullist",{type:i.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:k("UL"),menu:i,onshow:g,onselect:function(a){f("UL",a.control.settings.data)},onclick:function(){f("UL",!1)}}))}),function(){}}),d("0")()}();
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("advlist",function(a){function c(b){return a.$.contains(a.getBody(),b)}function d(a){return a&&/^(OL|UL|DL)$/.test(a.nodeName)&&c(a)}function e(a,c){var d=[];return c&&b.each(c.split(/[ ,]/),function(a){d.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),d}function f(b,c){var d="UL"==b?"InsertUnorderedList":"InsertOrderedList";a.execCommand(d,!1,c===!1?null:{"list-style-type":c})}function g(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var h,i,j=function(a,c){var d=a.settings.plugins?a.settings.plugins:"";return b.inArray(d.split(/[ ,]/),c)!==-1};h=e("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),i=e("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square"));var k=function(c){return function(){var e=this;a.on("NodeChange",function(a){var f=b.grep(a.parents,d);e.active(f.length>0&&f[0].nodeName===c)})}};j(a,"lists")&&(a.addCommand("ApplyUnorderedListStyle",function(a,b){f("UL",b["list-style-type"])}),a.addCommand("ApplyOrderedListStyle",function(a,b){f("OL",b["list-style-type"])}),a.addButton("numlist",{type:h.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:h,onPostRender:k("OL"),onshow:g,onselect:function(a){f("OL",a.control.settings.data)},onclick:function(){a.execCommand("InsertOrderedList")}}),a.addButton("bullist",{type:i.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:k("UL"),menu:i,onshow:g,onselect:function(a){f("UL",a.control.settings.data)},onclick:function(){a.execCommand("InsertUnorderedList")}}))}),function(){}}),d("0")()}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("autolink",function(b){function c(a){f(a,-1,"(",!0)}function d(a){f(a,0,"",!0)}function e(a){f(a,-1,"",!1)}function f(a,b,c){function d(a,b){if(b<0&&(b=0),3==a.nodeType){var c=a.data.length;b>c&&(b=c)}return b}function e(a,b){1!=a.nodeType||a.hasChildNodes()?g.setStart(a,d(a,b)):g.setStartBefore(a)}function f(a,b){1!=a.nodeType||a.hasChildNodes()?g.setEnd(a,d(a,b)):g.setEndAfter(a)}var g,i,j,k,l,m,n,o,p,q;if("A"!=a.selection.getNode().tagName){if(g=a.selection.getRng(!0).cloneRange(),g.startOffset<5){if(o=g.endContainer.previousSibling,!o){if(!g.endContainer.firstChild||!g.endContainer.firstChild.nextSibling)return;o=g.endContainer.firstChild.nextSibling}if(p=o.length,e(o,p),f(o,p),g.endOffset<5)return;i=g.endOffset,k=o}else{if(k=g.endContainer,3!=k.nodeType&&k.firstChild){for(;3!=k.nodeType&&k.firstChild;)k=k.firstChild;3==k.nodeType&&(e(k,0),f(k,k.nodeValue.length))}i=1==g.endOffset?2:g.endOffset-1-b}j=i;do e(k,i>=2?i-2:0),f(k,i>=1?i-1:0),i-=1,q=g.toString();while(" "!=q&&""!==q&&160!=q.charCodeAt(0)&&i-2>=0&&q!=c);g.toString()==c||160==g.toString().charCodeAt(0)?(e(k,i),f(k,j),i+=1):0===g.startOffset?(e(k,0),f(k,j)):(e(k,i),f(k,j)),m=g.toString(),"."==m.charAt(m.length-1)&&f(k,j-1),m=g.toString(),n=m.match(h),n&&("www."==n[1]?n[1]="http://www.":/@$/.test(n[1])&&!/^mailto:/.test(n[1])&&(n[1]="mailto:"+n[1]),l=a.selection.getBookmark(),a.selection.setRng(g),a.execCommand("createlink",!1,n[1]+n[2]),a.settings.default_link_target&&a.dom.setAttrib(a.selection.getNode(),"target",a.settings.default_link_target),a.selection.moveToBookmark(l),a.nodeChanged())}}var g,h=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return b.settings.autolink_pattern&&(h=b.settings.autolink_pattern),b.on("keydown",function(a){if(13==a.keyCode)return e(b)}),a.ie?void b.on("focus",function(){if(!g){g=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41==a.keyCode)return c(b)}),void b.on("keyup",function(a){if(32==a.keyCode)return d(b)}))}),function(){}}),d("0")()}();
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){var c=function(a,b){return a===b||" "===a||160===a.charCodeAt(0)};return b.add("autolink",function(b){function d(a){g(a,-1,"(",!0)}function e(a){g(a,0,"",!0)}function f(a){g(a,-1,"",!1)}function g(a,b,d){function e(a,b){if(b<0&&(b=0),3==a.nodeType){var c=a.data.length;b>c&&(b=c)}return b}function f(a,b){1!=a.nodeType||a.hasChildNodes()?h.setStart(a,e(a,b)):h.setStartBefore(a)}function g(a,b){1!=a.nodeType||a.hasChildNodes()?h.setEnd(a,e(a,b)):h.setEndAfter(a)}var h,j,k,l,m,n,o,p,q,r;if("A"!=a.selection.getNode().tagName){if(h=a.selection.getRng(!0).cloneRange(),h.startOffset<5){if(p=h.endContainer.previousSibling,!p){if(!h.endContainer.firstChild||!h.endContainer.firstChild.nextSibling)return;p=h.endContainer.firstChild.nextSibling}if(q=p.length,f(p,q),g(p,q),h.endOffset<5)return;j=h.endOffset,l=p}else{if(l=h.endContainer,3!=l.nodeType&&l.firstChild){for(;3!=l.nodeType&&l.firstChild;)l=l.firstChild;3==l.nodeType&&(f(l,0),g(l,l.nodeValue.length))}j=1==h.endOffset?2:h.endOffset-1-b}k=j;do f(l,j>=2?j-2:0),g(l,j>=1?j-1:0),j-=1,r=h.toString();while(" "!=r&&""!==r&&160!=r.charCodeAt(0)&&j-2>=0&&r!=d);c(h.toString(),d)?(f(l,j),g(l,k),j+=1):0===h.startOffset?(f(l,0),g(l,k)):(f(l,j),g(l,k)),n=h.toString(),"."==n.charAt(n.length-1)&&g(l,k-1),n=h.toString(),o=n.match(i),o&&("www."==o[1]?o[1]="http://www.":/@$/.test(o[1])&&!/^mailto:/.test(o[1])&&(o[1]="mailto:"+o[1]),m=a.selection.getBookmark(),a.selection.setRng(h),a.execCommand("createlink",!1,o[1]+o[2]),a.settings.default_link_target&&a.dom.setAttrib(a.selection.getNode(),"target",a.settings.default_link_target),a.selection.moveToBookmark(m),a.nodeChanged())}}var h,i=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return b.settings.autolink_pattern&&(i=b.settings.autolink_pattern),b.on("keydown",function(a){if(13==a.keyCode)return f(b)}),a.ie?void b.on("focus",function(){if(!h){h=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41==a.keyCode)return d(b)}),void b.on("keyup",function(a){if(32==a.keyCode)return e(b)}))}),function(){}}),d("0")()}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.EditorManager")}),g("2",["6"],function(a){return a("tinymce.PluginManager")}),g("3",["6"],function(a){return a("tinymce.util.LocalStorage")}),g("4",["6"],function(a){return a("tinymce.util.Tools")}),h("5",window),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return a._beforeUnloadHandler=function(){var b;return d.each(a.editors,function(a){a.plugins.autosave&&a.plugins.autosave.storeDraft(),!b&&a.isDirty()&&a.getParam("autosave_ask_before_unload",!0)&&(b=a.translate("You have unsaved changes are you sure you want to navigate away?"))}),b},b.add("autosave",function(b){function f(a,b){var c={s:1e3,m:6e4};return a=/^(\d+)([ms]?)$/.exec(""+(a||b)),(a[2]?c[a[2]]:1)*parseInt(a,10)}function g(){var a=parseInt(c.getItem(o+"time"),10)||0;return!((new Date).getTime()-a>q.autosave_retention)||(h(!1),!1)}function h(a){c.removeItem(o+"draft"),c.removeItem(o+"time"),a!==!1&&b.fire("RemoveDraft")}function i(){!n()&&b.isDirty()&&(c.setItem(o+"draft",b.getContent({format:"raw",no_events:!0})),c.setItem(o+"time",(new Date).getTime()),b.fire("StoreDraft"))}function j(){g()&&(b.setContent(c.getItem(o+"draft"),{format:"raw"}),b.fire("RestoreDraft"))}function k(){p||(setInterval(function(){b.removed||i()},q.autosave_interval),p=!0)}function l(){var a=this;a.disabled(!g()),b.on("StoreDraft RestoreDraft RemoveDraft",function(){a.disabled(!g())}),k()}function m(){b.undoManager.beforeChange(),j(),h(),b.undoManager.add()}function n(a){var c=b.settings.forced_root_block;return a=d.trim("undefined"==typeof a?b.getBody().innerHTML:a),""===a||new RegExp("^<"+c+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+c+">|<br>$","i").test(a)}var o,p,q=b.settings;o=q.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",o=o.replace(/\{path\}/g,document.location.pathname),o=o.replace(/\{query\}/g,document.location.search),o=o.replace(/\{id\}/g,b.id),q.autosave_interval=f(q.autosave_interval,"30s"),q.autosave_retention=f(q.autosave_retention,"20m"),b.addButton("restoredraft",{title:"Restore last draft",onclick:m,onPostRender:l}),b.addMenuItem("restoredraft",{text:"Restore last draft",onclick:m,onPostRender:l,context:"file"}),b.settings.autosave_restore_when_empty!==!1&&(b.on("init",function(){g()&&n()&&j()}),b.on("saveContent",function(){h()})),e.onbeforeunload=a._beforeUnloadHandler,this.hasDraft=g,this.storeDraft=i,this.restoreDraft=j,this.removeDraft=h,this.isEmpty=n}),function(){}}),d("0")()}();
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.EditorManager")}),g("2",["6"],function(a){return a("tinymce.PluginManager")}),g("3",["6"],function(a){return a("tinymce.util.LocalStorage")}),g("4",["6"],function(a){return a("tinymce.util.Tools")}),h("5",window),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return a._beforeUnloadHandler=function(){var b;return d.each(a.get(),function(a){a.plugins.autosave&&a.plugins.autosave.storeDraft(),!b&&a.isDirty()&&a.getParam("autosave_ask_before_unload",!0)&&(b=a.translate("You have unsaved changes are you sure you want to navigate away?"))}),b},b.add("autosave",function(b){function f(a,b){var c={s:1e3,m:6e4};return a=/^(\d+)([ms]?)$/.exec(""+(a||b)),(a[2]?c[a[2]]:1)*parseInt(a,10)}function g(){var a=parseInt(c.getItem(o+"time"),10)||0;return!((new Date).getTime()-a>q.autosave_retention)||(h(!1),!1)}function h(a){c.removeItem(o+"draft"),c.removeItem(o+"time"),a!==!1&&b.fire("RemoveDraft")}function i(){!n()&&b.isDirty()&&(c.setItem(o+"draft",b.getContent({format:"raw",no_events:!0})),c.setItem(o+"time",(new Date).getTime()),b.fire("StoreDraft"))}function j(){g()&&(b.setContent(c.getItem(o+"draft"),{format:"raw"}),b.fire("RestoreDraft"))}function k(){p||(setInterval(function(){b.removed||i()},q.autosave_interval),p=!0)}function l(){var a=this;a.disabled(!g()),b.on("StoreDraft RestoreDraft RemoveDraft",function(){a.disabled(!g())}),k()}function m(){b.undoManager.beforeChange(),j(),h(),b.undoManager.add()}function n(a){var c=b.settings.forced_root_block;return a=d.trim("undefined"==typeof a?b.getBody().innerHTML:a),""===a||new RegExp("^<"+c+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+c+">|<br>$","i").test(a)}var o,p,q=b.settings;o=q.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",o=o.replace(/\{path\}/g,document.location.pathname),o=o.replace(/\{query\}/g,document.location.search),o=o.replace(/\{id\}/g,b.id),q.autosave_interval=f(q.autosave_interval,"30s"),q.autosave_retention=f(q.autosave_retention,"20m"),b.addButton("restoredraft",{title:"Restore last draft",onclick:m,onPostRender:l}),b.addMenuItem("restoredraft",{text:"Restore last draft",onclick:m,onPostRender:l,context:"file"}),b.settings.autosave_restore_when_empty!==!1&&(b.on("init",function(){g()&&n()&&j()}),b.on("saveContent",function(){h()})),e.onbeforeunload=a._beforeUnloadHandler,this.hasDraft=g,this.storeDraft=i,this.restoreDraft=j,this.removeDraft=h,this.isEmpty=n}),function(){}}),d("0")()}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("2",tinymce.util.Tools.resolve),g("1",["2"],function(a){return a("tinymce.PluginManager")}),g("0",["1"],function(a){return a.add("nonbreaking",function(a){var b=a.getParam("nonbreaking_force_tab");if(a.addCommand("mceNonBreaking",function(){a.insertContent(a.plugins.visualchars&&a.plugins.visualchars.state?'<span class="mce-nbsp">&nbsp;</span>':"&nbsp;"),a.dom.setAttrib(a.dom.select("span.mce-nbsp"),"data-mce-bogus","1")}),a.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),a.addMenuItem("nonbreaking",{text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"}),b){var c=+b>1?+b:3;a.on("keydown",function(b){if(9==b.keyCode){if(b.shiftKey)return;b.preventDefault();for(var d=0;d<c;d++)a.execCommand("mceNonBreaking")}})}}),function(){}}),d("0")()}();
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("2",tinymce.util.Tools.resolve),g("1",["2"],function(a){return a("tinymce.PluginManager")}),g("0",["1"],function(a){return a.add("nonbreaking",function(a){var b=a.getParam("nonbreaking_force_tab"),c=function(a,b){for(var c="",d=0;d<b;d++)c+=a;return c},d=function(b){var d=a.plugins.visualchars&&a.plugins.visualchars.state?'<span class="mce-nbsp">&nbsp;</span>':"&nbsp;";a.insertContent(c(d,b)),a.dom.setAttrib(a.dom.select("span.mce-nbsp"),"data-mce-bogus","1")};if(a.addCommand("mceNonBreaking",function(){d(1)}),a.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),a.addMenuItem("nonbreaking",{text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"}),b){var e=+b>1?+b:3;a.on("keydown",function(a){if(9==a.keyCode){if(a.shiftKey)return;a.preventDefault(),d(e)}})}}),function(){}}),d("0")()}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("4",tinymce.util.Tools.resolve),g("1",["4"],function(a){return a("tinymce.PluginManager")}),g("2",["4"],function(a){return a("tinymce.Env")}),g("3",["4"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2","3"],function(a,b,c){return a.add("preview",function(a){var d=a.settings,e=!b.ie;a.addCommand("mcePreview",function(){a.windowManager.open({title:"Preview",width:parseInt(a.getParam("plugin_preview_width","650"),10),height:parseInt(a.getParam("plugin_preview_height","500"),10),html:'<iframe src="javascript:\'\'" frameborder="0"'+(e?' sandbox="allow-scripts"':"")+"></iframe>",buttons:{text:"Close",onclick:function(){this.parent().parent().close()}},onPostRender:function(){var b,f="";f+='<base href="'+a.documentBaseURI.getURI()+'">',c.each(a.contentCSS,function(b){f+='<link type="text/css" rel="stylesheet" href="'+a.documentBaseURI.toAbsolute(b)+'">'});var g=d.body_id||"tinymce";g.indexOf("=")!=-1&&(g=a.getParam("body_id","","hash"),g=g[a.id]||g);var h=d.body_class||"";h.indexOf("=")!=-1&&(h=a.getParam("body_class","","hash"),h=h[a.id]||"");var i='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A") {e.preventDefault();}}}, false);</script> ',j=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(b="<!DOCTYPE html><html><head>"+f+'</head><body id="'+g+'" class="mce-content-body '+h+'"'+j+">"+a.getContent()+i+"</body></html>",e)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(b);else{var k=this.getEl("body").firstChild.contentWindow.document;k.open(),k.write(b),k.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})}),function(){}}),d("0")()}();
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("4",tinymce.util.Tools.resolve),g("1",["4"],function(a){return a("tinymce.PluginManager")}),g("2",["4"],function(a){return a("tinymce.Env")}),g("3",["4"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2","3"],function(a,b,c){return a.add("preview",function(a){var d=a.settings,e=!b.ie;a.addCommand("mcePreview",function(){a.windowManager.open({title:"Preview",width:parseInt(a.getParam("plugin_preview_width","650"),10),height:parseInt(a.getParam("plugin_preview_height","500"),10),html:'<iframe src="javascript:\'\'" frameborder="0"'+(e?' sandbox="allow-scripts"':"")+"></iframe>",buttons:{text:"Close",onclick:function(){this.parent().parent().close()}},onPostRender:function(){var b,f="",g=a.dom.encode;f+='<base href="'+g(a.documentBaseURI.getURI())+'">',c.each(a.contentCSS,function(b){f+='<link type="text/css" rel="stylesheet" href="'+g(a.documentBaseURI.toAbsolute(b))+'">'});var h=d.body_id||"tinymce";h.indexOf("=")!=-1&&(h=a.getParam("body_id","","hash"),h=h[a.id]||h);var i=d.body_class||"";i.indexOf("=")!=-1&&(i=a.getParam("body_class","","hash"),i=i[a.id]||"");var j='<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A") {e.preventDefault();}}}, false);</script> ',k=a.settings.directionality?' dir="'+a.settings.directionality+'"':"";if(b="<!DOCTYPE html><html><head>"+f+'</head><body id="'+g(h)+'" class="mce-content-body '+g(i)+'"'+g(k)+">"+a.getContent()+j+"</body></html>",e)this.getEl("body").firstChild.src="data:text/html;charset=utf-8,"+encodeURIComponent(b);else{var l=this.getEl("body").firstChild.contentWindow.document;l.open(),l.write(b),l.close()}}})}),a.addButton("preview",{title:"Preview",cmd:"mcePreview"}),a.addMenuItem("preview",{text:"Preview",cmd:"mcePreview",context:"view"})}),function(){}}),d("0")()}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #F00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#3399ff !important}.mce-edit-focus{outline:1px dotted #333}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1}.mce-content-body p,.mce-content-body div,.mce-content-body h1,.mce-content-body h2,.mce-content-body h3,.mce-content-body h4,.mce-content-body h5,.mce-content-body h6{line-height:1.2em}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7ACAFF}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-content-body a[data-mce-selected],.mce-content-body code[data-mce-selected]{background:#bfe6ff}.mce-content-body hr{cursor:default}
.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #F00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#3399ff !important}.mce-edit-focus{outline:1px dotted #333}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7ACAFF}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-content-body a[data-mce-selected],.mce-content-body code[data-mce-selected],.mce-content-body b[data-mce-selected],.mce-content-body i[data-mce-selected],.mce-content-body em[data-mce-selected],.mce-content-body strong[data-mce-selected],.mce-content-body sup[data-mce-selected],.mce-content-body sub[data-mce-selected]{background:#bfe6ff}.mce-content-body hr{cursor:default}.mce-content-body{line-height:1.3}

View File

@@ -1 +1 @@
body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #F00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#3399ff !important}.mce-edit-focus{outline:1px dotted #333}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1}.mce-content-body p,.mce-content-body div,.mce-content-body h1,.mce-content-body h2,.mce-content-body h3,.mce-content-body h4,.mce-content-body h5,.mce-content-body h6{line-height:1.2em}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7ACAFF}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-content-body a[data-mce-selected],.mce-content-body code[data-mce-selected]{background:#bfe6ff}.mce-content-body hr{cursor:default}
body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;line-height:1.3;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid #F00;cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#3399ff !important}.mce-edit-focus{outline:1px dotted #333}.mce-resize-bar-dragging{background-color:blue;opacity:.25;filter:alpha(opacity=25);zoom:1}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2d8ac7}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #7ACAFF}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2d8ac7}.mce-content-body a[data-mce-selected],.mce-content-body code[data-mce-selected],.mce-content-body b[data-mce-selected],.mce-content-body i[data-mce-selected],.mce-content-body em[data-mce-selected],.mce-content-body strong[data-mce-selected],.mce-content-body sup[data-mce-selected],.mce-content-body sub[data-mce-selected]{background:#bfe6ff}.mce-content-body hr{cursor:default}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -54,8 +54,19 @@ Once done you can run `phpunit` in the application root directory to run all tes
As part of BookStack v0.14 support for translations has been built in. All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
You will also need to add the language to the `locales` array in the `config/app.php` file.
There is a script available which compares translation content to `en` files to see what items are missing or redundant. This can be ran like so from your BookStack install folder:
```bash
# Syntax
php resources/lang/check.php <lang>
# Examples
php resources/lang/check.php fr
php resources/lang/check.php pt_BR
```
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
## Contributing
@@ -76,7 +87,6 @@ The BookStack source is provided under the MIT License.
These are the great open-source projects used to help build BookStack:
* [Laravel](http://laravel.com/)
* [AngularJS](https://angularjs.org/)
* [jQuery](https://jquery.com/)
* [TinyMCE](https://www.tinymce.com/)
* [CodeMirror](https://codemirror.net)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 129 129"><path fill="#f25022" d="M0 0h61.3v61.3H0z"/><path fill="#7fba00" d="M67.7 0H129v61.3H67.7z"/><path fill="#00a4ef" d="M0 67.7h61.3V129H0z"/><path fill="#ffb900" d="M67.7 67.7H129V129H67.7z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm-.035 5.537a6.427 6.427 0 0 1 6.428 6.428 6.427 6.427 0 0 1-6.428 6.428 6.427 6.427 0 0 1-6.428-6.428 6.427 6.427 0 0 1 6.428-6.428z" fill="#007dc1"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,37 @@
/**
* Collapsible
* Provides some simple logic to allow collapsible sections.
*/
class Collapsible {
constructor(elem) {
this.elem = elem;
this.trigger = elem.querySelector('[collapsible-trigger]');
this.content = elem.querySelector('[collapsible-content]');
if (!this.trigger) return;
this.trigger.addEventListener('click', this.toggle.bind(this));
}
open() {
this.elem.classList.add('open');
$(this.content).slideDown(400);
}
close() {
this.elem.classList.remove('open');
$(this.content).slideUp(400);
}
toggle() {
if (this.elem.classList.contains('open')) {
this.close();
} else {
this.open();
}
}
}
module.exports = Collapsible;

View File

@@ -0,0 +1,47 @@
class EditorToolbox {
constructor(elem) {
// Elements
this.elem = elem;
this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
this.toggleButton = elem.querySelector('[toolbox-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);
});
// Set the first tab as active on load
this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
}
toggle() {
this.elem.classList.toggle('open');
}
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';
}
if (openToolbox) this.elem.classList.add('open');
}
}
module.exports = EditorToolbox;

View File

@@ -0,0 +1,59 @@
class ImagePicker {
constructor(elem) {
this.elem = elem;
this.imageElem = elem.querySelector('img');
this.input = elem.querySelector('input');
this.isUsingIds = elem.getAttribute('data-current-id') !== '';
this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width');
this.isResizeCropping = elem.getAttribute('data-resize-crop') !== '';
let selectButton = elem.querySelector('button[data-action="show-image-manager"]');
selectButton.addEventListener('click', this.selectImage.bind(this));
let resetButton = elem.querySelector('button[data-action="reset-image"]');
resetButton.addEventListener('click', this.reset.bind(this));
let removeButton = elem.querySelector('button[data-action="remove-image"]');
if (removeButton) {
removeButton.addEventListener('click', this.removeImage.bind(this));
}
}
selectImage() {
window.ImageManager.show(image => {
if (!this.isResizing) {
this.setImage(image);
return;
}
let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false');
window.$http.get(window.baseUrl(requestString)).then(resp => {
image.url = resp.data.url;
this.setImage(image);
});
});
}
reset() {
this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')});
}
setImage(image) {
this.imageElem.src = image.url;
this.input.value = this.isUsingIds ? image.id : image.url;
this.imageElem.classList.remove('none');
}
removeImage() {
this.imageElem.src = this.elem.getAttribute('data-default-image');
this.imageElem.classList.add('none');
this.input.value = 'none';
}
}
module.exports = ImagePicker;

View File

@@ -11,6 +11,11 @@ let componentMapping = {
'sidebar': require('./sidebar'),
'page-picker': require('./page-picker'),
'page-comments': require('./page-comments'),
'wysiwyg-editor': require('./wysiwyg-editor'),
'markdown-editor': require('./markdown-editor'),
'editor-toolbox': require('./editor-toolbox'),
'image-picker': require('./image-picker'),
'collapsible': require('./collapsible'),
};
window.components = {};

View File

@@ -0,0 +1,295 @@
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('../code');
class MarkdownEditor {
constructor(elem) {
this.elem = elem;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
this.display = this.elem.querySelector('.markdown-display');
this.input = this.elem.querySelector('textarea');
this.htmlInput = this.elem.querySelector('input[name=html]');
this.cm = code.markdownEditor(this.input);
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
this.init();
}
init() {
// Prevent markdown display link click redirect
this.display.addEventListener('click', event => {
let link = event.target.closest('a');
if (link === null) return;
event.preventDefault();
window.open(link.getAttribute('href'));
});
// 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();
});
window.$events.listen('editor-markdown-update', value => {
this.cm.setValue(value);
this.updateAndRender();
});
this.codeMirrorSetup();
}
// Update the input content and render the display.
updateAndRender() {
let content = this.cm.getValue();
this.input.value = content;
let html = this.markdown.render(content);
window.$events.emit('editor-html-change', html);
window.$events.emit('editor-markdown-change', content);
this.display.innerHTML = html;
this.htmlInput.value = html;
}
onMarkdownScroll(lineCount) {
let elems = this.display.children;
if (elems.length <= lineCount) return;
let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
// TODO - Replace jQuery
$(this.display).animate({
scrollTop: topElem.offsetTop
}, {queue: false, duration: 200, easing: 'linear'});
}
codeMirrorSetup() {
let cm = this.cm;
// Custom key commands
let metaKey = 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>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
cm.on('change', (instance, changeObj) => {
this.updateAndRender();
});
// Handle scroll to sync display view
cm.on('scroll', 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);
});
// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
}
});
// Handle images on drag-drop
cm.on('drop', (cm, event) => {
event.stopPropagation();
event.preventDefault();
let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
if (!event.dataTransfer || !event.dataTransfer.files) return;
for (let i = 0; i < event.dataTransfer.files.length; i++) {
uploadImage(event.dataTransfer.files[i]);
}
});
// 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]);
}
// 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
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
cm.replaceSelection(placeHolderText);
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
window.$http.post('/images/gallery/upload', formData).then(resp => {
replaceContent(placeholderImage, resp.data.thumbs.display);
}).catch(err => {
events.emit('error', trans('errors.image_upload_error'));
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() {
let cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = this.cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
let 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);
});
}
}
module.exports = MarkdownEditor ;

View File

@@ -0,0 +1,11 @@
class WysiwygEditor {
constructor(elem) {
this.elem = elem;
this.options = require("../pages/page-form");
tinymce.init(this.options);
}
}
module.exports = WysiwygEditor;

View File

@@ -1,147 +0,0 @@
"use strict";
const moment = require('moment');
require('moment/locale/en-gb');
const editorOptions = require("./pages/page-form");
moment.locale('en-gb');
module.exports = function (ngApp, events) {
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
$scope.editorOptions = editorOptions();
$scope.editContent = '';
$scope.draftText = '';
let pageId = Number($attrs.pageId);
let isEdit = pageId !== 0;
let autosaveFrequency = 30; // AutoSave interval in seconds.
let isMarkdown = $attrs.editorType === 'markdown';
$scope.draftsEnabled = $attrs.draftsEnabled === 'true';
$scope.isUpdateDraft = Number($attrs.pageUpdateDraft) === 1;
$scope.isNewPageDraft = Number($attrs.pageNewDraft) === 1;
// Set initial header draft text
if ($scope.isUpdateDraft || $scope.isNewPageDraft) {
$scope.draftText = trans('entities.pages_editing_draft');
} else {
$scope.draftText = trans('entities.pages_editing_page');
}
let autoSave = false;
let currentContent = {
title: false,
html: false
};
if (isEdit && $scope.draftsEnabled) {
setTimeout(() => {
startAutoSave();
}, 1000);
}
// Actions specifically for the markdown editor
if (isMarkdown) {
$scope.displayContent = '';
// Editor change event
$scope.editorChange = function (content) {
$scope.displayContent = $sce.trustAsHtml(content);
}
}
if (!isMarkdown) {
$scope.editorChange = function() {};
}
let lastSave = 0;
/**
* Start the AutoSave loop, Checks for content change
* before performing the costly AJAX request.
*/
function startAutoSave() {
currentContent.title = $('#name').val();
currentContent.html = $scope.editContent;
autoSave = $interval(() => {
// Return if manually saved recently to prevent bombarding the server
if (Date.now() - lastSave < (1000*autosaveFrequency)/2) return;
let newTitle = $('#name').val();
let newHtml = $scope.editContent;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
saveDraft();
}
}, 1000 * autosaveFrequency);
}
let draftErroring = false;
/**
* Save a draft update into the system via an AJAX request.
*/
function saveDraft() {
if (!$scope.draftsEnabled) return;
let data = {
name: $('#name').val(),
html: isMarkdown ? $sce.getTrustedHtml($scope.displayContent) : $scope.editContent
};
if (isMarkdown) data.markdown = $scope.editContent;
let url = window.baseUrl('/ajax/page/' + pageId + '/save-draft');
$http.put(url, data).then(responseData => {
draftErroring = false;
let updateTime = moment.utc(moment.unix(responseData.data.timestamp)).toDate();
$scope.draftText = responseData.data.message + moment(updateTime).format('HH:mm');
if (!$scope.isNewPageDraft) $scope.isUpdateDraft = true;
showDraftSaveNotification();
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
events.emit('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
}
function showDraftSaveNotification() {
$scope.draftUpdated = true;
$timeout(() => {
$scope.draftUpdated = false;
}, 2000)
}
$scope.forceDraftSave = function() {
saveDraft();
};
// Listen to save draft events from editor
$scope.$on('save-draft', saveDraft);
/**
* Discard the current draft and grab the current page
* content from the system via an AJAX request.
*/
$scope.discardDraft = function () {
let url = window.baseUrl('/ajax/page/' + pageId);
$http.get(url).then(responseData => {
if (autoSave) $interval.cancel(autoSave);
$scope.draftText = trans('entities.pages_editing_page');
$scope.isUpdateDraft = false;
$scope.$broadcast('html-update', responseData.data.html);
$scope.$broadcast('markdown-update', responseData.data.markdown || responseData.data.html);
$('#name').val(responseData.data.name);
$timeout(() => {
startAutoSave();
}, 1000);
events.emit('success', trans('entities.pages_draft_discarded'));
});
};
}]);
};

View File

@@ -1,393 +0,0 @@
"use strict";
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
const code = require('./code');
module.exports = function (ngApp, events) {
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.
*/
ngApp.directive('tinymce', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: {
tinymce: '=',
mceModel: '=',
mceChange: '='
},
link: function (scope, element, attrs) {
function tinyMceSetup(editor) {
editor.on('ExecCommand change input NodeChange ObjectResized', (e) => {
let content = editor.getContent();
$timeout(() => {
scope.mceModel = content;
});
scope.mceChange(content);
});
editor.on('keydown', (event) => {
if (event.keyCode === 83 && (navigator.platform.match("Mac") ? event.metaKey : event.ctrlKey)) {
event.preventDefault();
scope.$emit('save-draft', event);
}
});
editor.on('init', (e) => {
scope.mceModel = editor.getContent();
});
scope.$on('html-update', (event, value) => {
editor.setContent(value);
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
scope.mceModel = editor.getContent();
});
}
scope.tinymce.extraSetups.push(tinyMceSetup);
tinymce.init(scope.tinymce);
}
}
}]);
const md = new MarkdownIt({html: true});
md.use(mdTasksLists, {label: true});
/**
* Markdown input
* Handles the logic for just the editor input field.
*/
ngApp.directive('markdownInput', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: {
mdModel: '=',
mdChange: '='
},
link: function (scope, element, attrs) {
// Codemirror Setup
element = element.find('textarea').first();
let cm = code.markdownEditor(element[0]);
// Custom key commands
let metaKey = 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`] = function(cm) {scope.$emit('save-draft');};
// Show link selector
extraKeys[`Shift-${metaKey}-K`] = function(cm) {showLinkSelector()};
// Insert Link
extraKeys[`${metaKey}-K`] = function(cm) {insertLink()};
// FormatShortcuts
extraKeys[`${metaKey}-1`] = function(cm) {replaceLineStart('##');};
extraKeys[`${metaKey}-2`] = function(cm) {replaceLineStart('###');};
extraKeys[`${metaKey}-3`] = function(cm) {replaceLineStart('####');};
extraKeys[`${metaKey}-4`] = function(cm) {replaceLineStart('#####');};
extraKeys[`${metaKey}-5`] = function(cm) {replaceLineStart('');};
extraKeys[`${metaKey}-d`] = function(cm) {replaceLineStart('');};
extraKeys[`${metaKey}-6`] = function(cm) {replaceLineStart('>');};
extraKeys[`${metaKey}-q`] = function(cm) {replaceLineStart('>');};
extraKeys[`${metaKey}-7`] = function(cm) {wrapSelection('\n```\n', '\n```');};
extraKeys[`${metaKey}-8`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`Shift-${metaKey}-E`] = function(cm) {wrapSelection('`', '`');};
extraKeys[`${metaKey}-9`] = function(cm) {wrapSelection('<p class="callout info">', '</p>');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
cm.on('change', (instance, changeObj) => {
update(instance);
});
// Handle scroll to sync display view
cm.on('scroll', 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) {
scope.$emit('markdown-scroll', -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(md.render(range), 'text/html');
let totalLines = doc.documentElement.querySelectorAll('body > *');
scope.$emit('markdown-scroll', totalLines.length);
});
// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
}
});
// Handle images on drag-drop
cm.on('drop', (cm, event) => {
event.stopPropagation();
event.preventDefault();
let cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
cm.setCursor(cursorPos);
if (!event.dataTransfer || !event.dataTransfer.files) return;
for (let i = 0; i < event.dataTransfer.files.length; i++) {
uploadImage(event.dataTransfer.files[i]);
}
});
// 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]);
}
// 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
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
cm.replaceSelection(placeHolderText);
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
window.$http.post('/images/gallery/upload', formData).then(resp => {
replaceContent(placeholderImage, resp.data.thumbs.display);
}).catch(err => {
events.emit('error', trans('errors.image_upload_error'));
replaceContent(placeHolderText, selectedText);
console.log(err);
});
}
// Show the popup link selector and insert a link when finished
function showLinkSelector() {
let cursorPos = cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
cm.focus();
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
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);
}
// Show the image manager and handle image insertion
function showImageManager() {
let cursorPos = cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
cm.focus();
cm.replaceSelection(newText);
cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
});
}
// Update the data models and rendered output
function update(instance) {
let content = instance.getValue();
element.val(content);
$timeout(() => {
scope.mdModel = content;
scope.mdChange(md.render(content));
});
}
update(cm);
// Listen to commands from parent scope
scope.$on('md-insert-link', showLinkSelector);
scope.$on('md-insert-image', showImageManager);
scope.$on('markdown-update', (event, value) => {
cm.setValue(value);
element.val(value);
scope.mdModel = value;
scope.mdChange(md.render(value));
});
}
}
}]);
/**
* Markdown Editor
* Handles all functionality of the markdown editor.
*/
ngApp.directive('markdownEditor', ['$timeout', '$rootScope', function ($timeout, $rootScope) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
// Editor Elements
const $display = element.find('.markdown-display').first();
const $insertImage = element.find('button[data-action="insertImage"]');
const $insertEntityLink = element.find('button[data-action="insertEntityLink"]');
// Prevent markdown display link click redirect
$display.on('click', 'a', function(event) {
event.preventDefault();
window.open(this.getAttribute('href'));
});
// Editor UI Actions
$insertEntityLink.click(e => {scope.$broadcast('md-insert-link');});
$insertImage.click(e => {scope.$broadcast('md-insert-image');});
// Handle scroll sync event from editor scroll
$rootScope.$on('markdown-scroll', (event, lineCount) => {
let elems = $display[0].children[0].children;
if (elems.length > lineCount) {
let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
$display.animate({
scrollTop: topElem.offsetTop
}, {queue: false, duration: 200, easing: 'linear'});
}
});
}
}
}]);
/**
* Page Editor Toolbox
* Controls all functionality for the sliding toolbox
* on the page edit view.
*/
ngApp.directive('toolbox', [function () {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
// Get common elements
const $buttons = elem.find('[toolbox-tab-button]');
const $content = elem.find('[toolbox-tab-content]');
const $toggle = elem.find('[toolbox-toggle]');
// Handle toolbox toggle click
$toggle.click((e) => {
elem.toggleClass('open');
});
// Set an active tab/content by name
function setActive(tabName, openToolbox) {
$buttons.removeClass('active');
$content.hide();
$buttons.filter(`[toolbox-tab-button="${tabName}"]`).addClass('active');
$content.filter(`[toolbox-tab-content="${tabName}"]`).show();
if (openToolbox) elem.addClass('open');
}
// Set the first tab content active on load
setActive($content.first().attr('toolbox-tab-content'), false);
// Handle tab button click
$buttons.click(function (e) {
let name = $(this).attr('toolbox-tab-button');
setActive(name, true);
});
}
}
}]);
};

View File

@@ -58,16 +58,6 @@ window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
Vue.prototype.$events = window.$events;
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
const Translations = require("./translations");
@@ -79,11 +69,6 @@ window.trans_choice = translator.getPlural.bind(translator);
require("./vues/vues");
require("./components");
// Load in angular specific items
const Directives = require('./directives');
const Controllers = require('./controllers');
Directives(ngApp, window.$events);
Controllers(ngApp, window.$events);
//Global jQuery Config & Extensions

View File

@@ -1,54 +1,55 @@
"use strict";
const Code = require('../code');
/**
* Handle pasting images from clipboard.
* @param e - event
* @param editor - editor instance
* @param {ClipboardEvent} event
* @param editor
*/
function editorPaste(e, editor) {
if (!e.clipboardData) return;
let items = e.clipboardData.items;
if (!items) return;
function editorPaste(event, editor) {
if (!event.clipboardData || !event.clipboardData.items) return;
let items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") === -1) return;
let file = items[i].getAsFile();
let formData = new FormData();
let ext = 'png';
let xhr = new XMLHttpRequest();
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches) {
ext = fileNameMatches[1];
}
}
if (items[i].type.indexOf("image") === -1) continue;
event.preventDefault();
let id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif');
editor.execCommand('mceInsertContent', false, `<img src="${loadingImage}" id="${id}">`);
let remoteFilename = "image-" + Date.now() + "." + ext;
formData.append('file', file, remoteFilename);
formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content'));
xhr.open('POST', window.baseUrl('/images/gallery/upload'));
xhr.onload = function () {
if (xhr.status === 200 || xhr.status === 201) {
let result = JSON.parse(xhr.responseText);
editor.dom.setAttrib(id, 'src', result.thumbs.display);
} else {
console.log('An error occurred uploading the image', xhr.responseText);
let file = items[i].getAsFile();
setTimeout(() => {
editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
uploadImageFile(file).then(resp => {
editor.dom.setAttrib(id, 'src', resp.thumbs.display);
}).catch(err => {
editor.dom.remove(id);
}
};
xhr.send(formData);
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
});
}, 10);
}
}
/**
* Upload an image file to the server
* @param {File} file
*/
function uploadImageFile(file) {
if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
let ext = 'png';
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
return window.$http.post('/images/gallery/upload', formData).then(resp => (resp.data));
}
function registerEditorShortcuts(editor) {
// Headers
for (let i = 1; i < 5; i++) {
@@ -64,6 +65,17 @@ function registerEditorShortcuts(editor) {
editor.shortcuts.add('meta+e', '', ['codeeditor', false, 'pre']);
editor.shortcuts.add('meta+8', '', ['FormatBlock', false, 'code']);
editor.shortcuts.add('meta+shift+E', '', ['FormatBlock', false, 'code']);
// Save draft shortcut
editor.shortcuts.add('meta+S', '', () => {
window.$events.emit('editor-save-draft');
});
// Save page shortcut
editor.shortcuts.add('meta+13', '', () => {
window.$events.emit('editor-save-page');
});
// Loop through callout styles
editor.shortcuts.add('meta+9', '', function() {
let selectedNode = editor.selection.getNode();
@@ -82,8 +94,20 @@ function registerEditorShortcuts(editor) {
}
editor.formatter.apply('p');
});
}
/**
* Load custom HTML head content from the settings into the editor.
* @param editor
*/
function loadCustomHeadContent(editor) {
window.$http.get(window.baseUrl('/custom-head-content')).then(resp => {
if (!resp.data) return;
let head = editor.getDoc().querySelector('head');
head.innerHTML += resp.data;
});
}
/**
* Create and enable our custom code plugin
@@ -194,191 +218,195 @@ function codePlugin() {
});
}
codePlugin();
function hrPlugin() {
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
let hrElem = document.createElement('hr');
let cNode = editor.selection.getNode();
let parentNode = cNode.parentNode;
parentNode.insertBefore(hrElem, cNode);
});
editor.addButton('hr', {
icon: 'hr',
tooltip: 'Horizontal line',
cmd: 'InsertHorizontalRule'
});
editor.addMenuItem('hr', {
icon: 'hr',
text: 'Horizontal line',
cmd: 'InsertHorizontalRule',
context: 'insert'
});
window.tinymce.PluginManager.add('customhr', function (editor) {
editor.addCommand('InsertHorizontalRule', function () {
let hrElem = document.createElement('hr');
let cNode = editor.selection.getNode();
let parentNode = cNode.parentNode;
parentNode.insertBefore(hrElem, cNode);
});
}
module.exports = function() {
hrPlugin();
codePlugin();
let settings = {
selector: '#html-editor',
content_css: [
window.baseUrl('/css/styles.css'),
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
branding: false,
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"},
{title: "Header Small", format: "h4"},
{title: "Header Tiny", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
{title: "Inline Code", icon: "code", inline: "code"},
{title: "Callouts", items: [
{title: "Info", format: 'calloutinfo'},
{title: "Success", format: 'calloutsuccess'},
{title: "Warning", format: 'calloutwarning'},
{title: "Danger", format: 'calloutdanger'}
]},
],
style_formats_merge: false,
formats: {
codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
},
file_browser_callback: function (field_name, url, type, win) {
editor.addButton('hr', {
icon: 'hr',
tooltip: 'Horizontal line',
cmd: 'InsertHorizontalRule'
});
if (type === 'file') {
window.EntitySelectorPopup.show(function(entity) {
let originalField = win.document.getElementById(field_name);
originalField.value = entity.link;
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
editor.addMenuItem('hr', {
icon: 'hr',
text: 'Horizontal line',
cmd: 'InsertHorizontalRule',
context: 'insert'
});
});
module.exports = {
selector: '#html-editor',
content_css: [
window.baseUrl('/css/styles.css'),
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
branding: false,
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
statusbar: false,
menubar: false,
paste_data_images: false,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|h1|h2|h3|h4|h5|h6|blockquote],+div[pre]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",
style_formats: [
{title: "Header Large", format: "h2"},
{title: "Header Medium", format: "h3"},
{title: "Header Small", format: "h4"},
{title: "Header Tiny", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
{title: "Inline Code", icon: "code", inline: "code"},
{title: "Callouts", items: [
{title: "Info", format: 'calloutinfo'},
{title: "Success", format: 'calloutsuccess'},
{title: "Warning", format: 'calloutwarning'},
{title: "Danger", format: 'calloutdanger'}
]},
],
style_formats_merge: false,
formats: {
codeeditor: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div'},
alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'},
aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'},
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}},
calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}},
calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}},
calloutdanger: {block: 'p', exact: true, attributes: {class: 'callout danger'}}
},
file_browser_callback: function (field_name, url, type, win) {
if (type === 'file') {
window.EntitySelectorPopup.show(function(entity) {
let originalField = win.document.getElementById(field_name);
originalField.value = entity.link;
$(originalField).closest('.mce-form').find('input').eq(2).val(entity.name);
});
}
if (type === 'image') {
// Show image manager
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
let evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
win.document.getElementById(field_name).dispatchEvent(evt);
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
// Replace the actively selected content with the linked image
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
});
}
},
paste_preprocess: function (plugin, args) {
let content = args.content;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
init_instance_callback: function(editor) {
loadCustomHeadContent(editor);
},
setup: function (editor) {
editor.on('init ExecCommand change input NodeChange ObjectResized', editorChange);
function editorChange() {
let content = editor.getContent();
window.$events.emit('editor-html-change', content);
}
window.$events.listen('editor-html-update', html => {
editor.setContent(html);
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(false);
editorChange(html);
});
registerEditorShortcuts(editor);
let wrap;
function hasTextContent(node) {
return node && !!( node.textContent || node.innerText );
}
editor.on('dragstart', function () {
let node = editor.selection.getNode();
if (node.nodeName !== 'IMG') return;
wrap = editor.dom.getParent(node, '.mceTemp');
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
wrap = node.parentNode;
}
});
editor.on('drop', function (event) {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
});
}
if (type === 'image') {
// Show image manager
wrap = null;
});
// Custom Image picker button
editor.addButton('image-insert', {
title: 'My title',
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
let evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
win.document.getElementById(field_name).dispatchEvent(evt);
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
// Replace the actively selected content with the linked image
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
editor.execCommand('mceInsertContent', false, html);
});
}
});
},
paste_preprocess: function (plugin, args) {
let content = args.content;
if (content.indexOf('<img src="file://') !== -1) {
args.content = '';
}
},
extraSetups: [],
setup: function (editor) {
// Run additional setup actions
// Used by the angular side of things
for (let i = 0; i < settings.extraSetups.length; i++) {
settings.extraSetups[i](editor);
}
registerEditorShortcuts(editor);
let wrap;
function hasTextContent(node) {
return node && !!( node.textContent || node.innerText );
}
editor.on('dragstart', function () {
let node = editor.selection.getNode();
if (node.nodeName !== 'IMG') return;
wrap = editor.dom.getParent(node, '.mceTemp');
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
wrap = node.parentNode;
}
});
editor.on('drop', function (event) {
let dom = editor.dom,
rng = tinymce.dom.RangeUtils.getCaretRangeFromPoint(event.clientX, event.clientY, editor.getDoc());
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(function () {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
});
}
wrap = null;
});
// Custom Image picker button
editor.addButton('image-insert', {
title: 'My title',
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.show(function (image) {
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';
editor.execCommand('mceInsertContent', false, html);
});
}
});
// Paste image-uploads
editor.on('paste', function(event) {
editorPaste(event, editor);
});
}
};
return settings;
// Paste image-uploads
editor.on('paste', event => { editorPaste(event, editor) });
}
};

View File

@@ -102,23 +102,28 @@ let setupPageShow = window.setupPageShow = function (pageId) {
let $window = $(window);
let $sidebar = $("#sidebar .scroll-body");
let $bookTreeParent = $sidebar.parent();
// Check the page is scrollable and the content is taller than the tree
let pageScrollable = ($(document).height() > $window.height()) && ($sidebar.height() < $('.page-content').height());
// Get current tree's width and header height
let headerHeight = $("#header").height() + $(".toolbar").height();
let isFixed = $window.scrollTop() > headerHeight;
// Function to fix the tree as a sidebar
// Fix the tree as a sidebar
function stickTree() {
$sidebar.width($bookTreeParent.width() + 15);
$sidebar.addClass("fixed");
isFixed = true;
}
// Function to un-fix the tree back into position
// Un-fix the tree back into position
function unstickTree() {
$sidebar.css('width', 'auto');
$sidebar.removeClass("fixed");
isFixed = false;
}
// Checks if the tree stickiness state should change
function checkTreeStickiness(skipCheck) {
let shouldBeFixed = $window.scrollTop() > headerHeight;
@@ -150,6 +155,59 @@ let setupPageShow = window.setupPageShow = function (pageId) {
unstickTree();
}
});
// Check if support is present for IntersectionObserver
if ('IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
addPageHighlighting();
}
function addPageHighlighting() {
let pageNav = document.querySelector('.sidebar-page-nav');
// fetch all the headings.
let 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);
}
function addNavObserver(headings) {
// Setup the intersection observer.
let intersectOpts = {
rootMargin: '0px 0px 0px 0px',
threshold: 1.0
};
let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
// observe each heading
for (let i = 0; i !== headings.length; ++i) {
pageNavObserver.observe(headings[i]);
}
}
function headingVisibilityChange(entries, observer) {
for (let i = 0; i < entries.length; i++) {
let currentEntry = entries[i];
let isVisible = (currentEntry.intersectionRatio === 1);
toggleAnchorHighlighting(currentEntry.target.id, isVisible);
}
}
function toggleAnchorHighlighting(elementId, shouldHighlight) {
let anchorsToHighlight = pageNav.querySelectorAll('a[href="#' + elementId + '"]');
for (let i = 0; i < anchorsToHighlight.length; i++) {
// Change below to use classList.toggle when IE support is dropped.
if (shouldHighlight) {
anchorsToHighlight[i].classList.add('current-heading');
} else {
anchorsToHighlight[i].classList.remove('current-heading');
}
}
}
}
};
module.exports = setupPageShow;

View File

@@ -125,6 +125,4 @@ const methods = {
};
const computed = [];
module.exports = {template, data, props, methods, computed};
module.exports = {template, data, props, methods};

View File

@@ -0,0 +1,153 @@
const moment = require('moment');
require('moment/locale/en-gb');
moment.locale('en-gb');
let autoSaveFrequency = 30;
let autoSave = false;
let draftErroring = false;
let currentContent = {
title: false,
html: false
};
let lastSave = 0;
function mounted() {
let elem = this.$el;
this.draftsEnabled = elem.getAttribute('drafts-enabled') === 'true';
this.editorType = elem.getAttribute('editor-type');
this.pageId= Number(elem.getAttribute('page-id'));
this.isNewDraft = Number(elem.getAttribute('page-new-draft')) === 1;
this.isUpdateDraft = Number(elem.getAttribute('page-update-draft')) === 1;
if (this.pageId !== 0 && this.draftsEnabled) {
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
}
if (this.isUpdateDraft || this.isNewDraft) {
this.draftText = trans('entities.pages_editing_draft');
} else {
this.draftText = trans('entities.pages_editing_page');
}
// Listen to save events from editor
window.$events.listen('editor-save-draft', this.saveDraft);
window.$events.listen('editor-save-page', this.savePage);
// Listen to content changes from the editor
window.$events.listen('editor-html-change', html => {
this.editorHTML = html;
});
window.$events.listen('editor-markdown-change', markdown => {
this.editorMarkdown = markdown;
});
}
let data = {
draftsEnabled: false,
editorType: 'wysiwyg',
pagedId: 0,
isNewDraft: false,
isUpdateDraft: false,
draftText: '',
draftUpdated : false,
changeSummary: '',
editorHTML: '',
editorMarkdown: '',
};
let methods = {
startAutoSave() {
currentContent.title = document.getElementById('name').value.trim();
currentContent.html = this.editorHTML;
autoSave = window.setInterval(() => {
// Return if manually saved recently to prevent bombarding the server
if (Date.now() - lastSave < (1000 * autoSaveFrequency)/2) return;
let newTitle = document.getElementById('name').value.trim();
let newHtml = this.editorHTML;
if (newTitle !== currentContent.title || newHtml !== currentContent.html) {
currentContent.html = newHtml;
currentContent.title = newTitle;
this.saveDraft();
}
}, 1000 * autoSaveFrequency);
},
saveDraft() {
if (!this.draftsEnabled) return;
let data = {
name: document.getElementById('name').value.trim(),
html: this.editorHTML
};
if (this.editorType === 'markdown') data.markdown = this.editorMarkdown;
let url = window.baseUrl(`/ajax/page/${this.pageId}/save-draft`);
window.$http.put(url, data).then(response => {
draftErroring = false;
let updateTime = moment.utc(moment.unix(response.data.timestamp)).toDate();
if (!this.isNewDraft) this.isUpdateDraft = true;
this.draftNotifyChange(response.data.message + moment(updateTime).format('HH:mm'));
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
window.$events('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
},
savePage() {
this.$el.closest('form').submit();
},
draftNotifyChange(text) {
this.draftText = text;
this.draftUpdated = true;
window.setTimeout(() => {
this.draftUpdated = false;
}, 2000);
},
discardDraft() {
let url = window.baseUrl(`/ajax/page/${this.pageId}`);
window.$http.get(url).then(response => {
if (autoSave) window.clearInterval(autoSave);
this.draftText = trans('entities.pages_editing_page');
this.isUpdateDraft = false;
window.$events.emit('editor-html-update', response.data.html);
window.$events.emit('editor-markdown-update', response.data.markdown || response.data.html);
document.getElementById('name').value = response.data.name;
window.setTimeout(() => {
this.startAutoSave();
}, 1000);
window.$events.emit('success', trans('entities.pages_draft_discarded'));
});
},
};
let computed = {
changeSummaryShort() {
let len = this.changeSummary.length;
if (len === 0) return trans('entities.pages_edit_set_changelog');
if (len <= 16) return this.changeSummary;
return this.changeSummary.slice(0, 16) + '...';
}
};
module.exports = {
mounted, data, methods, computed,
};

View File

@@ -9,8 +9,6 @@ let data = {
const components = {draggable, autosuggest};
const directives = {};
let computed = {};
let methods = {
addEmptyTag() {
@@ -64,5 +62,5 @@ function mounted() {
}
module.exports = {
data, computed, methods, mounted, components, directives
data, methods, mounted, components, directives
};

View File

@@ -11,6 +11,7 @@ let vueMapping = {
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
'attachment-manager': require('./attachment-manager'),
'page-editor': require('./page-editor'),
};
window.vues = {};

View File

@@ -195,10 +195,13 @@
font-weight: 400;
text-transform: uppercase;
}
h3 a {
line-height: 1;
}
.body, p.empty-text {
padding: $-m;
}
a {
a, p {
word-wrap: break-word;
word-break: break-word;
}

View File

@@ -370,6 +370,7 @@ span.CodeMirror-selectedtext { background: none; }
.cm-s-base16-light span.cm-keyword { color: #ac4142; }
.cm-s-base16-light span.cm-string { color: #e09c3c; }
.cm-s-base16-light span.cm-builtin { color: #4c7f9e; }
.cm-s-base16-light span.cm-variable { color: #90a959; }
.cm-s-base16-light span.cm-variable-2 { color: #6a9fb5; }
.cm-s-base16-light span.cm-def { color: #d28445; }

View File

@@ -63,9 +63,10 @@
padding: 0 $-m 0;
margin-left: -1px;
overflow-y: scroll;
.page-content {
margin: 0 auto;
}
}
.markdown-display.page-content {
margin: 0 auto;
max-width: 100%;
}
}
.editor-toolbar {
@@ -190,6 +191,41 @@ input:checked + .toggle-switch {
}
}
.form-group[collapsible] {
margin-left: -$-m;
margin-right: -$-m;
padding: 0 $-m;
border-top: 1px solid #DDD;
border-bottom: 1px solid #DDD;
.collapse-title {
margin-left: -$-m;
margin-right: -$-m;
padding: $-s $-m;
}
.collapse-title, .collapse-title label {
cursor: pointer;
}
.collapse-title label {
padding-bottom: 0;
margin-bottom: 0;
color: inherit;
}
.collapse-title label:before {
display: inline-block;
content: '';
margin-right: $-m;
transition: all ease-in-out 400ms;
transform: rotate(0);
}
.collapse-content {
display: none;
padding-bottom: $-m;
}
&.open .collapse-title label:before {
transform: rotate(90deg);
}
}
.inline-input-style {
display: block;
width: 100%;

View File

@@ -19,6 +19,7 @@ body.flexbox {
display: flex;
align-items: stretch;
min-height: 0;
max-width: 100%;
position: relative;
&.rows {
flex-direction: row;
@@ -134,7 +135,7 @@ body.flexbox {
width: 30%;
left: 0;
height: 100%;
overflow-y: scroll;
overflow-y: auto;
-ms-overflow-style: none;
//background-color: $primary-faded;
border-left: 1px solid #DDD;
@@ -194,6 +195,14 @@ div[class^="col-"] img {
display: inline-block;
}
@include larger-than(991px) {
.row.auto-clear .col-md-4:nth-child(3n+1){clear:left;}
}
@include smaller-than(992px) {
.row.auto-clear .col-xs-6:nth-child(2n+1){clear:left;}
}
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
position: relative;
min-height: 1px;

View File

@@ -82,6 +82,9 @@
.h6 {
margin-left: $nav-indent*4;
}
.current-heading {
font-weight: bold;
}
}
// Sidebar list
@@ -373,3 +376,51 @@ ul.pagination {
border-bottom: 1px solid #DDD;
}
}
// Books grid view
.featured-image-container {
position: relative;
overflow: hidden;
background: #F2F2F2;
border: 1px solid #ddd;
border-bottom: 0;
img {
display: block;
max-width: 100%;
height: auto;
transition: all .5s ease;
}
img:hover {
transform: scale(1.15);
opacity: .5;
}
}
.book-grid-content {
padding: 30px;
border: 1px solid #ddd;
border-top: 0;
border-bottom-width: 2px;
h2 {
font-size: 1.5em;
margin: 0 0 10px;
}
h2 a {
display: block;
line-height: 1.2;
color: #009688;;
text-decoration: none;
}
p {
font-size: .85em;
margin: 0 0 10px;
line-height: 1.6em;
}
p.small {
font-size: .8em;
}
}
.book-grid-item {
margin-bottom : 20px;
}

View File

@@ -1,16 +1,13 @@
.mce-tinymce.mce-container.fullscreen {
.mce-tinymce.mce-container.mce-fullscreen {
position: fixed;
top: 0;
height: 100%;
width: 825px;
width: 100%;
max-width: 100%;
margin-left: -$-s;
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.08);
z-index: 100;
}
.mce-tinymce {
.mce-panel {
background-color: #FFF;

View File

@@ -231,7 +231,4 @@ $btt-size: 40px;
input {
width: 100%;
}
}
}

114
resources/lang/check.php Executable file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env php
<?php
/**
* Compares translation files to find missing and redundant content.
*/
$args = array_slice($argv, 1);
if (count($args) === 0) {
errorOut("Please provide a language code as the first argument (./check.php fr)");
}
// Get content from files
$lang = formatLang($args[0]);
$enContent = loadLang('en');
$langContent = loadLang($lang);
if (count($langContent) === 0) {
errorOut("No language content found for '{$lang}'");
}
info("Checking '{$lang}' translation content against 'en'");
// Track missing lang strings
$missingLangStrings = [];
foreach ($enContent as $enKey => $enStr) {
if (strpos($enKey, 'settings.language_select.') === 0) {
unset($langContent[$enKey]);
continue;
}
if (!isset($langContent[$enKey])) {
$missingLangStrings[$enKey] = $enStr;
continue;
}
unset($langContent[$enKey]);
}
if (count($missingLangStrings) > 0) {
info("\n========================");
info("Missing language content");
info("========================");
outputFlatArray($missingLangStrings, $lang);
}
if (count($langContent) > 0) {
info("\n==========================");
info("Redundant language content");
info("==========================");
outputFlatArray($langContent, $lang);
}
function outputFlatArray($arr, $lang) {
$grouped = [];
foreach ($arr as $key => $val) {
$explodedKey = explode('.', $key);
$group = $explodedKey[0];
$path = implode('.', array_slice($explodedKey, 1));
if (!isset($grouped[$group])) $grouped[$group] = [];
$grouped[$group][$path] = $val;
}
foreach ($grouped as $filename => $arr) {
echo "\e[36m" . $lang . '/' . $filename . ".php\e[0m\n";
echo json_encode($arr, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . "\n";
}
}
function formatLang($lang) {
$langParts = explode('_', strtoupper($lang));
$langParts[0] = strtolower($langParts[0]);
return implode('_', $langParts);
}
function loadLang(string $lang) {
$dir = __DIR__ . "/{$lang}";
if (!file_exists($dir)) {
errorOut("Expected directory '{$dir}' does not exist");
}
$files = scandir($dir);
$data = [];
foreach ($files as $file) {
if (substr($file, -4) !== '.php') continue;
$fileData = include ($dir . '/' . $file);
$name = substr($file, 0, -4);
$data[$name] = $fileData;
}
return flattenArray($data);
}
function flattenArray(array $arr) {
$data = [];
foreach ($arr as $key => $arrItem) {
if (!is_array($arrItem)) {
$data[$key] = $arrItem;
continue;
}
$toUse = flattenArray($arrItem);
foreach ($toUse as $innerKey => $item) {
$data[$key . '.' . $innerKey] = $item;
}
}
return $data;
}
function info($text) {
echo "\e[34m" . $text . "\e[0m\n";
}
function errorOut($text) {
echo "\e[31m" . $text . "\e[0m\n";
exit(1);
}

View File

@@ -17,6 +17,8 @@ return [
'name' => 'Name',
'description' => 'Beschreibung',
'role' => 'Rolle',
'cover_image' => 'Titelbild',
'cover_image_description' => 'Das Bild sollte eine Auflösung von 300x170px haben.',
/**
* Actions
@@ -43,7 +45,7 @@ return [
'no_items' => 'Keine Einträge gefunden.',
'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken',
'toggle_thumbnails' => 'Thumbnails zeigen/verstecken',
/**
* Header
*/

View File

@@ -31,6 +31,8 @@ return [
'app_logo_desc' => "Dieses Bild sollte 43px hoch sein.\nGrößere Bilder werden verkleinert.",
'app_primary_color' => 'Primäre Anwendungsfarbe',
'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.",
'app_disable_comments' => 'Kommentare deaktivieren',
'app_disable_comments_desc' => 'Deaktiviert Kommentare über alle Seiten in der Anwendung. Vorhandene Kommentare werden nicht angezeigt.',
/**
* Registration settings
@@ -96,6 +98,7 @@ return [
'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
'users_books_view_type' => 'Bevorzugtes Display-Layout für Bücher',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',

View File

@@ -18,7 +18,9 @@ return [
'name' => 'Name',
'description' => 'Description',
'role' => 'Role',
'cover_image' => 'Cover image',
'cover_image_description' => 'This image should be approx 440x250px.',
/**
* Actions
*/
@@ -45,6 +47,7 @@ return [
'no_items' => 'No items available',
'back_to_top' => 'Back to top',
'toggle_details' => 'Toggle Details',
'toggle_thumbnails' => 'Toggle Thumbnails',
'details' => 'Details',
/**

View File

@@ -20,6 +20,7 @@ return [
'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'No action defined',
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
'social_account_existing' => 'This :socialAccount is already attached to your profile.',
@@ -41,6 +42,7 @@ return [
// Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
// Entities
'entity_not_found' => 'Entity not found',

View File

@@ -34,6 +34,8 @@ return [
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_default' => 'Default homepage view chosen',
'app_disable_comments' => 'Disable comments',
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',
/**
* Registration settings
@@ -94,6 +96,7 @@ return [
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Only fill the below if you would like to change your password:',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_books_view_type' => 'Preferred layout for books viewing',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
@@ -120,13 +123,15 @@ return [
'en' => 'English',
'de' => 'Deutsch',
'es' => 'Español',
'es_AR' => 'Español Argentina',
'fr' => 'Français',
'nl' => 'Nederlands',
'pt_BR' => 'Português do Brasil',
'sk' => 'Slovensky',
'ja' => '日本語',
'pl' => 'Polski',
'it' => 'Italian'
'it' => 'Italian',
'ru' => 'Русский'
]
///////////////////////////////////
];

View File

@@ -17,7 +17,8 @@ return [
'name' => 'Nombre',
'description' => 'Descripción',
'role' => 'Rol',
'cover_image' => 'Imagen de portada',
'cover_image_description' => 'Esta imagen debe ser aproximadamente 300x170px.',
/**
* Actions
*/
@@ -43,6 +44,7 @@ return [
'no_items' => 'No hay items disponibles',
'back_to_top' => 'Volver arriba',
'toggle_details' => 'Alternar detalles',
'toggle_thumbnails' => 'Alternar miniaturas',
/**
* Header

View File

@@ -31,6 +31,8 @@ return [
'app_logo_desc' => 'Esta imagen debería de ser 43px en altura. <br>Iágenes grandes seán escaladas.',
'app_primary_color' => 'Color primario de la aplicación',
'app_primary_color_desc' => 'Esto debería ser un valor hexadecimal. <br>Deje el valor vaío para reiniciar al valor por defecto.',
'app_disable_comments' => 'Deshabilitar comentarios',
'app_disable_comments_desc' => 'Deshabilita los comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran. ',
/**
* Registration settings
@@ -91,6 +93,7 @@ return [
'users_external_auth_id' => 'ID externo de autenticación',
'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sio que es asignado automáticamente.',
'users_books_view_type' => 'Diseño de pantalla preferido para libros',
'users_delete' => 'Borrar usuario',
'users_delete_named' => 'Borrar usuario :userName',
'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',

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