Merge pull request #13 from BookStackApp/master

Getting the latest changes.
This commit is contained in:
Abijeet Patro 2017-08-14 23:09:26 +05:30 committed by GitHub
commit 4df3267521
98 changed files with 3611 additions and 1292 deletions

View File

@ -0,0 +1,57 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class UpgradeDatabaseEncoding extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:db-utf8mb4 {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate SQL commands to upgrade the database to UTF8mb4';
/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
}
$database = DB::getDatabaseName();
$tables = DB::select('SHOW TABLES');
$this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
$this->line('USE `'.$database.'`;');
$key = 'Tables_in_' . $database;
foreach ($tables as $table) {
$tableName = $table->$key;
$this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
}
DB::setDefaultConnection($connection);
}
}

View File

@ -15,7 +15,8 @@ class Kernel extends ConsoleKernel
Commands\ClearActivity::class,
Commands\ClearRevisions::class,
Commands\RegeneratePermissions::class,
Commands\RegenerateSearch::class
Commands\RegenerateSearch::class,
Commands\UpgradeDatabaseEncoding::class
];
/**

View File

@ -23,6 +23,9 @@ class AppServiceProvider extends ServiceProvider
\Blade::directive('icon', function($expression) {
return "<?php echo icon($expression); ?>";
});
// Allow longer string lengths after upgrade to utf8mb4
\Schema::defaultStringLength(191);
}
/**

View File

@ -571,7 +571,7 @@ class EntityRepo
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->text = $this->pageToPlainText($draftPage);
$draftPage->draft = false;
$draftPage->revision_count = 1;
@ -713,6 +713,17 @@ class EntityRepo
return $content;
}
/**
* Get the plain text version of a page's content.
* @param Page $page
* @return string
*/
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page);
return strip_tags($html);
}
/**
* Get a new draft page instance.
* @param Book $book
@ -816,7 +827,7 @@ class EntityRepo
$userId = user()->id;
$page->fill($input);
$page->html = $this->formatHtml($input['html']);
$page->text = strip_tags($page->html);
$page->text = $this->pageToPlainText($page);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId;
$page->revision_count++;
@ -933,7 +944,7 @@ class EntityRepo
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
$page->text = strip_tags($page->html);
$page->text = $this->pageToPlainText($page);
$page->updated_by = user()->id;
$page->save();
$this->searchService->indexEntity($page);
@ -953,7 +964,7 @@ class EntityRepo
if ($page->draft) {
$page->fill($data);
if (isset($data['html'])) {
$page->text = strip_tags($data['html']);
$page->text = $this->pageToPlainText($page);
}
$page->save();
return $page;

View File

@ -42,6 +42,8 @@ class LdapService
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$emailAttr = $this->config['email_attribute'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;

View File

@ -259,7 +259,7 @@ class PermissionService
$roleIds = array_map(function($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('id', $roleIds)->delete();
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
}
/**

View File

@ -58,7 +58,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk'],
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'],
/*
|--------------------------------------------------------------------------

View File

@ -16,6 +16,14 @@ if (env('REDIS_SERVERS', false)) {
}
}
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);
if (count($mysql_host_exploded) > 1) {
$mysql_host = $mysql_host_exploded[0];
$mysql_port = intval($mysql_host_exploded[1]);
}
return [
/*
@ -70,12 +78,13 @@ return [
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', 'localhost'),
'host' => $mysql_host,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'port' => $mysql_port,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => false,
],

View File

@ -80,6 +80,7 @@ return [
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
]
];

View File

@ -1,112 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (Schema::hasTable('comments')) {
return;
}
Schema::create('comments', function (Blueprint $table) {
$table->increments('id')->unsigned();
$table->integer('page_id')->unsigned();
$table->longText('text')->nullable();
$table->longText('html')->nullable();
$table->integer('parent_id')->unsigned()->nullable();
$table->integer('created_by')->unsigned();
$table->integer('updated_by')->unsigned()->nullable();
$table->index(['page_id', 'parent_id']);
$table->timestamps();
// Get roles with permissions we need to change
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
// Get roles with permissions we need to change
/*
$editorRole = DB::table('roles')->where('name', '=', 'editor')->first();
if (!empty($editorRole)) {
$editorRoleId = $editorRole->id;
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update Own', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $editorRoleId,
'permission_id' => $permissionId
]);
}
}
// Get roles with permissions we need to change
$viewerRole = DB::table('roles')->where('name', '=', 'viewer')->first();
if (!empty($viewerRole)) {
$viewerRoleId = $viewerRole->id;
// Create & attach new entity permissions
$ops = ['Create All'];
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $viewerRoleId,
'permission_id' => $permissionId
]);
}
}
*/
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}

View File

@ -1,38 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CommentsAddActiveCol extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('comments', function (Blueprint $table) {
// add column active
$table->boolean('active')->default(true);
$table->dropIndex('comments_page_id_parent_id_index');
$table->index(['page_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('comments', function (Blueprint $table) {
// reversing the schema
$table->dropIndex('comments_page_id_index');
$table->dropColumn('active');
$table->index(['page_id', 'parent_id']);
});
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
class UpdateDbEncodingToUt8mb4 extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Migration removed due to issues during live migration.
// Instead you can run the command `artisan bookstack:db-utf8mb4`
// which will generate out the SQL request to upgrade your DB to utf8mb4.
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View File

@ -0,0 +1,66 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->increments('id')->unsigned();
$table->integer('page_id')->unsigned();
$table->longText('text')->nullable();
$table->longText('html')->nullable();
$table->integer('parent_id')->unsigned()->nullable();
$table->integer('created_by')->unsigned();
$table->integer('updated_by')->unsigned()->nullable();
$table->boolean('active')->default(true);
$table->index(['page_id']);
$table->timestamps();
// Assign new comment permissions to admin role
$adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
// Create & attach new entity permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permissionId = DB::table('role_permissions')->insertGetId([
'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)),
'display_name' => $op . ' ' . $entity . 's',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
DB::table('permission_role')->insert([
'role_id' => $adminRoleId,
'permission_id' => $permissionId
]);
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
// Delete comment role permissions
$ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
$entity = 'Comment';
foreach ($ops as $op) {
$permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op));
DB::table('role_permissions')->where('name', '=', $permName)->delete();
}
}
}

View File

@ -14,8 +14,10 @@ const babelify = require("babelify");
const watchify = require("watchify");
const envify = require("envify");
const gutil = require("gulp-util");
const liveReload = require('gulp-livereload');
if (argv.production) process.env.NODE_ENV = 'production';
let isProduction = argv.production || process.env.NODE_ENV === 'production';
gulp.task('styles', () => {
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
@ -26,31 +28,40 @@ gulp.task('styles', () => {
}}))
.pipe(sass())
.pipe(autoprefixer('last 2 versions'));
if (argv.production) chain = chain.pipe(minifycss());
return chain.pipe(gulp.dest('public/css/'));
if (isProduction) chain = chain.pipe(minifycss());
return chain.pipe(gulp.dest('public/css/')).pipe(liveReload());
});
function scriptTask(watch=false) {
function scriptTask(watch = false) {
let props = {
basedir: 'resources/assets/js',
debug: true,
entries: ['global.js']
entries: ['global.js'],
fast: !isProduction,
cache: {},
packageCache: {},
};
let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
if (isProduction) {
bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
}
function rebundle() {
let stream = bundler.bundle();
stream = stream.pipe(source('common.js'));
if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
return stream.pipe(gulp.dest('public/js/'));
if (isProduction) stream = stream.pipe(buffer()).pipe(uglify());
return stream.pipe(gulp.dest('public/js/')).pipe(liveReload());
}
bundler.on('update', function() {
rebundle();
gutil.log('Rebundle...');
gutil.log('Rebundling assets...');
});
bundler.on('log', gutil.log);
return rebundle();
}
@ -59,6 +70,7 @@ gulp.task('scripts', () => {scriptTask(false)});
gulp.task('scripts-watch', () => {scriptTask(true)});
gulp.task('default', ['styles', 'scripts-watch'], () => {
liveReload.listen();
gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
});

View File

@ -4,7 +4,8 @@
"build": "gulp build",
"production": "gulp build --production",
"dev": "gulp",
"watch": "gulp"
"watch": "gulp",
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
},
"devDependencies": {
"babelify": "^7.3.0",
@ -13,6 +14,7 @@
"gulp": "3.9.1",
"gulp-autoprefixer": "3.1.1",
"gulp-clean-css": "^3.0.4",
"gulp-livereload": "^3.8.1",
"gulp-minify-css": "1.2.4",
"gulp-plumber": "1.1.0",
"gulp-sass": "3.1.0",
@ -29,15 +31,17 @@
"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",
"clipboard": "^1.5.16",
"clipboard": "^1.7.1",
"codemirror": "^5.26.0",
"dropzone": "^4.0.1",
"gulp-util": "^3.0.8",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"moment": "^2.12.0",
"vue": "^2.2.6"
"vue": "^2.2.6",
"vuedraggable": "^2.14.1"
},
"browser": {
"vue": "vue/dist/vue.common.js"

View File

@ -22,9 +22,12 @@ All development on BookStack is currently done on the master branch. When it's t
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. To run the build task you can use the following commands:
``` bash
# Build and minify for production
# Build assets for development
npm run-script build
# Build and minify assets for production
npm run-script production
# Build for dev (With sourcemaps) and watch for changes
npm run-script dev
```
@ -64,17 +67,19 @@ The BookStack source is provided under the MIT License.
## Attribution
These are the great projects used to help build BookStack:
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/)
* [highlight.js](https://highlightjs.org/)
* [CodeMirror](https://codemirror.net)
* [Vue.js](http://vuejs.org/)
* [Axios](https://github.com/mzabriskie/axios)
* [jQuery Sortable](https://johnny.github.io/jquery-sortable/)
* [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html)
* [Dropzone.js](http://www.dropzonejs.com/)
* [ZeroClipboard](http://zeroclipboard.org/)
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
@ -84,5 +89,3 @@ These are the great projects used to help build BookStack:
* [Snappy (WKHTML2PDF)](https://github.com/barryvdh/laravel-snappy)
* [Laravel IDE helper](https://github.com/barryvdh/laravel-ide-helper)
* [WKHTMLtoPDF](http://wkhtmltopdf.org/index.html)
Additionally, Thank you [BrowserStack](https://www.browserstack.com/) for supporting us and making cross-browser testing easy.

View File

@ -1,5 +1,6 @@
require('codemirror/mode/css/css');
require('codemirror/mode/clike/clike');
require('codemirror/mode/diff/diff');
require('codemirror/mode/go/go');
require('codemirror/mode/htmlmixed/htmlmixed');
require('codemirror/mode/javascript/javascript');
@ -17,30 +18,148 @@ require('codemirror/mode/yaml/yaml');
const CodeMirror = require('codemirror');
const modeMap = {
css: 'css',
c: 'clike',
java: 'clike',
scala: 'clike',
kotlin: 'clike',
'c++': 'clike',
'c#': 'clike',
csharp: 'clike',
diff: 'diff',
go: 'go',
html: 'htmlmixed',
javascript: 'javascript',
json: {name: 'javascript', json: true},
js: 'javascript',
php: 'php',
md: 'markdown',
mdown: 'markdown',
markdown: 'markdown',
nginx: 'nginx',
powershell: 'powershell',
py: 'python',
python: 'python',
ruby: 'ruby',
rb: 'ruby',
shell: 'shell',
sh: 'shell',
bash: 'shell',
toml: 'toml',
sql: 'sql',
xml: 'xml',
yaml: 'yaml',
yml: 'yaml',
};
module.exports.highlight = function() {
let codeBlocks = document.querySelectorAll('.page-content pre');
for (let i = 0; i < codeBlocks.length; i++) {
codeBlocks[i].innerHTML = codeBlocks[i].innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
let content = codeBlocks[i].textContent;
highlightElem(codeBlocks[i]);
}
};
function highlightElem(elem) {
let innerCodeElem = elem.querySelector('code[class^=language-]');
let mode = '';
if (innerCodeElem !== null) {
let langName = innerCodeElem.className.replace('language-', '');
mode = getMode(langName);
}
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
let content = elem.textContent;
CodeMirror(function(elt) {
codeBlocks[i].parentNode.replaceChild(elt, codeBlocks[i]);
elem.parentNode.replaceChild(elt, elem);
}, {
value: content,
mode: "",
mode: mode,
lineNumbers: true,
theme: 'base16-light',
readOnly: true
});
}
/**
* Search for a codemirror code based off a user suggestion
* @param suggestion
* @returns {string}
*/
function getMode(suggestion) {
suggestion = suggestion.trim().replace(/^\./g, '').toLowerCase();
return (typeof modeMap[suggestion] !== 'undefined') ? modeMap[suggestion] : '';
}
module.exports.highlightElem = highlightElem;
module.exports.wysiwygView = function(elem) {
let doc = elem.ownerDocument;
let codeElem = elem.querySelector('code');
let lang = (elem.className || '').replace('language-', '');
if (lang === '' && codeElem) {
lang = (codeElem.className || '').replace('language-', '')
}
elem.innerHTML = elem.innerHTML.replace(/<br\s*[\/]?>/gi ,'\n');
let content = elem.textContent;
let newWrap = doc.createElement('div');
let newTextArea = doc.createElement('textarea');
newWrap.className = 'CodeMirrorContainer';
newWrap.setAttribute('data-lang', lang);
newTextArea.style.display = 'none';
elem.parentNode.replaceChild(newWrap, elem);
newWrap.appendChild(newTextArea);
newWrap.contentEditable = false;
newTextArea.textContent = content;
let cm = CodeMirror(function(elt) {
newWrap.appendChild(elt);
}, {
value: content,
mode: getMode(lang),
lineNumbers: true,
theme: 'base16-light',
readOnly: true
});
setTimeout(() => {
cm.refresh();
}, 300);
return {wrap: newWrap, editor: cm};
};
module.exports.popupEditor = function(elem, modeSuggestion) {
let content = elem.textContent;
return CodeMirror(function(elt) {
elem.parentNode.insertBefore(elt, elem);
elem.style.display = 'none';
}, {
value: content,
mode: getMode(modeSuggestion),
lineNumbers: true,
theme: 'base16-light',
lineWrapping: true
});
};
module.exports.setMode = function(cmInstance, modeSuggestion) {
cmInstance.setOption('mode', getMode(modeSuggestion));
};
module.exports.setContent = function(cmInstance, codeContent) {
cmInstance.setValue(codeContent);
setTimeout(() => {
cmInstance.refresh();
}, 10);
};
module.exports.markdownEditor = function(elem) {
let content = elem.textContent;
let cm = CodeMirror(function(elt) {
return CodeMirror(function (elt) {
elem.parentNode.insertBefore(elt, elem);
elem.style.display = 'none';
}, {
@ -50,7 +169,10 @@ module.exports.markdownEditor = function(elem) {
theme: 'base16-light',
lineWrapping: true
});
return cm;
};
module.exports.getMetaKey = function() {
let mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault;
return mac ? "Cmd" : "Ctrl";
};

View File

@ -0,0 +1,53 @@
class BackToTop {
constructor(elem) {
this.elem = elem;
this.targetElem = document.getElementById('header');
this.showing = false;
this.breakPoint = 1200;
this.elem.addEventListener('click', this.scrollToTop.bind(this));
window.addEventListener('scroll', this.onPageScroll.bind(this));
}
onPageScroll() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!this.showing && scrollTopPos > this.breakPoint) {
this.elem.style.display = 'block';
this.showing = true;
setTimeout(() => {
this.elem.style.opacity = 0.4;
}, 1);
} else if (this.showing && scrollTopPos < this.breakPoint) {
this.elem.style.opacity = 0;
this.showing = false;
setTimeout(() => {
this.elem.style.display = 'none';
}, 500);
}
}
scrollToTop() {
let targetTop = this.targetElem.getBoundingClientRect().top;
let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body;
let duration = 300;
let start = Date.now();
let scrollStart = this.targetElem.getBoundingClientRect().top;
function setPos() {
let percentComplete = (1-((Date.now() - start) / duration));
let target = Math.abs(percentComplete * scrollStart);
if (percentComplete > 0) {
scrollElem.scrollTop = target;
requestAnimationFrame(setPos.bind(this));
} else {
scrollElem.scrollTop = targetTop;
}
}
requestAnimationFrame(setPos.bind(this));
}
}
module.exports = BackToTop;

View File

@ -0,0 +1,67 @@
class ChapterToggle {
constructor(elem) {
this.elem = elem;
this.isOpen = elem.classList.contains('open');
elem.addEventListener('click', this.click.bind(this));
}
open() {
let list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.add('open');
list.style.display = 'block';
list.style.height = '';
let height = list.getBoundingClientRect().height;
list.style.height = '0px';
list.style.overflow = 'hidden';
list.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
list.style.overflow = '';
list.style.height = '';
list.style.transition = '';
list.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
list.style.height = `${height}px`;
list.addEventListener('transitionend', transitionEndBound)
}, 1);
}
close() {
let list = this.elem.parentNode.querySelector('.inset-list');
this.elem.classList.remove('open');
list.style.display = 'block';
list.style.height = list.getBoundingClientRect().height + 'px';
list.style.overflow = 'hidden';
list.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
list.style.overflow = '';
list.style.height = '';
list.style.transition = '';
list.style.display = 'none';
list.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
list.style.height = `0px`;
list.addEventListener('transitionend', transitionEndBound)
}, 1);
}
click(event) {
event.preventDefault();
this.isOpen ? this.close() : this.open();
this.isOpen = !this.isOpen;
}
}
module.exports = ChapterToggle;

View File

@ -0,0 +1,48 @@
/**
* Dropdown
* Provides some simple logic to create simple dropdown menus.
*/
class DropDown {
constructor(elem) {
this.container = elem;
this.menu = elem.querySelector('ul');
this.toggle = elem.querySelector('[dropdown-toggle]');
this.setupListeners();
}
show() {
this.menu.style.display = 'block';
this.menu.classList.add('anim', 'menuIn');
this.container.addEventListener('mouseleave', this.hide.bind(this));
// Focus on first input if existing
let input = this.menu.querySelector('input');
if (input !== null) input.focus();
}
hide() {
this.menu.style.display = 'none';
this.menu.classList.remove('anim', 'menuIn');
}
setupListeners() {
// Hide menu on option click
this.container.addEventListener('click', event => {
let possibleChildren = Array.from(this.menu.querySelectorAll('a'));
if (possibleChildren.indexOf(event.target) !== -1) this.hide();
});
// Show dropdown on toggle click
this.toggle.addEventListener('click', this.show.bind(this));
// Hide menu on enter press
this.container.addEventListener('keypress', event => {
if (event.keyCode !== 13) return true;
event.preventDefault();
this.hide();
return false;
});
}
}
module.exports = DropDown;

View File

@ -0,0 +1,65 @@
class ExpandToggle {
constructor(elem) {
this.elem = elem;
this.isOpen = false;
this.selector = elem.getAttribute('expand-toggle');
elem.addEventListener('click', this.click.bind(this));
}
open(elemToToggle) {
elemToToggle.style.display = 'block';
elemToToggle.style.height = '';
let height = elemToToggle.getBoundingClientRect().height;
elemToToggle.style.height = '0px';
elemToToggle.style.overflow = 'hidden';
elemToToggle.style.transition = 'height ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
elemToToggle.style.overflow = '';
elemToToggle.style.height = '';
elemToToggle.style.transition = '';
elemToToggle.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
elemToToggle.style.height = `${height}px`;
elemToToggle.addEventListener('transitionend', transitionEndBound)
}, 1);
}
close(elemToToggle) {
elemToToggle.style.display = 'block';
elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px';
elemToToggle.style.overflow = 'hidden';
elemToToggle.style.transition = 'all ease-in-out 240ms';
let transitionEndBound = onTransitionEnd.bind(this);
function onTransitionEnd() {
elemToToggle.style.overflow = '';
elemToToggle.style.height = '';
elemToToggle.style.transition = '';
elemToToggle.style.display = 'none';
elemToToggle.removeEventListener('transitionend', transitionEndBound);
}
setTimeout(() => {
elemToToggle.style.height = `0px`;
elemToToggle.addEventListener('transitionend', transitionEndBound)
}, 1);
}
click(event) {
event.preventDefault();
let matchingElems = document.querySelectorAll(this.selector);
for (let i = 0, len = matchingElems.length; i < len; i++) {
this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]);
}
this.isOpen = !this.isOpen;
}
}
module.exports = ExpandToggle;

View File

@ -0,0 +1,28 @@
let componentMapping = {
'dropdown': require('./dropdown'),
'overlay': require('./overlay'),
'back-to-top': require('./back-top-top'),
'notification': require('./notification'),
'chapter-toggle': require('./chapter-toggle'),
'expand-toggle': require('./expand-toggle'),
};
window.components = {};
let componentNames = Object.keys(componentMapping);
for (let i = 0, len = componentNames.length; i < len; i++) {
let name = componentNames[i];
let elems = document.querySelectorAll(`[${name}]`);
if (elems.length === 0) continue;
let component = componentMapping[name];
if (typeof window.components[name] === "undefined") window.components[name] = [];
for (let j = 0, jLen = elems.length; j < jLen; j++) {
let instance = new component(elems[j]);
if (typeof elems[j].components === 'undefined') elems[j].components = {};
elems[j].components[name] = instance;
window.components[name].push(instance);
}
}

View File

@ -0,0 +1,41 @@
class Notification {
constructor(elem) {
this.elem = elem;
this.type = elem.getAttribute('notification');
this.textElem = elem.querySelector('span');
this.autohide = this.elem.hasAttribute('data-autohide');
window.Events.listen(this.type, text => {
this.show(text);
});
elem.addEventListener('click', this.hide.bind(this));
if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent);
this.hideCleanup = this.hideCleanup.bind(this);
}
show(textToShow = '') {
this.elem.removeEventListener('transitionend', this.hideCleanup);
this.textElem.textContent = textToShow;
this.elem.style.display = 'block';
setTimeout(() => {
this.elem.classList.add('showing');
}, 1);
if (this.autohide) setTimeout(this.hide.bind(this), 2000);
}
hide() {
this.elem.classList.remove('showing');
this.elem.addEventListener('transitionend', this.hideCleanup);
}
hideCleanup() {
this.elem.style.display = 'none';
this.elem.removeEventListener('transitionend', this.hideCleanup);
}
}
module.exports = Notification;

View File

@ -0,0 +1,39 @@
class Overlay {
constructor(elem) {
this.container = elem;
elem.addEventListener('click', event => {
if (event.target === elem) return this.hide();
});
let closeButtons = elem.querySelectorAll('.overlay-close');
for (let i=0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', this.hide.bind(this));
}
}
toggle(show = true) {
let start = Date.now();
let duration = 240;
function setOpacity() {
let elapsedTime = (Date.now() - start);
let targetOpacity = show ? (elapsedTime / duration) : 1-(elapsedTime / duration);
this.container.style.opacity = targetOpacity;
if (elapsedTime > duration) {
this.container.style.display = show ? 'flex' : 'none';
this.container.style.opacity = '';
} else {
requestAnimationFrame(setOpacity.bind(this));
}
}
requestAnimationFrame(setOpacity.bind(this));
}
hide() { this.toggle(false); }
show() { this.toggle(true); }
}
module.exports = Overlay;

View File

@ -8,256 +8,6 @@ moment.locale('en-gb');
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
$scope.images = [];
$scope.imageType = $attrs.imageType;
$scope.selectedImage = false;
$scope.dependantPages = false;
$scope.showing = false;
$scope.hasMore = false;
$scope.imageUpdateSuccess = false;
$scope.imageDeleteSuccess = false;
$scope.uploadedTo = $attrs.uploadedTo;
$scope.view = 'all';
$scope.searching = false;
$scope.searchTerm = '';
let page = 0;
let previousClickTime = 0;
let previousClickImage = 0;
let dataLoaded = false;
let callback = false;
let preSearchImages = [];
let preSearchHasMore = false;
/**
* Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return window.baseUrl('/images/' + $scope.imageType + '/upload');
};
/**
* Cancel the current search operation.
*/
function cancelSearch() {
$scope.searching = false;
$scope.searchTerm = '';
$scope.images = preSearchImages;
$scope.hasMore = preSearchHasMore;
}
$scope.cancelSearch = cancelSearch;
/**
* Runs on image upload, Adds an image to local list of images
* and shows a success message to the user.
* @param file
* @param data
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
events.emit('success', trans('components.image_upload_success'));
};
/**
* Runs the callback and hides the image manager.
* @param returnData
*/
function callbackAndHide(returnData) {
if (callback) callback(returnData);
$scope.hide();
}
/**
* Image select action. Checks if a double-click was fired.
* @param image
*/
$scope.imageSelect = function (image) {
let dblClickTime = 300;
let currentTime = Date.now();
let timeDiff = currentTime - previousClickTime;
if (timeDiff < dblClickTime && image.id === previousClickImage) {
// If double click
callbackAndHide(image);
} else {
// If single
$scope.selectedImage = image;
$scope.dependantPages = false;
}
previousClickTime = currentTime;
previousClickImage = image.id;
};
/**
* Action that runs when the 'Select image' button is clicked.
* Runs the callback and hides the image manager.
*/
$scope.selectButtonClick = function () {
callbackAndHide($scope.selectedImage);
};
/**
* Show the image manager.
* Takes a callback to execute later on.
* @param doneCallback
*/
function show(doneCallback) {
callback = doneCallback;
$scope.showing = true;
$('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240);
// Get initial images if they have not yet been loaded in.
if (!dataLoaded) {
fetchData();
dataLoaded = true;
}
}
// Connects up the image manger so it can be used externally
// such as from TinyMCE.
imageManagerService.show = show;
imageManagerService.showExternal = function (doneCallback) {
$scope.$apply(() => {
show(doneCallback);
});
};
window.ImageManager = imageManagerService;
/**
* Hide the image manager
*/
$scope.hide = function () {
$scope.showing = false;
$('#image-manager').find('.overlay').fadeOut(240);
};
let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/');
/**
* Fetch the list image data from the server.
*/
function fetchData() {
let url = baseUrl + page + '?';
let components = {};
if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo;
if ($scope.searching) components['term'] = $scope.searchTerm;
url += Object.keys(components).map((key) => {
return key + '=' + encodeURIComponent(components[key]);
}).join('&');
$http.get(url).then((response) => {
$scope.images = $scope.images.concat(response.data.images);
$scope.hasMore = response.data.hasMore;
page++;
});
}
$scope.fetchData = fetchData;
/**
* Start a search operation
*/
$scope.searchImages = function() {
if ($scope.searchTerm === '') {
cancelSearch();
return;
}
if (!$scope.searching) {
preSearchImages = $scope.images;
preSearchHasMore = $scope.hasMore;
}
$scope.searching = true;
$scope.images = [];
$scope.hasMore = false;
page = 0;
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/');
fetchData();
};
/**
* Set the current image listing view.
* @param viewName
*/
$scope.setView = function(viewName) {
cancelSearch();
$scope.images = [];
$scope.hasMore = false;
page = 0;
$scope.view = viewName;
baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/');
fetchData();
};
/**
* Save the details of an image.
* @param event
*/
$scope.saveImageDetails = function (event) {
event.preventDefault();
let url = window.baseUrl('/images/update/' + $scope.selectedImage.id);
$http.put(url, this.selectedImage).then(response => {
events.emit('success', trans('components.image_update_success'));
}, (response) => {
if (response.status === 422) {
let errors = response.data;
let message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
events.emit('error', message);
} else if (response.status === 403) {
events.emit('error', response.data.error);
}
});
};
/**
* Delete an image from system and notify of success.
* Checks if it should force delete when an image
* has dependant pages.
* @param event
*/
$scope.deleteImage = function (event) {
event.preventDefault();
let force = $scope.dependantPages !== false;
let url = window.baseUrl('/images/' + $scope.selectedImage.id);
if (force) url += '?force=true';
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
events.emit('success', trans('components.image_delete_success'));
}, (response) => {
// Pages failure
if (response.status === 400) {
$scope.dependantPages = response.data;
} else if (response.status === 403) {
events.emit('error', response.data.error);
}
});
};
/**
* Simple date creator used to properly format dates.
* @param stringDate
* @returns {Date}
*/
$scope.getDate = function (stringDate) {
return new Date(stringDate);
};
}]);
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {
@ -370,14 +120,8 @@ module.exports = function (ngApp, events) {
saveDraft();
};
// Listen to shortcuts coming via events
$scope.$on('editor-keydown', (event, data) => {
// Save shortcut (ctrl+s)
if (data.keyCode == 83 && (navigator.platform.match("Mac") ? data.metaKey : data.ctrlKey)) {
data.preventDefault();
saveDraft();
}
});
// Listen to save draft events from editor
$scope.$on('save-draft', saveDraft);
/**
* Discard the current draft and grab the current page
@ -385,7 +129,7 @@ module.exports = function (ngApp, events) {
*/
$scope.discardDraft = function () {
let url = window.baseUrl('/ajax/page/' + pageId);
$http.get(url).then((responseData) => {
$http.get(url).then(responseData => {
if (autoSave) $interval.cancel(autoSave);
$scope.draftText = trans('entities.pages_editing_page');
$scope.isUpdateDraft = false;
@ -401,90 +145,6 @@ module.exports = function (ngApp, events) {
}]);
ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
function ($scope, $http, $attrs) {
const pageId = Number($attrs.pageId);
$scope.tags = [];
$scope.sortOptions = {
handle: '.handle',
items: '> tr',
containment: "parent",
axis: "y"
};
/**
* Push an empty tag to the end of the scope tags.
*/
function addEmptyTag() {
$scope.tags.push({
name: '',
value: ''
});
}
$scope.addEmptyTag = addEmptyTag;
/**
* Get all tags for the current book and add into scope.
*/
function getTags() {
let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`);
$http.get(url).then((responseData) => {
$scope.tags = responseData.data;
addEmptyTag();
});
}
getTags();
/**
* Set the order property on all tags.
*/
function setTagOrder() {
for (let i = 0; i < $scope.tags.length; i++) {
$scope.tags[i].order = i;
}
}
/**
* When an tag changes check if another empty editable
* field needs to be added onto the end.
* @param tag
*/
$scope.tagChange = function(tag) {
let cPos = $scope.tags.indexOf(tag);
if (cPos !== $scope.tags.length-1) return;
if (tag.name !== '' || tag.value !== '') {
addEmptyTag();
}
};
/**
* When an tag field loses focus check the tag to see if its
* empty and therefore could be removed from the list.
* @param tag
*/
$scope.tagBlur = function(tag) {
let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
if (tag.name === '' && tag.value === '' && !isLast) {
let cPos = $scope.tags.indexOf(tag);
$scope.tags.splice(cPos, 1);
}
};
/**
* Remove a tag from the current list.
* @param tag
*/
$scope.removeTag = function(tag) {
let cIndex = $scope.tags.indexOf(tag);
$scope.tags.splice(cIndex, 1);
};
}]);
ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs',
function ($scope, $http, $attrs) {

View File

@ -114,39 +114,6 @@ module.exports = function (ngApp, events) {
};
}]);
/**
* Dropdown
* Provides some simple logic to create small dropdown menus
*/
ngApp.directive('dropdown', [function () {
return {
restrict: 'A',
link: function (scope, element, attrs) {
const menu = element.find('ul');
element.find('[dropdown-toggle]').on('click', function () {
menu.show().addClass('anim menuIn');
let inputs = menu.find('input');
let hasInput = inputs.length > 0;
if (hasInput) {
inputs.first().focus();
element.on('keypress', 'input', event => {
if (event.keyCode === 13) {
event.preventDefault();
menu.hide();
menu.removeClass('anim menuIn');
return false;
}
});
}
element.mouseleave(function () {
menu.hide();
menu.removeClass('anim menuIn');
});
});
}
};
}]);
/**
* TinyMCE
* An angular wrapper around the tinyMCE editor.
@ -187,30 +154,6 @@ module.exports = function (ngApp, events) {
}
scope.tinymce.extraSetups.push(tinyMceSetup);
// Custom tinyMCE plugins
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'
});
});
tinymce.init(scope.tinymce);
}
}
@ -232,15 +175,48 @@ module.exports = function (ngApp, events) {
},
link: function (scope, element, attrs) {
// Set initial model content
element = element.find('textarea').first();
// 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">', '</div>');};
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();
@ -257,6 +233,166 @@ module.exports = function (ngApp, events) {
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 + (newLineContent.length - lineLen)});
}
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.showEntityLinkSelector(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);
@ -267,6 +403,9 @@ module.exports = function (ngApp, events) {
}
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);
@ -287,8 +426,7 @@ module.exports = function (ngApp, events) {
restrict: 'A',
link: function (scope, element, attrs) {
// Elements
const $input = element.find('[markdown-input] textarea').first();
// 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"]');
@ -299,11 +437,9 @@ module.exports = function (ngApp, events) {
window.open(this.getAttribute('href'));
});
let currentCaretPos = 0;
$input.blur(event => {
currentCaretPos = $input[0].selectionStart;
});
// 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) => {
@ -315,140 +451,6 @@ module.exports = function (ngApp, events) {
}, {queue: false, duration: 200, easing: 'linear'});
}
});
// Editor key-presses
$input.keydown(event => {
// Insert image shortcut
if (event.which === 73 && event.ctrlKey && event.shiftKey) {
event.preventDefault();
let caretPos = $input[0].selectionStart;
let currentContent = $input.val();
const mdImageText = "![](http://)";
$input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
$input.focus();
$input[0].selectionStart = caretPos + ("![](".length);
$input[0].selectionEnd = caretPos + ('![](http://'.length);
return;
}
// Insert entity link shortcut
if (event.which === 75 && event.ctrlKey && event.shiftKey) {
showLinkSelector();
return;
}
// Pass key presses to controller via event
scope.$emit('editor-keydown', event);
});
// Insert image from image manager
$insertImage.click(event => {
window.ImageManager.showExternal(image => {
let caretPos = currentCaretPos;
let currentContent = $input.val();
let mdImageText = "![" + image.name + "](" + image.thumbs.display + ")";
$input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos));
$input.change();
});
});
function showLinkSelector() {
window.showEntityLinkSelector((entity) => {
let selectionStart = currentCaretPos;
let selectionEnd = $input[0].selectionEnd;
let textSelected = (selectionEnd !== selectionStart);
let currentContent = $input.val();
if (textSelected) {
let selectedText = currentContent.substring(selectionStart, selectionEnd);
let linkText = `[${selectedText}](${entity.link})`;
$input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionEnd));
} else {
let linkText = ` [${entity.name}](${entity.link}) `;
$input.val(currentContent.substring(0, selectionStart) + linkText + currentContent.substring(selectionStart))
}
$input.change();
});
}
$insertEntityLink.click(showLinkSelector);
// Upload and insert image on paste
function editorPaste(e) {
e = e.originalEvent;
if (!e.clipboardData) return
let items = e.clipboardData.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
uploadImage(items[i].getAsFile());
}
}
$input.on('paste', editorPaste);
// Handle image drop, Uploads images to BookStack.
function handleImageDrop(event) {
event.stopPropagation();
event.preventDefault();
let files = event.originalEvent.dataTransfer.files;
for (let i = 0; i < files.length; i++) {
uploadImage(files[i]);
}
}
$input.on('drop', handleImageDrop);
// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file.type.indexOf('image') !== 0) return;
let formData = new FormData();
let ext = 'png';
let xhr = new XMLHttpRequest();
if (file.name) {
let fileNameMatches = file.name.match(/\.(.+)$/);
if (fileNameMatches) {
ext = fileNameMatches[1];
}
}
// Insert image into markdown
let id = "image-" + Math.random().toString(16).slice(2);
let selectStart = $input[0].selectionStart;
let selectEnd = $input[0].selectionEnd;
let content = $input[0].value;
let selectText = content.substring(selectStart, selectEnd);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let innerContent = ((selectEnd > selectStart) ? `![${selectText}]` : '![]') + `(${placeholderImage})`;
$input[0].value = content.substring(0, selectStart) + innerContent + content.substring(selectEnd);
$input.focus();
$input[0].selectionStart = selectStart;
$input[0].selectionEnd = selectStart;
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 () {
let selectStart = $input[0].selectionStart;
if (xhr.status === 200 || xhr.status === 201) {
let result = JSON.parse(xhr.responseText);
$input[0].value = $input[0].value.replace(placeholderImage, result.thumbs.display);
$input.change();
} else {
console.log(trans('errors.image_upload_error'));
console.log(xhr.responseText);
$input[0].value = $input[0].value.replace(innerContent, '');
$input.change();
}
$input.focus();
$input[0].selectionStart = selectStart;
$input[0].selectionEnd = selectStart;
};
xhr.send(formData);
}
}
}
}]);
@ -494,188 +496,6 @@ module.exports = function (ngApp, events) {
}
}]);
/**
* Tag Autosuggestions
* Listens to child inputs and provides autosuggestions depending on field type
* and input. Suggestions provided by server.
*/
ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
return {
restrict: 'A',
link: function (scope, elem, attrs) {
// Local storage for quick caching.
const localCache = {};
// Create suggestion element
const suggestionBox = document.createElement('ul');
suggestionBox.className = 'suggestion-box';
suggestionBox.style.position = 'absolute';
suggestionBox.style.display = 'none';
const $suggestionBox = $(suggestionBox);
// General state tracking
let isShowing = false;
let currentInput = false;
let active = 0;
// Listen to input events on autosuggest fields
elem.on('input focus', '[autosuggest]', function (event) {
let $input = $(this);
let val = $input.val();
let url = $input.attr('autosuggest');
let type = $input.attr('autosuggest-type');
// Add name param to request if for a value
if (type.toLowerCase() === 'value') {
let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
let nameVal = $nameInput.val();
if (nameVal !== '') {
url += '?name=' + encodeURIComponent(nameVal);
}
}
let suggestionPromise = getSuggestions(val.slice(0, 3), url);
suggestionPromise.then(suggestions => {
if (val.length === 0) {
displaySuggestions($input, suggestions.slice(0, 6));
} else {
suggestions = suggestions.filter(item => {
return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
}).slice(0, 4);
displaySuggestions($input, suggestions);
}
});
});
// Hide autosuggestions when input loses focus.
// Slight delay to allow clicks.
let lastFocusTime = 0;
elem.on('blur', '[autosuggest]', function (event) {
let startTime = Date.now();
setTimeout(() => {
if (lastFocusTime < startTime) {
$suggestionBox.hide();
isShowing = false;
}
}, 200)
});
elem.on('focus', '[autosuggest]', function (event) {
lastFocusTime = Date.now();
});
elem.on('keydown', '[autosuggest]', function (event) {
if (!isShowing) return;
let suggestionElems = suggestionBox.childNodes;
let suggestCount = suggestionElems.length;
// Down arrow
if (event.keyCode === 40) {
let newActive = (active === suggestCount - 1) ? 0 : active + 1;
changeActiveTo(newActive, suggestionElems);
}
// Up arrow
else if (event.keyCode === 38) {
let newActive = (active === 0) ? suggestCount - 1 : active - 1;
changeActiveTo(newActive, suggestionElems);
}
// Enter or tab key
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
currentInput[0].value = suggestionElems[active].textContent;
currentInput.focus();
$suggestionBox.hide();
isShowing = false;
if (event.keyCode === 13) {
event.preventDefault();
return false;
}
}
});
// Change the active suggestion to the given index
function changeActiveTo(index, suggestionElems) {
suggestionElems[active].className = '';
active = index;
suggestionElems[active].className = 'active';
}
// Display suggestions on a field
let prevSuggestions = [];
function displaySuggestions($input, suggestions) {
// Hide if no suggestions
if (suggestions.length === 0) {
$suggestionBox.hide();
isShowing = false;
prevSuggestions = suggestions;
return;
}
// Otherwise show and attach to input
if (!isShowing) {
$suggestionBox.show();
isShowing = true;
}
if ($input !== currentInput) {
$suggestionBox.detach();
$input.after($suggestionBox);
currentInput = $input;
}
// Return if no change
if (prevSuggestions.join() === suggestions.join()) {
prevSuggestions = suggestions;
return;
}
// Build suggestions
$suggestionBox[0].innerHTML = '';
for (let i = 0; i < suggestions.length; i++) {
let suggestion = document.createElement('li');
suggestion.textContent = suggestions[i];
suggestion.onclick = suggestionClick;
if (i === 0) {
suggestion.className = 'active';
active = 0;
}
$suggestionBox[0].appendChild(suggestion);
}
prevSuggestions = suggestions;
}
// Suggestion click event
function suggestionClick(event) {
currentInput[0].value = this.textContent;
currentInput.focus();
$suggestionBox.hide();
isShowing = false;
}
// Get suggestions & cache
function getSuggestions(input, url) {
let hasQuery = url.indexOf('?') !== -1;
let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
// Get from local cache if exists
if (typeof localCache[searchUrl] !== 'undefined') {
return new Promise((resolve, reject) => {
resolve(localCache[searchUrl]);
});
}
return $http.get(searchUrl).then(response => {
localCache[searchUrl] = response.data;
return response.data;
});
}
}
}
}]);
ngApp.directive('entityLinkSelector', [function($http) {
return {
restrict: 'A',
@ -711,6 +531,7 @@ module.exports = function (ngApp, events) {
function hide() {
element.fadeOut(240);
}
scope.hide = hide;
// Listen to confirmation of entity selections (doubleclick)
events.listen('entity-select-confirm', entity => {

View File

@ -1,4 +1,5 @@
"use strict";
require("babel-polyfill");
// Url retrieval function
window.baseUrl = function(path) {
@ -17,11 +18,9 @@ let axiosInstance = axios.create({
'baseURL': window.baseUrl('')
}
});
window.$http = axiosInstance;
Vue.prototype.$http = axiosInstance;
require("./vues/vues");
// AngularJS - Create application and load components
const angular = require("angular");
@ -64,11 +63,12 @@ class EventManager {
window.Events = new EventManager();
Vue.prototype.$events = window.Events;
require("./vues/vues");
require("./components");
// Load in angular specific items
const Services = require('./services');
const Directives = require('./directives');
const Controllers = require('./controllers');
Services(ngApp, window.Events);
Directives(ngApp, window.Events);
Controllers(ngApp, window.Events);
@ -90,83 +90,11 @@ jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) {
};
});
// Global jQuery Elements
let notifications = $('.notification');
let successNotification = notifications.filter('.pos');
let errorNotification = notifications.filter('.neg');
let warningNotification = notifications.filter('.warning');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('warning', function (text) {
warningNotification.find('span').text(text);
warningNotification.show();
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
notifications.click(function () {
$(this).fadeOut(100);
});
// Chapter page list toggles
$('.chapter-toggle').click(function (e) {
e.preventDefault();
$(this).toggleClass('open');
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
let scrollTopShowing = false;
let scrollTop = document.getElementById('back-to-top');
let scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) {
scrollTop.style.display = 'block';
scrollTopShowing = true;
setTimeout(() => {
scrollTop.style.opacity = 0.4;
}, 1);
} else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
// Common jQuery actions
$('[data-action="expand-entity-list-details"]').click(function() {
$('.entity-list.compact').find('p').not('.empty-text').slideToggle(240);
});
// Popup close
$('.popup-close').click(function() {
$(this).closest('.overlay').fadeOut(240);
});
$('.overlay').click(function(event) {
if (!$(event.target).hasClass('overlay')) return;
$(this).fadeOut(240);
});
// Detect IE for css
if(navigator.userAgent.indexOf('MSIE')!==-1
|| navigator.appVersion.indexOf('Trident/') > 0
|| navigator.userAgent.indexOf('Safari') !== -1){
$('body').addClass('flexbox-support');
document.body.classList.add('flexbox-support');
}
// Page specific items

View File

@ -1,5 +1,7 @@
"use strict";
const Code = require('../code');
/**
* Handle pasting images from clipboard.
* @param e - event
@ -50,23 +52,183 @@ function editorPaste(e, editor) {
function registerEditorShortcuts(editor) {
// Headers
for (let i = 1; i < 5; i++) {
editor.addShortcut('meta+' + i, '', ['FormatBlock', false, 'h' + i]);
editor.shortcuts.add('meta+' + i, '', ['FormatBlock', false, 'h' + (i+1)]);
}
// Other block shortcuts
editor.addShortcut('meta+q', '', ['FormatBlock', false, 'blockquote']);
editor.addShortcut('meta+d', '', ['FormatBlock', false, 'p']);
editor.addShortcut('meta+e', '', ['FormatBlock', false, 'pre']);
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
editor.shortcuts.add('meta+5', '', ['FormatBlock', false, 'p']);
editor.shortcuts.add('meta+d', '', ['FormatBlock', false, 'p']);
editor.shortcuts.add('meta+6', '', ['FormatBlock', false, 'blockquote']);
editor.shortcuts.add('meta+q', '', ['FormatBlock', false, 'blockquote']);
editor.shortcuts.add('meta+7', '', ['codeeditor', false, 'pre']);
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']);
// Loop through callout styles
editor.shortcuts.add('meta+9', '', function() {
let selectedNode = editor.selection.getNode();
let formats = ['info', 'success', 'warning', 'danger'];
if (!selectedNode || selectedNode.className.indexOf('callout') === -1) {
editor.formatter.apply('calloutinfo');
return;
}
for (let i = 0; i < formats.length; i++) {
if (selectedNode.className.indexOf(formats[i]) === -1) continue;
let newFormat = (i === formats.length -1) ? formats[0] : formats[i+1];
editor.formatter.apply('callout' + newFormat);
return;
}
editor.formatter.apply('p');
});
}
/**
* Create and enable our custom code plugin
*/
function codePlugin() {
function elemIsCodeBlock(elem) {
return elem.className === 'CodeMirrorContainer';
}
function showPopup(editor) {
let selectedNode = editor.selection.getNode();
if (!elemIsCodeBlock(selectedNode)) {
let providedCode = editor.selection.getNode().textContent;
window.vues['code-editor'].open(providedCode, '', (code, lang) => {
let wrap = document.createElement('div');
wrap.innerHTML = `<pre><code class="language-${lang}"></code></pre>`;
wrap.querySelector('code').innerText = code;
editor.formatter.toggle('pre');
let node = editor.selection.getNode();
editor.dom.setHTML(node, wrap.querySelector('pre').innerHTML);
editor.fire('SetContent');
});
return;
}
let lang = selectedNode.hasAttribute('data-lang') ? selectedNode.getAttribute('data-lang') : '';
let currentCode = selectedNode.querySelector('textarea').textContent;
window.vues['code-editor'].open(currentCode, lang, (code, lang) => {
let editorElem = selectedNode.querySelector('.CodeMirror');
let cmInstance = editorElem.CodeMirror;
if (cmInstance) {
Code.setContent(cmInstance, code);
Code.setMode(cmInstance, lang);
}
let textArea = selectedNode.querySelector('textarea');
if (textArea) textArea.textContent = code;
selectedNode.setAttribute('data-lang', lang);
});
}
function codeMirrorContainerToPre($codeMirrorContainer) {
let textArea = $codeMirrorContainer[0].querySelector('textarea');
let code = textArea.textContent;
let lang = $codeMirrorContainer[0].getAttribute('data-lang');
$codeMirrorContainer.removeAttr('contentEditable');
let $pre = $('<pre></pre>');
$pre.append($('<code></code>').each((index, elem) => {
// Needs to be textContent since innerText produces BR:s
elem.textContent = code;
}).attr('class', `language-${lang}`));
$codeMirrorContainer.replaceWith($pre);
}
window.tinymce.PluginManager.add('codeeditor', function(editor, url) {
let $ = editor.$;
editor.addButton('codeeditor', {
text: 'Code block',
icon: false,
cmd: 'codeeditor'
});
editor.addCommand('codeeditor', () => {
showPopup(editor);
});
// Convert
editor.on('PreProcess', function (e) {
$('div.CodeMirrorContainer', e.node).
each((index, elem) => {
let $elem = $(elem);
codeMirrorContainerToPre($elem);
});
});
editor.on('dblclick', event => {
let selectedNode = editor.selection.getNode();
if (!elemIsCodeBlock(selectedNode)) return;
showPopup(editor);
});
editor.on('SetContent', function () {
// Recover broken codemirror instances
$('.CodeMirrorContainer').filter((index ,elem) => {
return typeof elem.querySelector('.CodeMirror').CodeMirror === 'undefined';
}).each((index, elem) => {
codeMirrorContainerToPre($(elem));
});
let codeSamples = $('body > pre').filter((index, elem) => {
return elem.contentEditable !== "false";
});
if (!codeSamples.length) return;
editor.undoManager.transact(function () {
codeSamples.each((index, elem) => {
Code.wysiwygView(elem);
});
});
});
});
}
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'
});
});
}
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,
@ -77,10 +239,10 @@ module.exports = function() {
paste_data_images: false,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codesample",
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 codesample",
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"},
@ -89,20 +251,25 @@ module.exports = function() {
{title: "Header Tiny", format: "h5"},
{title: "Paragraph", format: "p", exact: true, classes: ''},
{title: "Blockquote", format: "blockquote"},
{title: "Code Block", icon: "code", format: "pre"},
{title: "Code Block", icon: "code", cmd: 'codeeditor', format: 'codeeditor'},
{title: "Inline Code", icon: "code", inline: "code"},
{title: "Callouts", items: [
{title: "Success", block: 'p', exact: true, attributes : {'class' : 'callout success'}},
{title: "Info", block: 'p', exact: true, attributes : {'class' : 'callout info'}},
{title: "Warning", block: 'p', exact: true, attributes : {'class' : 'callout warning'}},
{title: "Danger", block: 'p', exact: true, attributes : {'class' : 'callout danger'}}
]}
{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) {
@ -116,7 +283,7 @@ module.exports = function() {
if (type === 'image') {
// Show image manager
window.ImageManager.showExternal(function (image) {
window.ImageManager.show(function (image) {
// Set popover link input to image url then fire change event
// to ensure the new value sticks
@ -198,7 +365,7 @@ module.exports = function() {
icon: 'image',
tooltip: 'Insert an image',
onclick: function () {
window.ImageManager.showExternal(function (image) {
window.ImageManager.show(function (image) {
let html = `<a href="${image.url}" target="_blank">`;
html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
html += '</a>';

View File

@ -1,5 +1,3 @@
"use strict";
// Configure ZeroClipboard
const Clipboard = require("clipboard");
const Code = require('../code');

View File

@ -1,12 +0,0 @@
"use strict";
module.exports = function(ngApp, events) {
ngApp.factory('imageManagerService', function() {
return {
show: false,
showExternal: false
};
});
};

View File

@ -0,0 +1,43 @@
const codeLib = require('../code');
const methods = {
show() {
if (!this.editor) this.editor = codeLib.popupEditor(this.$refs.editor, this.language);
this.$refs.overlay.style.display = 'flex';
},
hide() {
this.$refs.overlay.style.display = 'none';
},
updateEditorMode(language) {
codeLib.setMode(this.editor, language);
},
updateLanguage(lang) {
this.language = lang;
this.updateEditorMode(lang);
},
open(code, language, callback) {
this.show();
this.updateEditorMode(language);
this.language = language;
codeLib.setContent(this.editor, code);
this.code = code;
this.callback = callback;
},
save() {
if (!this.callback) return;
this.callback(this.editor.getValue(), this.language);
this.hide();
}
};
const data = {
editor: null,
language: '',
code: '',
callback: null
};
module.exports = {
methods,
data
};

View File

@ -0,0 +1,130 @@
const template = `
<div>
<input :value="value" :autosuggest-type="type" ref="input"
:placeholder="placeholder" :name="name"
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
@blur="inputBlur"
@keydown="inputKeydown"
/>
<ul class="suggestion-box" v-if="showSuggestions">
<li v-for="(suggestion, i) in suggestions"
@click="selectSuggestion(suggestion)"
:class="{active: (i === active)}">{{suggestion}}</li>
</ul>
</div>
`;
function data() {
return {
suggestions: [],
showSuggestions: false,
active: 0,
};
}
const ajaxCache = {};
const props = ['url', 'type', 'value', 'placeholder', 'name'];
function getNameInputVal(valInput) {
let parentRow = valInput.parentNode.parentNode;
let nameInput = parentRow.querySelector('[autosuggest-type="name"]');
return (nameInput === null) ? '' : nameInput.value;
}
const methods = {
inputUpdate(inputValue) {
this.$emit('input', inputValue);
let params = {};
if (this.type === 'value') {
let nameVal = getNameInputVal(this.$el);
if (nameVal !== "") params.name = nameVal;
}
this.getSuggestions(inputValue.slice(0, 3), params).then(suggestions => {
if (inputValue.length === 0) {
this.displaySuggestions(suggestions.slice(0, 6));
return;
}
// Filter to suggestions containing searched term
suggestions = suggestions.filter(item => {
return item.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
}).slice(0, 4);
this.displaySuggestions(suggestions);
});
},
inputBlur() {
setTimeout(() => {
this.$emit('blur');
this.showSuggestions = false;
}, 100);
},
inputKeydown(event) {
if (event.keyCode === 13) event.preventDefault();
if (!this.showSuggestions) return;
// Down arrow
if (event.keyCode === 40) {
this.active = (this.active === this.suggestions.length - 1) ? 0 : this.active+1;
}
// Up Arrow
else if (event.keyCode === 38) {
this.active = (this.active === 0) ? this.suggestions.length - 1 : this.active-1;
}
// Enter or tab keys
else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
this.selectSuggestion(this.suggestions[this.active]);
}
// Escape key
else if (event.keyCode === 27) {
this.showSuggestions = false;
}
},
displaySuggestions(suggestions) {
if (suggestions.length === 0) {
this.suggestions = [];
this.showSuggestions = false;
return;
}
this.suggestions = suggestions;
this.showSuggestions = true;
this.active = 0;
},
selectSuggestion(suggestion) {
this.$refs.input.value = suggestion;
this.$refs.input.focus();
this.$emit('input', suggestion);
this.showSuggestions = false;
},
/**
* Get suggestions from BookStack. Store and use local cache if already searched.
* @param {String} input
* @param {Object} params
*/
getSuggestions(input, params) {
params.search = input;
let cacheKey = `${this.url}:${JSON.stringify(params)}`;
if (typeof ajaxCache[cacheKey] !== "undefined") return Promise.resolve(ajaxCache[cacheKey]);
return this.$http.get(this.url, {params}).then(resp => {
ajaxCache[cacheKey] = resp.data;
return resp.data;
});
}
};
const computed = [];
module.exports = {template, data, props, methods, computed};

View File

@ -0,0 +1,60 @@
const DropZone = require("dropzone");
const template = `
<div class="dropzone-container">
<div class="dz-message">{{placeholder}}</div>
</div>
`;
const props = ['placeholder', 'uploadUrl', 'uploadedTo'];
// TODO - Remove jQuery usage
function mounted() {
let container = this.$el;
let _this = this;
new DropZone(container, {
url: function() {
return _this.uploadUrl;
},
init: function () {
let dz = this;
dz.on('sending', function (file, xhr, data) {
let token = window.document.querySelector('meta[name=token]').getAttribute('content');
data.append('_token', token);
let uploadedTo = typeof _this.uploadedTo === 'undefined' ? 0 : _this.uploadedTo;
data.append('uploaded_to', uploadedTo);
});
dz.on('success', function (file, data) {
_this.$emit('success', {file, data});
$(file.previewElement).fadeOut(400, function () {
dz.removeFile(file);
});
});
dz.on('error', function (file, errorMessage, xhr) {
_this.$emit('error', {file, errorMessage, xhr});
console.log(errorMessage);
console.log(xhr);
function setMessage(message) {
$(file.previewElement).find('[data-dz-errormessage]').text(message);
}
if (xhr.status === 413) setMessage(trans('errors.server_upload_limit'));
if (errorMessage.file) setMessage(errorMessage.file[0]);
});
}
});
}
function data() {
return {}
}
module.exports = {
template,
props,
mounted,
data,
};

View File

@ -0,0 +1,182 @@
const dropzone = require('./components/dropzone');
let page = 0;
let previousClickTime = 0;
let previousClickImage = 0;
let dataLoaded = false;
let callback = false;
let baseUrl = '';
let preSearchImages = [];
let preSearchHasMore = false;
const data = {
images: [],
imageType: false,
uploadedTo: false,
selectedImage: false,
dependantPages: false,
showing: false,
view: 'all',
hasMore: false,
searching: false,
searchTerm: '',
imageUpdateSuccess: false,
imageDeleteSuccess: false,
};
const methods = {
show(providedCallback) {
callback = providedCallback;
this.showing = true;
this.$el.children[0].components.overlay.show();
// Get initial images if they have not yet been loaded in.
if (dataLoaded) return;
this.fetchData();
dataLoaded = true;
},
hide() {
this.showing = false;
this.$el.children[0].components.overlay.hide();
},
fetchData() {
let url = baseUrl + page;
let query = {};
if (this.uploadedTo !== false) query.page_id = this.uploadedTo;
if (this.searching) query.term = this.searchTerm;
this.$http.get(url, {params: query}).then(response => {
this.images = this.images.concat(response.data.images);
this.hasMore = response.data.hasMore;
page++;
});
},
setView(viewName) {
this.cancelSearch();
this.images = [];
this.hasMore = false;
page = 0;
this.view = viewName;
baseUrl = window.baseUrl(`/images/${this.imageType}/${viewName}/`);
this.fetchData();
},
searchImages() {
if (this.searchTerm === '') return this.cancelSearch();
// Cache current settings for later
if (!this.searching) {
preSearchImages = this.images;
preSearchHasMore = this.hasMore;
}
this.searching = true;
this.images = [];
this.hasMore = false;
page = 0;
baseUrl = window.baseUrl(`/images/${this.imageType}/search/`);
this.fetchData();
},
cancelSearch() {
this.searching = false;
this.searchTerm = '';
this.images = preSearchImages;
this.hasMore = preSearchHasMore;
},
imageSelect(image) {
let dblClickTime = 300;
let currentTime = Date.now();
let timeDiff = currentTime - previousClickTime;
let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage;
if (isDblClick) {
this.callbackAndHide(image);
} else {
this.selectedImage = image;
this.dependantPages = false;
}
previousClickTime = currentTime;
previousClickImage = image.id;
},
callbackAndHide(imageResult) {
if (callback) callback(imageResult);
this.hide();
},
saveImageDetails() {
let url = window.baseUrl(`/images/update/${this.selectedImage.id}`);
this.$http.put(url, this.selectedImage).then(response => {
this.$events.emit('success', trans('components.image_update_success'));
}).catch(error => {
if (error.response.status === 422) {
let errors = error.response.data;
let message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
this.$events.emit('error', message);
} else if (error.response.status === 403) {
this.$events.emit('error', error.response.data.error);
}
});
},
deleteImage() {
let force = this.dependantPages !== false;
let url = window.baseUrl('/images/' + this.selectedImage.id);
if (force) url += '?force=true';
this.$http.delete(url).then(response => {
this.images.splice(this.images.indexOf(this.selectedImage), 1);
this.selectedImage = false;
this.$events.emit('success', trans('components.image_delete_success'));
}).catch(error=> {
if (error.response.status === 400) {
this.dependantPages = error.response.data;
} else if (error.response.status === 403) {
this.$events.emit('error', error.response.data.error);
}
});
},
getDate(stringDate) {
return new Date(stringDate);
},
uploadSuccess(event) {
this.images.unshift(event.data);
this.$events.emit('success', trans('components.image_upload_success'));
},
};
const computed = {
uploadUrl() {
return window.baseUrl(`/images/${this.imageType}/upload`);
}
};
function mounted() {
window.ImageManager = this;
this.imageType = this.$el.getAttribute('image-type');
this.uploadedTo = this.$el.getAttribute('uploaded-to');
baseUrl = window.baseUrl('/images/' + this.imageType + '/all/')
}
module.exports = {
mounted,
methods,
data,
computed,
components: {dropzone},
};

View File

@ -149,7 +149,7 @@ let methods = {
updateSearch(e) {
e.preventDefault();
window.location = '/search?term=' + encodeURIComponent(this.termString);
window.location = window.baseUrl('/search?term=' + encodeURIComponent(this.termString));
},
enableDate(optionName) {

View File

@ -0,0 +1,68 @@
const draggable = require('vuedraggable');
const autosuggest = require('./components/autosuggest');
let data = {
pageId: false,
tags: [],
};
const components = {draggable, autosuggest};
const directives = {};
let computed = {};
let methods = {
addEmptyTag() {
this.tags.push({name: '', value: '', key: Math.random().toString(36).substring(7)});
},
/**
* When an tag changes check if another empty editable field needs to be added onto the end.
* @param tag
*/
tagChange(tag) {
let tagPos = this.tags.indexOf(tag);
if (tagPos === this.tags.length-1 && (tag.name !== '' || tag.value !== '')) this.addEmptyTag();
},
/**
* When an tag field loses focus check the tag to see if its
* empty and therefore could be removed from the list.
* @param tag
*/
tagBlur(tag) {
let isLast = (this.tags.indexOf(tag) === this.tags.length-1);
if (tag.name !== '' || tag.value !== '' || isLast) return;
let cPos = this.tags.indexOf(tag);
this.tags.splice(cPos, 1);
},
removeTag(tag) {
let tagPos = this.tags.indexOf(tag);
if (tagPos === -1) return;
this.tags.splice(tagPos, 1);
},
getTagFieldName(index, key) {
return `tags[${index}][${key}]`;
},
};
function mounted() {
this.pageId = Number(this.$el.getAttribute('page-id'));
let url = window.baseUrl(`/ajax/tags/get/page/${this.pageId}`);
this.$http.get(url).then(response => {
let tags = response.data;
for (let i = 0, len = tags.length; i < len; i++) {
tags[i].key = Math.random().toString(36).substring(7);
}
this.tags = tags;
this.addEmptyTag();
});
}
module.exports = {
data, computed, methods, mounted, components, directives
};

View File

@ -7,12 +7,17 @@ function exists(id) {
let vueMapping = {
'search-system': require('./search'),
'entity-dashboard': require('./entity-search'),
'code-editor': require('./code-editor'),
'image-manager': require('./image-manager'),
'tag-manager': require('./tag-manager'),
};
window.vues = {};
Object.keys(vueMapping).forEach(id => {
if (exists(id)) {
let config = vueMapping[id];
config.el = '#' + id;
new Vue(config);
window.vues[id] = new Vue(config);
}
});

View File

@ -36,41 +36,12 @@
}
}
.anim.notification {
transform: translate3d(580px, 0, 0);
animation-name: notification;
animation-duration: 3s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
&.stopped {
animation-name: notificationStopped;
}
}
@keyframes notification {
0% {
transform: translate3d(580px, 0, 0);
}
10% {
transform: translate3d(0, 0, 0);
}
90% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(580px, 0, 0);
}
}
@keyframes notificationStopped {
0% {
transform: translate3d(580px, 0, 0);
}
10% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
.anim.menuIn {
transform-origin: 100% 0%;
animation-name: menuIn;
animation-duration: 120ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
@keyframes menuIn {
@ -85,14 +56,6 @@
}
}
.anim.menuIn {
transform-origin: 100% 0%;
animation-name: menuIn;
animation-duration: 120ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
@keyframes loadingBob {
0% {
transform: translate3d(0, 0, 0);

View File

@ -248,6 +248,10 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
&:after {
content: none;
display: none;
}
}
.CodeMirror-wrap pre {
word-wrap: break-word;

View File

@ -1,4 +1,65 @@
.overlay {
// System wide notifications
[notification] {
position: fixed;
top: 0;
right: 0;
margin: $-xl*2 $-xl;
padding: $-l $-xl;
background-color: #EEE;
border-radius: 3px;
box-shadow: $bs-med;
z-index: 999999;
display: block;
cursor: pointer;
max-width: 480px;
transition: transform ease-in-out 360ms;
transform: translate3d(580px, 0, 0);
i, span {
display: table-cell;
}
i {
font-size: 2em;
padding-right: $-l;
}
span {
vertical-align: middle;
}
&.pos {
background-color: $positive;
color: #EEE;
}
&.neg {
background-color: $negative;
color: #EEE;
}
&.warning {
background-color: $secondary;
color: #EEE;
}
&.showing {
transform: translate3d(0, 0, 0);
}
}
[chapter-toggle] {
cursor: pointer;
margin: 0;
transition: all ease-in-out 180ms;
user-select: none;
i.zmdi-caret-right {
transition: all ease-in-out 180ms;
transform: rotate(0deg);
transform-origin: 25% 50%;
}
&.open {
//margin-bottom: 0;
}
&.open i.zmdi-caret-right {
transform: rotate(90deg);
}
}
[overlay] {
background-color: rgba(0, 0, 0, 0.333);
position: fixed;
z-index: 95536;
@ -467,3 +528,16 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-picker .none {
display: none;
}
#code-editor .CodeMirror {
height: 400px;
}
#code-editor .lang-options {
max-width: 400px;
margin-bottom: $-s;
a {
margin-right: $-xs;
text-decoration: underline;
}
}

View File

@ -32,7 +32,7 @@
#markdown-editor {
position: relative;
z-index: 5;
textarea {
#markdown-editor-input {
font-family: 'Roboto Mono', monospace;
font-style: normal;
font-weight: 400;
@ -265,7 +265,7 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
}
}
input.outline {
.outline > input {
border: 0;
border-bottom: 2px solid #DDD;
border-radius: 0;

View File

@ -142,7 +142,6 @@ form.search-box {
color: #aaa;
padding: 0 $-xs;
}
.faded {
a, button, span, span > div {
color: #666;
@ -155,7 +154,6 @@ form.search-box {
text-decoration: none;
}
}
}
.faded span.faded-text {
@ -175,6 +173,15 @@ form.search-box {
&:last-child {
padding-right: 0;
}
&:first-child {
padding-left: 0;
}
}
.action-buttons .dropdown-container:last-child a {
padding-right: 0;
padding-left: $-s;
}
.action-buttons {
text-align: right;
@ -190,6 +197,25 @@ form.search-box {
}
}
@include smaller-than($m) {
.breadcrumbs .text-button, .action-buttons .text-button {
padding: $-s $-xs;
}
.action-buttons .dropdown-container:last-child a {
padding-left: $-xs;
}
.breadcrumbs .text-button {
font-size: 0;
}
.breadcrumbs a i {
font-size: $fs-m;
padding-right: 0;
}
.breadcrumbs span.sep {
padding: 0 $-xxs;
}
}
.nav-tabs {
text-align: center;
a, .tab-item {

View File

@ -9,7 +9,6 @@
.inset-list {
display: none;
overflow: hidden;
margin-bottom: $-l;
}
h5 {
display: block;
@ -22,6 +21,9 @@
border-left-color: $color-page-draft;
}
}
.entity-list-item {
margin-bottom: $-m;
}
hr {
margin-top: 0;
}
@ -51,23 +53,6 @@
margin-right: $-s;
}
}
.chapter-toggle {
cursor: pointer;
margin: 0 0 $-l 0;
transition: all ease-in-out 180ms;
user-select: none;
i.zmdi-caret-right {
transition: all ease-in-out 180ms;
transform: rotate(0deg);
transform-origin: 25% 50%;
}
&.open {
margin-bottom: 0;
}
&.open i.zmdi-caret-right {
transform: rotate(90deg);
}
}
.sidebar-page-nav {
$nav-indent: $-s;
@ -171,7 +156,7 @@
background-color: rgba($color-chapter, 0.12);
}
}
.chapter-toggle {
[chapter-toggle] {
padding-left: $-s;
}
.list-item-chapter {
@ -336,8 +321,10 @@ ul.pagination {
h4, a {
line-height: 1.2;
}
p {
.entity-item-snippet {
display: none;
}
p {
font-size: $fs-m * 0.8;
padding-top: $-xs;
margin: 0;

View File

@ -226,7 +226,7 @@
width: 100%;
min-width: 50px;
}
.tags td {
.tags td, .tag-table > div > div > div {
padding-right: $-s;
padding-top: $-s;
position: relative;

View File

@ -68,3 +68,16 @@ table.file-table {
display: table;
}
}
.fake-table {
display: table;
> div {
display: table-row-group;
}
> div > div {
display: table-row;
}
> div > div > div {
display: table-cell;
}
}

View File

@ -135,8 +135,31 @@ pre {
font-size: 12px;
background-color: #f5f5f5;
border: 1px solid #DDD;
padding-left: 31px;
position: relative;
padding-top: 3px;
padding-bottom: 3px;
&:after {
content: '';
display: block;
position: absolute;
top: 0;
width: 29px;
left: 0;
background-color: #f5f5f5;
height: 100%;
border-right: 1px solid #DDD;
}
}
@media print {
pre {
padding-left: 12px;
}
pre:after {
display: none;
}
}
blockquote {
display: block;
@ -182,6 +205,7 @@ pre code {
border: 0;
font-size: 1em;
display: block;
line-height: 1.6;
}
/*
* Text colors

View File

@ -66,44 +66,6 @@ body.dragging, body.dragging * {
}
}
// System wide notifications
.notification {
position: fixed;
top: 0;
right: 0;
margin: $-xl*2 $-xl;
padding: $-l $-xl;
background-color: #EEE;
border-radius: 3px;
box-shadow: $bs-med;
z-index: 999999;
display: block;
cursor: pointer;
max-width: 480px;
i, span {
display: table-cell;
}
i {
font-size: 2em;
padding-right: $-l;
}
span {
vertical-align: middle;
}
&.pos {
background-color: $positive;
color: #EEE;
}
&.neg {
background-color: $negative;
color: #EEE;
}
&.warning {
background-color: $secondary;
color: #EEE;
}
}
// Loading icon
$loadingSize: 10px;
.loading-container {
@ -151,7 +113,7 @@ $loadingSize: 10px;
// Back to top link
$btt-size: 40px;
#back-to-top {
[back-to-top] {
background-color: $primary;
position: fixed;
bottom: $-m;

View File

@ -20,5 +20,13 @@ return [
'image_preview' => 'Image Preview',
'image_upload_success' => 'Image uploaded successfully',
'image_update_success' => 'Image details successfully updated',
'image_delete_success' => 'Image successfully deleted'
'image_delete_success' => 'Image successfully deleted',
/**
* Code editor
*/
'code_editor' => 'Edit Code',
'code_language' => 'Code Language',
'code_content' => 'Code Content',
'code_save' => 'Save Code',
];

View File

@ -121,6 +121,8 @@ return [
'nl' => 'Nederlands',
'pt_BR' => 'Português do Brasil',
'sk' => 'Slovensky',
'ja' => '日本語',
'pl' => 'Polski',
]
///////////////////////////////////
];

View File

@ -10,7 +10,7 @@ return [
| these language lines according to your application's requirements.
|
*/
'failed' => 'Ces informations ne correspondent a aucun compte.',
'failed' => 'Ces informations ne correspondent à aucun compte.',
'throttle' => "Trop d'essais, veuillez réessayer dans :seconds secondes.",
/**
@ -26,7 +26,7 @@ return [
'password' => 'Mot de passe',
'password_confirm' => 'Confirmez le mot de passe',
'password_hint' => 'Doit faire plus de 5 caractères',
'forgot_password' => 'Mot de passe oublié?',
'forgot_password' => 'Mot de passe oublié ?',
'remember_me' => 'Se souvenir de moi',
'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
'create_account' => 'Créer un compte',
@ -35,9 +35,9 @@ return [
'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
'register_thanks' => 'Merci pour votre enregistrement',
'register_confirm' => 'Vérifiez vos e-mails et cliquer sur le lien de confirmation pour rejoindre :appName.',
'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',
'registrations_disabled' => "L'inscription est désactivée pour le moment",
'registration_email_domain_invalid' => 'Cette adresse e-mail ne peux pas adcéder à l\'application',
'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application',
'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
@ -51,7 +51,7 @@ return [
'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
'email_reset_text' => 'Vous recevez cet e-mail parceque nous avons reçu une demande de réinitialisation pour votre compte',
'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte',
'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
@ -59,11 +59,11 @@ return [
* Email Confirmation
*/
'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName!',
'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous:',
'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
'email_confirm_action' => 'Confirmez votre adresse e-mail',
'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
'email_confirm_success' => 'Votre adresse e-mail a été confirmée!',
'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
'email_not_confirmed' => 'Adresse e-mail non confirmée',

View File

@ -9,7 +9,7 @@ return [
'back' => 'Retour',
'save' => 'Enregistrer',
'continue' => 'Continuer',
'select' => 'Selectionner',
'select' => 'Sélectionner',
/**
* Form Labels
@ -53,6 +53,6 @@ return [
/**
* Email Content
*/
'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur:',
'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
'email_rights' => 'Tous droits réservés',
];

View File

@ -12,7 +12,7 @@ return [
'recently_update' => 'Mis à jour récemment',
'recently_viewed' => 'Vus récemment',
'recent_activity' => 'Activité récente',
'create_now' => 'En créer un récemment',
'create_now' => 'En créer un maintenant',
'revisions' => 'Révisions',
'meta_created' => 'Créé :timeLength',
'meta_created_name' => 'Créé :timeLength par :user',
@ -59,8 +59,8 @@ return [
'books_create' => 'Créer un nouveau livre',
'books_delete' => 'Supprimer un livre',
'books_delete_named' => 'Supprimer le livre :bookName',
'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', Tous les chapitres et pages seront supprimés.',
'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre?',
'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.',
'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
'books_edit' => 'Modifier le livre',
'books_edit_named' => 'Modifier le livre :bookName',
'books_form_book_name' => 'Nom du livre',
@ -90,18 +90,18 @@ return [
'chapters_create' => 'Créer un nouveau chapitre',
'chapters_delete' => 'Supprimer le chapitre',
'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', Toutes les pages seront déplacée dans le livre parent.',
'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre?',
'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
'chapters_edit' => 'Modifier le chapitre',
'chapters_edit_named' => 'Modifier le chapitre :chapterName',
'chapters_save' => 'Enregistrer le chapitre',
'chapters_move' => 'Déplace le chapitre',
'chapters_move' => 'Déplacer le chapitre',
'chapters_move_named' => 'Déplacer le chapitre :chapterName',
'chapter_move_success' => 'Chapitre déplacé dans :bookName',
'chapters_permissions' => 'Permissions du chapitre',
'chapters_empty' => 'Il n\'y a pas de pages dans ce chapitre actuellement.',
'chapters_empty' => 'Il n\'y a pas de page dans ce chapitre actuellement.',
'chapters_permissions_active' => 'Permissions du chapitre activées',
'chapters_permissions_success' => 'Permissions du chapitres mises à jour',
'chapters_permissions_success' => 'Permissions du chapitre mises à jour',
/**
* Pages
@ -118,8 +118,8 @@ return [
'pages_delete_draft' => 'Supprimer le brouillon',
'pages_delete_success' => 'Page supprimée',
'pages_delete_draft_success' => 'Brouillon supprimé',
'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page?',
'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon?',
'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
'pages_editing_named' => 'Modification de la page :pageName',
'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
'pages_edit_save_draft' => 'Enregistrer le brouillon',
@ -131,7 +131,7 @@ return [
'pages_edit_discard_draft' => 'Ecarter le brouillon',
'pages_edit_set_changelog' => 'Remplir le journal des changements',
'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués',
'pages_edit_enter_changelog' => 'Entrez dans le journal des changements',
'pages_edit_enter_changelog' => 'Entrer dans le journal des changements',
'pages_save' => 'Enregistrez la page',
'pages_title' => 'Titre de la page',
'pages_name' => 'Nom de la page',
@ -139,7 +139,7 @@ return [
'pages_md_preview' => 'Prévisualisation',
'pages_md_insert_image' => 'Insérer une image',
'pages_md_insert_link' => 'Insérer un lien',
'pages_not_in_chapter' => 'La page n\'est pas dans un chanpitre',
'pages_not_in_chapter' => 'La page n\'est pas dans un chapitre',
'pages_move' => 'Déplacer la page',
'pages_move_success' => 'Page déplacée à ":parentName"',
'pages_permissions' => 'Permissions de la page',
@ -160,15 +160,15 @@ return [
'pages_initial_revision' => 'Publication initiale',
'pages_initial_name' => 'Nouvelle page',
'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.',
'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visit. Vous devriez écarter ce brouillon.',
'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez écarter ce brouillon.',
'pages_draft_edit_active' => [
'start_a' => ':count utilisateurs ont commencé a éditer cette page',
'start_a' => ':count utilisateurs ont commencé à éditer cette page',
'start_b' => ':userName a commencé à éditer cette page',
'time_a' => 'depuis la dernière sauvegarde',
'time_b' => 'dans les :minCount dernières minutes',
'message' => ':start :time. Attention a ne pas écraser les mises à jour de quelqu\'un d\'autre!',
'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
],
'pages_draft_discarded' => 'Brouuillon écarté, la page est dans sa version actuelle.',
'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.',
/**
* Editor sidebar
@ -210,9 +210,9 @@ return [
*/
'profile_user_for_x' => 'Utilisateur depuis :time',
'profile_created_content' => 'Contenu créé',
'profile_not_created_pages' => ':userName n\'a pas créé de pages',
'profile_not_created_chapters' => ':userName n\'a pas créé de chapitres',
'profile_not_created_books' => ':userName n\'a pas créé de livres',
'profile_not_created_pages' => ':userName n\'a pas créé de page',
'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre',
'profile_not_created_books' => ':userName n\'a pas créé de livre',
/**
* Comments

View File

@ -18,21 +18,21 @@ return [
'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti',
'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe',
'ldap_extension_not_installed' => 'L\'extention LDAP PHP n\'est pas installée',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'No action defined',
'social_account_in_use' => 'Cet compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email Est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
'social_no_action_defined' => 'Pas d\'action définie',
'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.',
'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ',
'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
'social_driver_not_found' => 'Pilote de compte social absent',
'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
'cannot_get_image_from_url' => 'Impossible de récupérer l\'image depuis :url',
'cannot_create_thumbs' => 'Le serveur ne peux pas créer de miniatures, vérifier que l\extensions GD PHP est installée.',
'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\'extension PHP GD est installée.',
'server_upload_limit' => 'La taille du fichier est trop grande.',
'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
@ -57,7 +57,7 @@ return [
// Roles
'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et on ne peut pas le supprimer',
'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',
'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
// Error pages

View File

@ -16,7 +16,7 @@ return [
'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
'token' => 'Le jeton de réinitialisation est invalide.',
'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe!',
'reset' => 'Votre mot de passe a été réinitialisé!',
'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
'reset' => 'Votre mot de passe a été réinitialisé !',
];

View File

@ -19,27 +19,27 @@ return [
'app_settings' => 'Préférences de l\'application',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
'app_name_header' => 'Afficher le nom dans l\'en-tête?',
'app_public_viewing' => 'Accepter le visionnage public des pages?',
'app_secure_images' => 'Activer l\'ajout d\'image sécurisé?',
'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
'app_public_viewing' => 'Accepter le visionnage public des pages ?',
'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_editor' => 'Editeur des pages',
'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.',
'app_custom_html' => 'HTML personnalisé dans l\'en-tête',
'app_custom_html_desc' => 'Le contenu inséré ici sera jouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
'app_logo' => 'Logo de l\'Application',
'app_logo_desc' => 'Cette image doit faire 43px de hauteur. <br>Les images plus larges seront réduites.',
'app_primary_color' => 'Couleur principale de l\'application',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
'app_primary_color_desc' => 'Cela devrait être une valeur hexadécimale. <br>Laisser vide pour rétablir la couleur par défaut.',
/**
* Registration settings
*/
'reg_settings' => 'Préférence pour l\'inscription',
'reg_allow' => 'Accepter l\'inscription?',
'reg_allow' => 'Accepter l\'inscription ?',
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
'reg_confirm_email' => 'Obliger la confirmation par e-mail?',
'reg_confirm_email' => 'Obliger la confirmation par e-mail ?',
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateur recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
@ -57,17 +57,17 @@ return [
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle?',
'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle ?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
'role_name' => 'Nom du Rôle',
'role_name' => 'Nom du rôle',
'role_desc' => 'Courte description du rôle',
'role_system' => 'Permissions système',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres chapitres et pages',
'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
@ -94,7 +94,7 @@ return [
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur?',
'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
'users_delete_success' => 'Utilisateurs supprimés avec succès',
'users_edit' => 'Modifier l\'utilisateur',
'users_edit_profile' => 'Modifier le profil',
@ -106,7 +106,7 @@ return [
'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.',
'users_social_connect' => 'Connecter le compte',
'users_social_disconnect' => 'Déconnecter le compte',
'users_social_connected' => 'Votre compte :socialAccount a élté ajouté avec succès.',
'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.',
'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès',
];

View File

@ -0,0 +1,40 @@
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'がページを作成:',
'page_create_notification' => 'ページを作成しました',
'page_update' => 'がページを更新:',
'page_update_notification' => 'ページを更新しました',
'page_delete' => 'がページを削除:',
'page_delete_notification' => 'ページを削除しました',
'page_restore' => 'がページを復元:',
'page_restore_notification' => 'ページを復元しました',
'page_move' => 'がページを移動:',
// Chapters
'chapter_create' => 'がチャプターを作成:',
'chapter_create_notification' => 'チャプターを作成しました',
'chapter_update' => 'がチャプターを更新:',
'chapter_update_notification' => 'チャプターを更新しました',
'chapter_delete' => 'がチャプターを削除:',
'chapter_delete_notification' => 'チャプターを削除しました',
'chapter_move' => 'がチャプターを移動:',
// Books
'book_create' => 'がブックを作成:',
'book_create_notification' => 'ブックを作成しました',
'book_update' => 'がブックを更新:',
'book_update_notification' => 'ブックを更新しました',
'book_delete' => 'がブックを削除:',
'book_delete_notification' => 'ブックを削除しました',
'book_sort' => 'がブックの並び順を変更:',
'book_sort_notification' => '並び順を変更しました',
];

View File

@ -0,0 +1,76 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'この資格情報は登録されていません。',
'throttle' => 'ログイン試行回数が制限を超えました。:seconds秒後に再試行してください。',
/**
* Login & Register
*/
'sign_up' => '新規登録',
'log_in' => 'ログイン',
'log_in_with' => ':socialDriverでログイン',
'sign_up_with' => ':socialDriverで登録',
'logout' => 'ログアウト',
'name' => '名前',
'username' => 'ユーザ名',
'email' => 'メールアドレス',
'password' => 'パスワード',
'password_confirm' => 'パスワード (確認)',
'password_hint' => '5文字以上である必要があります',
'forgot_password' => 'パスワードをお忘れですか?',
'remember_me' => 'ログイン情報を保存する',
'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
'create_account' => 'アカウント作成',
'social_login' => 'SNSログイン',
'social_registration' => 'SNS登録',
'social_registration_text' => '他のサービスで登録 / ログインする',
'register_thanks' => '登録が完了しました!',
'register_confirm' => 'メール内の確認ボタンを押して、:appNameへアクセスしてください。',
'registrations_disabled' => '登録は現在停止中です。',
'registration_email_domain_invalid' => 'このEmailドメインでの登録は許可されていません。',
'register_success' => '登録が完了し、ログインできるようになりました!',
/**
* Password Reset
*/
'reset_password' => 'パスワードリセット',
'reset_password_send_instructions' => '以下にEメールアドレスを入力すると、パスワードリセットリンクが記載されたメールが送信されます。',
'reset_password_send_button' => 'リセットリンクを送信',
'reset_password_sent_success' => ':emailへリセットリンクを送信しました。',
'reset_password_success' => 'パスワードがリセットされました。',
'email_reset_subject' => ':appNameのパスワードをリセット',
'email_reset_text' => 'このメールは、パスワードリセットがリクエストされたため送信されています。',
'email_reset_not_requested' => 'もしパスワードリセットを希望しない場合、操作は不要です。',
/**
* Email Confirmation
*/
'email_confirm_subject' => ':appNameのメールアドレス確認',
'email_confirm_greeting' => ':appNameへ登録してくださりありがとうございます',
'email_confirm_text' => '以下のボタンを押し、メールアドレスを確認してください:',
'email_confirm_action' => 'メールアドレスを確認',
'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。',
'email_confirm_success' => 'Eメールアドレスが確認されました。',
'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。',
'email_not_confirmed' => 'Eメールアドレスが確認できていません',
'email_not_confirmed_text' => 'Eメールアドレスの確認が完了していません。',
'email_not_confirmed_click_link' => '登録時に受信したメールを確認し、確認リンクをクリックしてください。',
'email_not_confirmed_resend' => 'Eメールが見つからない場合、以下のフォームから再送信してください。',
'email_not_confirmed_resend_button' => '確認メールを再送信',
];

View File

@ -0,0 +1,59 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'キャンセル',
'confirm' => '確認',
'back' => '戻る',
'save' => '保存',
'continue' => '続ける',
'select' => '選択',
/**
* Form Labels
*/
'name' => '名称',
'description' => '概要',
'role' => '権限',
/**
* Actions
*/
'actions' => '実行',
'view' => '表示',
'create' => '作成',
'update' => '更新',
'edit' => '編集',
'sort' => '並び順',
'move' => '移動',
'delete' => '削除',
'search' => '検索',
'search_clear' => '検索をクリア',
'reset' => 'リセット',
'remove' => '削除',
'add' => '追加',
/**
* Misc
*/
'deleted_user' => '削除済みユーザ',
'no_activity' => '表示するアクティビティがありません',
'no_items' => 'アイテムはありません',
'back_to_top' => '上に戻る',
'toggle_details' => '概要の表示切替',
/**
* Header
*/
'view_profile' => 'プロフィール表示',
'edit_profile' => 'プロフィール編集',
/**
* Email Content
*/
'email_action_help' => '":actionText" をクリックできない場合、以下のURLをコピーしブラウザで開いてください:',
'email_rights' => 'All rights reserved',
];

View File

@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => '画像を選択',
'image_all' => 'すべて',
'image_all_title' => '全ての画像を表示',
'image_book_title' => 'このブックにアップロードされた画像を表示',
'image_page_title' => 'このページにアップロードされた画像を表示',
'image_search_hint' => '画像名で検索',
'image_uploaded' => 'アップロード日時: :uploadedDate',
'image_load_more' => 'さらに読み込む',
'image_image_name' => '画像名',
'image_delete_confirm' => 'この画像は以下のページで利用されています。削除してもよろしければ、再度ボタンを押して下さい。',
'image_select_image' => '選択',
'image_dropzone' => '画像をドロップするか、クリックしてアップロード',
'images_deleted' => '画像を削除しました',
'image_preview' => '画像プレビュー',
'image_upload_success' => '画像がアップロードされました',
'image_update_success' => '画像が更新されました',
'image_delete_success' => '画像が削除されました'
];

View File

@ -0,0 +1,236 @@
<?php
return [
/**
* Shared
*/
'recently_created' => '最近作成',
'recently_created_pages' => '最近作成されたページ',
'recently_updated_pages' => '最近更新されたページ',
'recently_created_chapters' => '最近作成されたチャプター',
'recently_created_books' => '最近作成されたブック',
'recently_update' => '最近更新',
'recently_viewed' => '閲覧履歴',
'recent_activity' => 'アクティビティ',
'create_now' => '作成する',
'revisions' => '編集履歴',
'meta_revision' => 'リビジョン #:revisionCount',
'meta_created' => '作成: :timeLength',
'meta_created_name' => '作成: :timeLength (:user)',
'meta_updated' => '更新: :timeLength',
'meta_updated_name' => '更新: :timeLength (:user)',
'x_pages' => ':countページ',
'entity_select' => 'エンティティ選択',
'images' => '画像',
'my_recent_drafts' => '最近の下書き',
'my_recently_viewed' => '閲覧履歴',
'no_pages_viewed' => 'なにもページを閲覧していません',
'no_pages_recently_created' => '最近作成されたページはありません',
'no_pages_recently_updated' => '最近更新されたページはありません。',
'export' => 'エクスポート',
'export_html' => 'Webページ',
'export_pdf' => 'PDF',
'export_text' => 'テキストファイル',
/**
* Permissions and restrictions
*/
'permissions' => '権限',
'permissions_intro' => 'この設定は各ユーザの役割よりも優先して適用されます。',
'permissions_enable' => 'カスタム権限設定を有効にする',
'permissions_save' => '権限を保存',
/**
* Search
*/
'search_results' => '検索結果',
'search_total_results_found' => ':count件見つかりました',
'search_clear' => '検索をクリア',
'search_no_pages' => 'ページが見つかりませんでした。',
'search_for_term' => ':term の検索結果',
'search_more' => 'さらに表示',
'search_filters' => '検索フィルタ',
'search_content_type' => '種類',
'search_exact_matches' => '完全一致',
'search_tags' => 'タグ検索',
'search_viewed_by_me' => '自分が閲覧したことがある',
'search_not_viewed_by_me' => '自分が閲覧したことがない',
'search_permissions_set' => '権限が設定されている',
'search_created_by_me' => '自分が作成した',
'search_updated_by_me' => '自分が更新した',
'search_updated_before' => '以前に更新',
'search_updated_after' => '以降に更新',
'search_created_before' => '以前に作成',
'search_created_after' => '以降に更新',
'search_set_date' => '日付を設定',
'search_update' => 'フィルタを更新',
/**
* Books
*/
'book' => 'Book',
'books' => 'ブック',
'books_empty' => 'まだブックは作成されていません',
'books_popular' => '人気のブック',
'books_recent' => '最近のブック',
'books_popular_empty' => 'ここに人気のブックが表示されます。',
'books_create' => '新しいブックを作成',
'books_delete' => 'ブックを削除',
'books_delete_named' => 'ブック「:bookName」を削除',
'books_delete_explain' => '「:bookName」を削除すると、ブック内のページとチャプターも削除されます。',
'books_delete_confirmation' => '本当にこのブックを削除してよろしいですか?',
'books_edit' => 'ブックを編集',
'books_edit_named' => 'ブック「:bookName」を編集',
'books_form_book_name' => 'ブック名',
'books_save' => 'ブックを保存',
'books_permissions' => 'ブックの権限',
'books_permissions_updated' => 'ブックの権限を更新しました',
'books_empty_contents' => 'まだページまたはチャプターが作成されていません。',
'books_empty_create_page' => '新しいページを作成',
'books_empty_or' => 'または',
'books_empty_sort_current_book' => 'ブックの並び順を変更',
'books_empty_add_chapter' => 'チャプターを追加',
'books_permissions_active' => 'ブックの権限は有効です',
'books_search_this' => 'このブックから検索',
'books_navigation' => '目次',
'books_sort' => '並び順を変更',
'books_sort_named' => 'ブック「:bookName」を並び替え',
'books_sort_show_other' => '他のブックを表示',
'books_sort_save' => '並び順を保存',
/**
* Chapters
*/
'chapter' => 'チャプター',
'chapters' => 'チャプター',
'chapters_popular' => '人気のチャプター',
'chapters_new' => 'チャプターを作成',
'chapters_create' => 'チャプターを作成',
'chapters_delete' => 'チャプターを削除',
'chapters_delete_named' => 'チャプター「:chapterName」を削除',
'chapters_delete_explain' => 'チャプター「:chapterName」を削除すると、チャプター内のすべてのページはブック内に直接追加されます。',
'chapters_delete_confirm' => 'チャプターを削除してよろしいですか?',
'chapters_edit' => 'チャプターを編集',
'chapters_edit_named' => 'チャプター「:chapterName」を編集',
'chapters_save' => 'チャプターを保存',
'chapters_move' => 'チャプターを移動',
'chapters_move_named' => 'チャプター「:chapterName」を移動',
'chapter_move_success' => 'チャプターを「:bookName」に移動しました',
'chapters_permissions' => 'チャプター権限',
'chapters_empty' => 'まだチャプター内にページはありません。',
'chapters_permissions_active' => 'チャプターの権限は有効です',
'chapters_permissions_success' => 'チャプターの権限を更新しました',
'chapters_search_this' => 'このチャプターを検索',
/**
* Pages
*/
'page' => 'ページ',
'pages' => 'ページ',
'pages_popular' => '人気のページ',
'pages_new' => 'ページを作成',
'pages_attachments' => '添付',
'pages_navigation' => 'ページナビゲーション',
'pages_delete' => 'ページを削除',
'pages_delete_named' => 'ページ :pageName を削除',
'pages_delete_draft_named' => 'ページ :pageName の下書きを削除',
'pages_delete_draft' => 'ページの下書きを削除',
'pages_delete_success' => 'ページを削除しました',
'pages_delete_draft_success' => 'ページの下書きを削除しました',
'pages_delete_confirm' => 'このページを削除してもよろしいですか?',
'pages_delete_draft_confirm' => 'このページの下書きを削除してもよろしいですか?',
'pages_editing_named' => 'ページ :pageName を編集',
'pages_edit_toggle_header' => 'ヘッダーの表示切替',
'pages_edit_save_draft' => '下書きを保存',
'pages_edit_draft' => 'ページの下書きを編集',
'pages_editing_draft' => '下書きを編集中',
'pages_editing_page' => 'ページを編集中',
'pages_edit_draft_save_at' => '下書きを保存済み: ',
'pages_edit_delete_draft' => '下書きを削除',
'pages_edit_discard_draft' => '下書きを破棄',
'pages_edit_set_changelog' => '編集内容についての説明',
'pages_edit_enter_changelog_desc' => 'どのような変更を行ったのかを記録してください',
'pages_edit_enter_changelog' => '編集内容を入力',
'pages_save' => 'ページを保存',
'pages_title' => 'ページタイトル',
'pages_name' => 'ページ名',
'pages_md_editor' => 'エディター',
'pages_md_preview' => 'プレビュー',
'pages_md_insert_image' => '画像を挿入',
'pages_md_insert_link' => 'エンティティへのリンクを挿入',
'pages_not_in_chapter' => 'チャプターが設定されていません',
'pages_move' => 'ページを移動',
'pages_move_success' => 'ページを ":parentName" へ移動しました',
'pages_permissions' => 'ページの権限設定',
'pages_permissions_success' => 'ページの権限を更新しました',
'pages_revisions' => '編集履歴',
'pages_revisions_named' => ':pageName のリビジョン',
'pages_revision_named' => ':pageName のリビジョン',
'pages_revisions_created_by' => '作成者',
'pages_revisions_date' => '日付',
'pages_revisions_number' => 'リビジョン',
'pages_revisions_changelog' => '説明',
'pages_revisions_changes' => '変更点',
'pages_revisions_current' => '現在のバージョン',
'pages_revisions_preview' => 'プレビュー',
'pages_revisions_restore' => '復元',
'pages_revisions_none' => 'このページにはリビジョンがありません',
'pages_copy_link' => 'リンクをコピー',
'pages_permissions_active' => 'ページの権限は有効です',
'pages_initial_revision' => '初回の公開',
'pages_initial_name' => '新規ページ',
'pages_editing_draft_notification' => ':timeDiffに保存された下書きを編集しています。',
'pages_draft_edited_notification' => 'このページは更新されています。下書きを破棄することを推奨します。',
'pages_draft_edit_active' => [
'start_a' => ':count人のユーザがページの編集を開始しました',
'start_b' => ':userNameがページの編集を開始しました',
'time_a' => '数秒前に保存されました',
'time_b' => ':minCount分前に保存されました',
'message' => ':start :time. 他のユーザによる更新を上書きしないよう注意してください。',
],
'pages_draft_discarded' => '下書きが破棄されました。エディタは現在の内容へ復元されています。',
/**
* Editor sidebar
*/
'page_tags' => 'タグ',
'tag' => 'タグ',
'tags' => '',
'tag_value' => '内容 (オプション)',
'tags_explain' => "タグを設定すると、コンテンツの管理が容易になります。\nより高度な管理をしたい場合、タグに内容を設定できます。",
'tags_add' => 'タグを追加',
'attachments' => '添付ファイル',
'attachments_explain' => 'ファイルをアップロードまたはリンクを添付することができます。これらはサイドバーで確認できます。',
'attachments_explain_instant_save' => 'この変更は即座に保存されます。',
'attachments_items' => 'アイテム',
'attachments_upload' => 'アップロード',
'attachments_link' => 'リンクを添付',
'attachments_set_link' => 'リンクを設定',
'attachments_delete_confirm' => 'もう一度クリックし、削除を確認してください。',
'attachments_dropzone' => 'ファイルをドロップするか、クリックして選択',
'attachments_no_files' => 'ファイルはアップロードされていません',
'attachments_explain_link' => 'ファイルをアップロードしたくない場合、他のページやクラウド上のファイルへのリンクを添付できます。',
'attachments_link_name' => 'リンク名',
'attachment_link' => '添付リンク',
'attachments_link_url' => 'ファイルURL',
'attachments_link_url_hint' => 'WebサイトまたはファイルへのURL',
'attach' => '添付',
'attachments_edit_file' => 'ファイルを編集',
'attachments_edit_file_name' => 'ファイル名',
'attachments_edit_drop_upload' => 'ファイルをドロップするか、クリックしてアップロード',
'attachments_order_updated' => '添付ファイルの並び順が変更されました',
'attachments_updated_success' => '添付ファイルが更新されました',
'attachments_deleted' => '添付は削除されました',
'attachments_file_uploaded' => 'ファイルがアップロードされました',
'attachments_file_updated' => 'ファイルが更新されました',
'attachments_link_attached' => 'リンクがページへ添付されました',
/**
* Profile View
*/
'profile_user_for_x' => ':time前に作成',
'profile_created_content' => '作成したコンテンツ',
'profile_not_created_pages' => ':userNameはページを作成していません',
'profile_not_created_chapters' => ':userNameはチャプターを作成していません',
'profile_not_created_books' => ':userNameはブックを作成していません',
];

View File

@ -0,0 +1,70 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'リクエストされたページへの権限がありません。',
'permissionJson' => '要求されたアクションを実行する権限がありません。',
// Auth
'error_user_exists_different_creds' => ':emailを持つユーザは既に存在しますが、資格情報が異なります。',
'email_already_confirmed' => 'Eメールは既に確認済みです。ログインしてください。',
'email_confirmation_invalid' => 'この確認トークンは無効か、または既に使用済みです。登録を再試行してください。',
'email_confirmation_expired' => '確認トークンは有効期限切れです。確認メールを再送しました。',
'ldap_fail_anonymous' => '匿名バインドを用いたLDAPアクセスに失敗しました',
'ldap_fail_authed' => '識別名, パスワードを用いたLDAPアクセスに失敗しました',
'ldap_extension_not_installed' => 'LDAP PHP extensionがインストールされていません',
'ldap_cannot_connect' => 'LDAPサーバに接続できませんでした',
'social_no_action_defined' => 'アクションが定義されていません',
'social_account_in_use' => ':socialAccountアカウントは既に使用されています。:socialAccountのオプションからログインを試行してください。',
'social_account_email_in_use' => ':emailは既に使用されています。ログイン後、プロフィール設定から:socialAccountアカウントを接続できます。',
'social_account_existing' => 'アカウント:socialAccountは既にあなたのプロフィールに接続されています。',
'social_account_already_used_existing' => 'この:socialAccountアカウントは既に他のユーザが使用しています。',
'social_account_not_used' => 'この:socialAccountアカウントはどのユーザにも接続されていません。プロフィール設定から接続できます。',
'social_account_register_instructions' => 'まだアカウントをお持ちでない場合、:socialAccountオプションから登録できます。',
'social_driver_not_found' => 'Social driverが見つかりません。',
'social_driver_not_configured' => 'あなたの:socialAccount設定は正しく構成されていません。',
// System
'path_not_writable' => 'ファイルパス :filePath へアップロードできませんでした。サーバ上での書き込みを許可してください。',
'cannot_get_image_from_url' => ':url から画像を取得できませんでした。',
'cannot_create_thumbs' => 'このサーバはサムネイルを作成できません。GD PHP extensionがインストールされていることを確認してください。',
'server_upload_limit' => 'このサイズの画像をアップロードすることは許可されていません。ファイルサイズを小さくし、再試行してください。',
'image_upload_error' => '画像アップロード時にエラーが発生しました。',
// Attachments
'attachment_page_mismatch' => '添付を更新するページが一致しません',
// Pages
'page_draft_autosave_fail' => '下書きの保存に失敗しました。インターネットへ接続してください。',
// Entities
'entity_not_found' => 'エンティティが見つかりません',
'book_not_found' => 'ブックが見つかりません',
'page_not_found' => 'ページが見つかりません',
'chapter_not_found' => 'チャプターが見つかりません',
'selected_book_not_found' => '選択されたブックが見つかりません',
'selected_book_chapter_not_found' => '選択されたブック、またはチャプターが見つかりません',
'guests_cannot_save_drafts' => 'ゲストは下書きを保存できません',
// Users
'users_cannot_delete_only_admin' => '唯一の管理者を削除することはできません',
'users_cannot_delete_guest' => 'ゲストユーザを削除することはできません',
// Roles
'role_cannot_be_edited' => 'この役割は編集できません',
'role_system_cannot_be_deleted' => 'この役割はシステムで管理されているため、削除できません',
'role_registration_default_cannot_delete' => 'この役割を登録時のデフォルトに設定することはできません',
// Error pages
'404_page_not_found' => 'ページが見つかりません',
'sorry_page_not_found' => 'ページを見つけることができませんでした。',
'return_home' => 'ホームに戻る',
'error_occurred' => 'エラーが発生しました',
'app_down' => ':appNameは現在停止しています',
'back_soon' => '回復までしばらくお待ちください。',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; 前',
'next' => '次 &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'パスワードは6文字以上である必要があります。',
'user' => "このEメールアドレスに一致するユーザが見つかりませんでした。",
'token' => 'このパスワードリセットトークンは無効です。',
'sent' => 'パスワードリセットリンクを送信しました。',
'reset' => 'パスワードはリセットされました。',
];

View File

@ -0,0 +1,112 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => '設定',
'settings_save' => '設定を保存',
'settings_save_success' => '設定を保存しました',
/**
* App settings
*/
'app_settings' => 'アプリケーション設定',
'app_name' => 'アプリケーション名',
'app_name_desc' => 'この名前はヘッダーやEメール内で表示されます。',
'app_name_header' => 'ヘッダーにアプリケーション名を表示する',
'app_public_viewing' => 'アプリケーションを公開する',
'app_secure_images' => '画像アップロード時のセキュリティを強化',
'app_secure_images_desc' => 'パフォーマンスの観点から、全ての画像が公開になっています。このオプションを有効にすると、画像URLの先頭にランダムで推測困難な文字列が追加され、アクセスを困難にします。',
'app_editor' => 'ページエディタ',
'app_editor_desc' => 'ここで選択されたエディタを全ユーザが使用します。',
'app_custom_html' => 'カスタムheadタグ',
'app_custom_html_desc' => 'スタイルシートやアナリティクスコード追加したい場合、ここを編集します。これは<head>の最下部に挿入されます。',
'app_logo' => 'ロゴ',
'app_logo_desc' => '高さ43pxで表示されます。これを上回る場合、自動で縮小されます。',
'app_primary_color' => 'プライマリカラー',
'app_primary_color_desc' => '16進数カラーコードで入力します。空にした場合、デフォルトの色にリセットされます。',
/**
* Registration settings
*/
'reg_settings' => '登録設定',
'reg_allow' => '新規登録を許可',
'reg_default_role' => '新規登録時のデフォルト役割',
'reg_confirm_email' => 'Eメール認証を必須にする',
'reg_confirm_email_desc' => 'ドメイン制限を有効にしている場合はEメール認証が必須となり、この項目は無視されます。',
'reg_confirm_restrict_domain' => 'ドメイン制限',
'reg_confirm_restrict_domain_desc' => '特定のドメインのみ登録できるようにする場合、以下にカンマ区切りで入力します。設定された場合、Eメール認証が必須になります。<br>登録後、ユーザは自由にEメールアドレスを変更できます。',
'reg_confirm_restrict_domain_placeholder' => '制限しない',
/**
* Role settings
*/
'roles' => '役割',
'role_user_roles' => '役割',
'role_create' => '役割を作成',
'role_create_success' => '役割を作成しました',
'role_delete' => '役割を削除',
'role_delete_confirm' => '役割「:roleName」を削除します。',
'role_delete_users_assigned' => 'この役割は:userCount人のユーザに付与されています。該当するユーザを他の役割へ移行できます。',
'role_delete_no_migration' => "ユーザを移行しない",
'role_delete_sure' => '本当に役割を削除してよろしいですか?',
'role_delete_success' => '役割を削除しました',
'role_edit' => '役割を編集',
'role_details' => '概要',
'role_name' => '役割名',
'role_desc' => '役割の説明',
'role_system' => 'システム権限',
'role_manage_users' => 'ユーザ管理',
'role_manage_roles' => '役割と権限の管理',
'role_manage_entity_permissions' => '全てのブック, チャプター, ページに対する権限の管理',
'role_manage_own_entity_permissions' => '自身のブック, チャプター, ページに対する権限の管理',
'role_manage_settings' => 'アプリケーション設定の管理',
'role_asset' => 'アセット権限',
'role_asset_desc' => '各アセットに対するデフォルトの権限を設定します。ここで設定した権限が優先されます。',
'role_all' => '全て',
'role_own' => '自身',
'role_controlled_by_asset' => 'このアセットに対し、右記の操作を許可:',
'role_save' => '役割を保存',
'role_update_success' => '役割を更新しました',
'role_users' => 'この役割を持つユーザ',
'role_users_none' => 'この役割が付与されたユーザは居ません',
/**
* Users
*/
'users' => 'ユーザ',
'user_profile' => 'ユーザプロフィール',
'users_add_new' => 'ユーザを追加',
'users_search' => 'ユーザ検索',
'users_role' => 'ユーザ役割',
'users_external_auth_id' => '外部認証ID',
'users_password_warning' => 'パスワードを変更したい場合のみ入力してください',
'users_system_public' => 'このユーザはアプリケーションにアクセスする全てのゲストを表します。ログインはできませんが、自動的に割り当てられます。',
'users_delete' => 'ユーザを削除',
'users_delete_named' => 'ユーザ「:userName」を削除',
'users_delete_warning' => 'ユーザ「:userName」を完全に削除します。',
'users_delete_confirm' => '本当にこのユーザを削除してよろしいですか?',
'users_delete_success' => 'ユーザを削除しました',
'users_edit' => 'ユーザ編集',
'users_edit_profile' => 'プロフィール編集',
'users_edit_success' => 'ユーザを更新しました',
'users_avatar' => 'アバター',
'users_avatar_desc' => '256pxの正方形である必要があります。',
'users_preferred_language' => '使用言語',
'users_social_accounts' => 'ソーシャルアカウント',
'users_social_accounts_info' => 'アカウントを接続すると、ログインが簡単になります。ここでアカウントの接続を解除すると、そのアカウントを経由したログインを禁止できます。接続解除後、各ソーシャルアカウントの設定にてこのアプリケーションへのアクセス許可を解除してください。',
'users_social_connect' => 'アカウントを接続',
'users_social_disconnect' => 'アカウントを接続解除',
'users_social_connected' => '「:socialAccount」がプロフィールに接続されました。',
'users_social_disconnected' => '「:socialAccount」がプロフィールから接続解除されました。'
];

View File

@ -0,0 +1,108 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attributeに同意する必要があります。',
'active_url' => ':attributeは正しいURLではありません。',
'after' => ':attributeは:date以降である必要があります。',
'alpha' => ':attributeは文字のみが含められます。',
'alpha_dash' => ':attributeは文字, 数値, ハイフンのみが含められます。',
'alpha_num' => ':attributeは文字と数値のみが含められます。',
'array' => ':attributeは配列である必要があります。',
'before' => ':attributeは:date以前である必要があります。',
'between' => [
'numeric' => ':attributeは:min〜:maxである必要があります。',
'file' => ':attributeは:min〜:maxキロバイトである必要があります。',
'string' => ':attributeは:min〜:max文字である必要があります。',
'array' => ':attributeは:min〜:max個である必要があります。',
],
'boolean' => ':attributeはtrueまたはfalseである必要があります。',
'confirmed' => ':attributeの確認が一致しません。',
'date' => ':attributeは正しい日時ではありません。',
'date_format' => ':attributeが:formatのフォーマットと一致しません。',
'different' => ':attributeと:otherは異なる必要があります。',
'digits' => ':attributeは:digitsデジットである必要があります',
'digits_between' => ':attributeは:min〜:maxである必要があります。',
'email' => ':attributeは正しいEメールアドレスである必要があります。',
'filled' => ':attributeは必須です。',
'exists' => '選択された:attributeは不正です。',
'image' => ':attributeは画像である必要があります。',
'in' => '選択された:attributeは不正です。',
'integer' => ':attributeは数値である必要があります。',
'ip' => ':attributeは正しいIPアドレスである必要があります。',
'max' => [
'numeric' => ':attributeは:maxを越えることができません。',
'file' => ':attributeは:maxキロバイトを越えることができません。',
'string' => ':attributeは:max文字をこえることができません。',
'array' => ':attributeは:max個を越えることができません。',
],
'mimes' => ':attributeのファイルタイプは以下のみが許可されています: :values.',
'min' => [
'numeric' => ':attributeは:min以上である必要があります。',
'file' => ':attributeは:minキロバイト以上である必要があります。',
'string' => ':attributeは:min文字以上である必要があります。',
'array' => ':attributeは:min個以上である必要があります。',
],
'not_in' => '選択された:attributeは不正です。',
'numeric' => ':attributeは数値である必要があります。',
'regex' => ':attributeのフォーマットは不正です。',
'required' => ':attributeは必須です。',
'required_if' => ':otherが:valueである場合、:attributeは必須です。',
'required_with' => ':valuesが設定されている場合、:attributeは必須です。',
'required_with_all' => ':valuesが設定されている場合、:attributeは必須です。',
'required_without' => ':valuesが設定されていない場合、:attributeは必須です。',
'required_without_all' => ':valuesが設定されていない場合、:attributeは必須です。',
'same' => ':attributeと:otherは一致している必要があります。',
'size' => [
'numeric' => ':attributeは:sizeである必要があります。',
'file' => ':attributeは:sizeキロバイトである必要があります。',
'string' => ':attributeは:size文字である必要があります。',
'array' => ':attributeは:size個である必要があります。',
],
'string' => ':attributeは文字列である必要があります。',
'timezone' => ':attributeは正しいタイムゾーンである必要があります。',
'unique' => ':attributeは既に使用されています。',
'url' => ':attributeのフォーマットは不正です。',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'パスワードの確認は必須です。',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -0,0 +1,40 @@
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'utworzono stronę',
'page_create_notification' => 'Strona utworzona pomyślnie',
'page_update' => 'zaktualizowano stronę',
'page_update_notification' => 'Strona zaktualizowana pomyślnie',
'page_delete' => 'usunięto stronę',
'page_delete_notification' => 'Strona usunięta pomyślnie',
'page_restore' => 'przywrócono stronę',
'page_restore_notification' => 'Stronga przywrócona pomyślnie',
'page_move' => 'przeniesiono stronę',
// Chapters
'chapter_create' => 'utworzono rozdział',
'chapter_create_notification' => 'Rozdział utworzony pomyślnie',
'chapter_update' => 'zaktualizowano rozdział',
'chapter_update_notification' => 'Rozdział zaktualizowany pomyślnie',
'chapter_delete' => 'usunięto rozdział',
'chapter_delete_notification' => 'Rozdział usunięty pomyślnie',
'chapter_move' => 'przeniesiono rozdział',
// Books
'book_create' => 'utworzono księgę',
'book_create_notification' => 'Księga utworzona pomyślnie',
'book_update' => 'zaktualizowano księgę',
'book_update_notification' => 'Księga zaktualizowana pomyślnie',
'book_delete' => 'usunięto księgę',
'book_delete_notification' => 'Księga usunięta pomyślnie',
'book_sort' => 'posortowano księgę',
'book_sort_notification' => 'Księga posortowana pomyślnie',
];

View File

@ -0,0 +1,76 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
/**
* Login & Register
*/
'sign_up' => 'Zarejestruj się',
'log_in' => 'Zaloguj się',
'log_in_with' => 'Zaloguj się za pomocą :socialDriver',
'sign_up_with' => 'Zarejestruj się za pomocą :socialDriver',
'logout' => 'Wyloguj',
'name' => 'Imię',
'username' => 'Nazwa użytkownika',
'email' => 'Email',
'password' => 'Hasło',
'password_confirm' => 'Potwierdzenie hasła',
'password_hint' => 'Musi mieć więcej niż 5 znaków',
'forgot_password' => 'Przypomnij hasło',
'remember_me' => 'Zapamiętaj mnie',
'ldap_email_hint' => 'Wprowadź adres email dla tego konta.',
'create_account' => 'Utwórz konto',
'social_login' => 'Logowanie za pomocą konta społecznościowego',
'social_registration' => 'Rejestracja za pomocą konta społecznościowego',
'social_registration_text' => 'Zarejestruj się za pomocą innej usługi.',
'register_thanks' => 'Dziękujemy za rejestrację!',
'register_confirm' => 'Sprawdź podany adres e-mail i kliknij w link, by uzyskać dostęp do :appName.',
'registrations_disabled' => 'Rejestracja jest obecnie zablokowana.',
'registration_email_domain_invalid' => 'Adresy e-mail z tej domeny nie mają dostępu do tej aplikacji',
'register_success' => 'Dziękujemy za rejestrację! Zalogowano Cię automatycznie.',
/**
* Password Reset
*/
'reset_password' => 'Resetowanie hasła',
'reset_password_send_instructions' => 'Wprowadź adres e-mail powiązany z Twoim kontem, by otrzymać link do resetowania hasła.',
'reset_password_send_button' => 'Wyślij link do resetowania hasła',
'reset_password_sent_success' => 'Wysłano link do resetowania hasła na adres :email.',
'reset_password_success' => 'Hasło zostało zresetowane pomyślnie.',
'email_reset_subject' => 'Resetowanie hasła do :appName',
'email_reset_text' => 'Otrzymujesz tę wiadomość ponieważ ktoś zażądał zresetowania hasła do Twojego konta.',
'email_reset_not_requested' => 'Jeśli to nie Ty złożyłeś żądanie zresetowania hasła, zignoruj tę wiadomość.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Potwierdź swój adres email w :appName',
'email_confirm_greeting' => 'Dziękujemy za dołączenie do :appName!',
'email_confirm_text' => 'Prosimy byś potwierdził swoje hasło klikając przycisk poniżej:',
'email_confirm_action' => 'Potwierdź email',
'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.',
'email_confirm_success' => 'Adres email został potwierdzony!',
'email_confirm_resent' => 'Wiadomość potwierdzająca została wysłana, sprawdź swoją skrzynkę.',
'email_not_confirmed' => 'Adres email niepotwierdzony',
'email_not_confirmed_text' => 'Twój adres email nie został jeszcze potwierdzony.',
'email_not_confirmed_click_link' => 'Aby potwierdzić swoje konto kliknij w link wysłany w wiadomości po rejestracji.',
'email_not_confirmed_resend' => 'Jeśli wiadomość do Ciebie nie dotarła możesz wysłać ją ponownie wypełniając formularz poniżej.',
'email_not_confirmed_resend_button' => 'Wyślij ponownie wiadomość z potwierdzeniem',
];

View File

@ -0,0 +1,59 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Anuluj',
'confirm' => 'Zatwierdź',
'back' => 'Wstecz',
'save' => 'Zapisz',
'continue' => 'Kontynuuj',
'select' => 'Wybierz',
/**
* Form Labels
*/
'name' => 'Nazwa',
'description' => 'Opis',
'role' => 'Rola',
/**
* Actions
*/
'actions' => 'Akcje',
'view' => 'Widok',
'create' => 'Utwórz',
'update' => 'Zaktualizuj',
'edit' => 'Edytuj',
'sort' => 'Sortuj',
'move' => 'Przenieś',
'delete' => 'Usuń',
'search' => 'Szukaj',
'search_clear' => 'Wyczyść wyszukiwanie',
'reset' => 'Resetuj',
'remove' => 'Usuń',
'add' => 'Dodaj',
/**
* Misc
*/
'deleted_user' => 'Użytkownik usunięty',
'no_activity' => 'Brak aktywności do pokazania',
'no_items' => 'Brak elementów do wyświetlenia',
'back_to_top' => 'Powrót na górę',
'toggle_details' => 'Włącz/wyłącz szczegóły',
/**
* Header
*/
'view_profile' => 'Zobacz profil',
'edit_profile' => 'Edytuj profil',
/**
* Email Content
*/
'email_action_help' => 'Jeśli masz problem z kliknięciem przycisku ":actionText", skopiuj i wklej poniższy adres URL w nowej karcie swojej przeglądarki:',
'email_rights' => 'Wszelkie prawa zastrzeżone',
];

View File

@ -0,0 +1,32 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Wybór obrazka',
'image_all' => 'Wszystkie',
'image_all_title' => 'Zobacz wszystkie obrazki',
'image_book_title' => 'Zobacz obrazki zapisane w tej księdze',
'image_page_title' => 'Zobacz obrazki zapisane na tej stronie',
'image_search_hint' => 'Szukaj po nazwie obrazka',
'image_uploaded' => 'Udostępniono :uploadedDate',
'image_load_more' => 'Wczytaj więcej',
'image_image_name' => 'Nazwa obrazka',
'image_delete_confirm' => 'Ten obrazek jest używany na stronach poniżej, kliknij ponownie Usuń by potwierdzić usunięcie obrazka.',
'image_select_image' => 'Wybierz obrazek',
'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do udostępnienia',
'images_deleted' => 'Usunięte obrazki',
'image_preview' => 'Podgląd obrazka',
'image_upload_success' => 'Obrazek wysłany pomyślnie',
'image_update_success' => 'Szczegóły obrazka zaktualizowane pomyślnie',
'image_delete_success' => 'Obrazek usunięty pomyślnie',
/**
* Code editor
*/
'code_editor' => 'Edytuj kod',
'code_language' => 'Język kodu',
'code_content' => 'Zawartość kodu',
'code_save' => 'Zapisz kod',
];

View File

@ -0,0 +1,237 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'Ostatnio utworzone',
'recently_created_pages' => 'Ostatnio utworzone strony',
'recently_updated_pages' => 'Ostatnio zaktualizowane strony',
'recently_created_chapters' => 'Ostatnio utworzone rozdziały',
'recently_created_books' => 'Ostatnio utworzone księgi',
'recently_update' => 'Ostatnio zaktualizowane',
'recently_viewed' => 'Ostatnio wyświetlane',
'recent_activity' => 'Ostatnia aktywność',
'create_now' => 'Utwórz teraz',
'revisions' => 'Rewizje',
'meta_revision' => 'Rewizja #:revisionCount',
'meta_created' => 'Utworzono :timeLength',
'meta_created_name' => 'Utworzono :timeLength przez :user',
'meta_updated' => 'Zaktualizowano :timeLength',
'meta_updated_name' => 'Zaktualizowano :timeLength przez :user',
'x_pages' => ':count stron',
'entity_select' => 'Wybór encji',
'images' => 'Obrazki',
'my_recent_drafts' => 'Moje ostatnie szkice',
'my_recently_viewed' => 'Moje ostatnio wyświetlane',
'no_pages_viewed' => 'Nie wyświetlano żadnych stron',
'no_pages_recently_created' => 'Nie utworzono ostatnio żadnych stron',
'no_pages_recently_updated' => 'Nie zaktualizowano ostatnio żadnych stron',
'export' => 'Eksportuj',
'export_html' => 'Plik HTML',
'export_pdf' => 'Plik PDF',
'export_text' => 'Plik tekstowy',
/**
* Permissions and restrictions
*/
'permissions' => 'Uprawnienia',
'permissions_intro' => 'Jeśli odblokowane, te uprawnienia będą miały priorytet względem pozostałych ustawionych uprawnień ról.',
'permissions_enable' => 'Odblokuj własne uprawnienia',
'permissions_save' => 'Zapisz uprawnienia',
/**
* Search
*/
'search_results' => 'Wyniki wyszukiwania',
'search_total_results_found' => ':count znalezionych wyników|:count ogółem znalezionych wyników',
'search_clear' => 'Wyczyść wyszukiwanie',
'search_no_pages' => 'Brak stron spełniających zadane kryterium',
'search_for_term' => 'Szukaj :term',
'search_more' => 'Więcej wyników',
'search_filters' => 'Filtry wyszukiwania',
'search_content_type' => 'Rodziaj treści',
'search_exact_matches' => 'Dokładne frazy',
'search_tags' => 'Tagi wyszukiwania',
'search_viewed_by_me' => 'Wyświetlone przeze mnie',
'search_not_viewed_by_me' => 'Niewyświetlone przeze mnie',
'search_permissions_set' => 'Zbiór uprawnień',
'search_created_by_me' => 'Utworzone przeze mnie',
'search_updated_by_me' => 'Zaktualizowane przeze mnie',
'search_updated_before' => 'Zaktualizowane przed',
'search_updated_after' => 'Zaktualizowane po',
'search_created_before' => 'Utworzone przed',
'search_created_after' => 'Utworzone po',
'search_set_date' => 'Ustaw datę',
'search_update' => 'Zaktualizuj wyszukiwanie',
/**
* Books
*/
'book' => 'Księga',
'books' => 'Księgi',
'books_empty' => 'Brak utworzonych ksiąg',
'books_popular' => 'Popularne księgi',
'books_recent' => 'Ostatnie księgi',
'books_popular_empty' => 'Najbardziej popularne księgi zostaną wyświetlone w tym miejscu.',
'books_create' => 'Utwórz księgę',
'books_delete' => 'Usuń księgę',
'books_delete_named' => 'Usuń księgę :bookName',
'books_delete_explain' => 'To spowoduje usunięcie księgi \':bookName\', Wszystkie strony i rozdziały zostaną usunięte.',
'books_delete_confirmation' => 'Czy na pewno chcesz usunąc tę księgę?',
'books_edit' => 'Edytuj księgę',
'books_edit_named' => 'Edytuj księgę :bookName',
'books_form_book_name' => 'Nazwa księgi',
'books_save' => 'Zapisz księgę',
'books_permissions' => 'Uprawnienia księgi',
'books_permissions_updated' => 'Zaktualizowano uprawnienia księgi',
'books_empty_contents' => 'Brak stron lub rozdziałów w tej księdze.',
'books_empty_create_page' => 'Utwórz nową stronę',
'books_empty_or' => 'lub',
'books_empty_sort_current_book' => 'posortuj bieżącą księgę',
'books_empty_add_chapter' => 'Dodaj rozdział',
'books_permissions_active' => 'Uprawnienia księgi aktywne',
'books_search_this' => 'Wyszukaj w tej księdze',
'books_navigation' => 'Nawigacja po księdze',
'books_sort' => 'Sortuj zawartość Księgi',
'books_sort_named' => 'Sortuj księgę :bookName',
'books_sort_show_other' => 'Pokaż inne księgi',
'books_sort_save' => 'Zapisz nowy porządek',
/**
* Chapters
*/
'chapter' => 'Rozdział',
'chapters' => 'Rozdziały',
'chapters_popular' => 'Popularne rozdziały',
'chapters_new' => 'Nowy rozdział',
'chapters_create' => 'Utwórz nowy rozdział',
'chapters_delete' => 'Usuń rozdział',
'chapters_delete_named' => 'Usuń rozdział :chapterName',
'chapters_delete_explain' => 'To spowoduje usunięcie rozdziału \':chapterName\', Wszystkie strony zostaną usunięte
i dodane bezpośrednio do księgi macierzystej.',
'chapters_delete_confirm' => 'Czy na pewno chcesz usunąć ten rozdział?',
'chapters_edit' => 'Edytuj rozdział',
'chapters_edit_named' => 'Edytuj rozdział :chapterName',
'chapters_save' => 'Zapisz rozdział',
'chapters_move' => 'Przenieś rozdział',
'chapters_move_named' => 'Przenieś rozdział :chapterName',
'chapter_move_success' => 'Rozdział przeniesiony do :bookName',
'chapters_permissions' => 'Uprawienia rozdziału',
'chapters_empty' => 'Brak stron w tym rozdziale.',
'chapters_permissions_active' => 'Uprawnienia rozdziału aktywne',
'chapters_permissions_success' => 'Zaktualizowano uprawnienia rozdziału',
'chapters_search_this' => 'Przeszukaj ten rozdział',
/**
* Pages
*/
'page' => 'Strona',
'pages' => 'Strony',
'pages_popular' => 'Popularne strony',
'pages_new' => 'Nowa strona',
'pages_attachments' => 'Załączniki',
'pages_navigation' => 'Nawigacja po stronie',
'pages_delete' => 'Usuń stronę',
'pages_delete_named' => 'Usuń stronę :pageName',
'pages_delete_draft_named' => 'Usuń szkic strony :pageName',
'pages_delete_draft' => 'Usuń szkic strony',
'pages_delete_success' => 'Strona usunięta pomyślnie',
'pages_delete_draft_success' => 'Szkic strony usunięty pomyślnie',
'pages_delete_confirm' => 'Czy na pewno chcesz usunąć tę stron?',
'pages_delete_draft_confirm' => 'Czy na pewno chcesz usunąć szkic strony?',
'pages_editing_named' => 'Edytowanie strony :pageName',
'pages_edit_toggle_header' => 'Włącz/wyłącz nagłówek',
'pages_edit_save_draft' => 'Zapisz szkic',
'pages_edit_draft' => 'Edytuj szkic strony',
'pages_editing_draft' => 'Edytowanie szkicu strony',
'pages_editing_page' => 'Edytowanie strony',
'pages_edit_draft_save_at' => 'Szkic zapisany ',
'pages_edit_delete_draft' => 'Usuń szkic',
'pages_edit_discard_draft' => 'Porzuć szkic',
'pages_edit_set_changelog' => 'Ustaw log zmian',
'pages_edit_enter_changelog_desc' => 'Opisz zmiany, które zostały wprowadzone',
'pages_edit_enter_changelog' => 'Wyświetl log zmian',
'pages_save' => 'Zapisz stronę',
'pages_title' => 'Tytuł strony',
'pages_name' => 'Nazwa strony',
'pages_md_editor' => 'Edytor',
'pages_md_preview' => 'Podgląd',
'pages_md_insert_image' => 'Wstaw obrazek',
'pages_md_insert_link' => 'Wstaw łącze do encji',
'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale',
'pages_move' => 'Przenieś stronę',
'pages_move_success' => 'Strona przeniesiona do ":parentName"',
'pages_permissions' => 'Uprawnienia strony',
'pages_permissions_success' => 'Zaktualizowano uprawnienia strony',
'pages_revisions' => 'Rewizje strony',
'pages_revisions_named' => 'Rewizje strony :pageName',
'pages_revision_named' => 'Rewizja stroony :pageName',
'pages_revisions_created_by' => 'Utworzona przez',
'pages_revisions_date' => 'Data rewizji',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Log zmian',
'pages_revisions_changes' => 'Zmiany',
'pages_revisions_current' => 'Obecna wersja',
'pages_revisions_preview' => 'Podgląd',
'pages_revisions_restore' => 'Przywróć',
'pages_revisions_none' => 'Ta strona nie posiada żadnych rewizji',
'pages_copy_link' => 'Kopiuj link',
'pages_permissions_active' => 'Uprawnienia strony aktywne',
'pages_initial_revision' => 'Wydanie pierwotne',
'pages_initial_name' => 'Nowa strona',
'pages_editing_draft_notification' => 'Edytujesz obecnie szkic, który był ostatnio zapisany :timeDiff.',
'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tego szkicu.',
'pages_draft_edit_active' => [
'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony',
'start_b' => ':userName edytuje stronę',
'time_a' => ' od czasu ostatniej edycji',
'time_b' => 'w ciągu ostatnich :minCount minut',
'message' => ':start :time. Pamiętaj by nie nadpisywać czyichś zmian!',
],
'pages_draft_discarded' => 'Szkic odrzucony, edytor został uzupełniony najnowszą wersją strony',
/**
* Editor sidebar
*/
'page_tags' => 'Tagi strony',
'tag' => 'Tag',
'tags' => '',
'tag_value' => 'Wartość tagu (opcjonalnie)',
'tags_explain' => "Dodaj tagi by skategoryzować zawartość. \n W celu dokładniejszej organizacji zawartości możesz dodać wartości do tagów.",
'tags_add' => 'Dodaj kolejny tag',
'attachments' => 'Załączniki',
'attachments_explain' => 'Udostępnij kilka plików lub załącz link. Będą one widoczne na marginesie strony.',
'attachments_explain_instant_save' => 'Zmiany są zapisywane natychmiastowo.',
'attachments_items' => 'Załączniki',
'attachments_upload' => 'Dodaj plik',
'attachments_link' => 'Dodaj link',
'attachments_set_link' => 'Ustaw link',
'attachments_delete_confirm' => 'Kliknij ponownie Usuń by potwierdzić usunięcie załącznika.',
'attachments_dropzone' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki',
'attachments_no_files' => 'Nie udostępniono plików',
'attachments_explain_link' => 'Możesz załączyć link jeśli nie chcesz udostępniać pliku. Może być to link do innej strony lub link do pliku w chmurze.',
'attachments_link_name' => 'Nazwa linku',
'attachment_link' => 'Link do załącznika',
'attachments_link_url' => 'Link do pliku',
'attachments_link_url_hint' => 'Strona lub plik',
'attach' => 'Załącz',
'attachments_edit_file' => 'Edytuj plik',
'attachments_edit_file_name' => 'Nazwa pliku',
'attachments_edit_drop_upload' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki i nadpisać istniejące',
'attachments_order_updated' => 'Kolejność załączników zaktualizowana',
'attachments_updated_success' => 'Szczegóły załączników zaktualizowane',
'attachments_deleted' => 'Załączniki usunięte',
'attachments_file_uploaded' => 'Plik załączony pomyślnie',
'attachments_file_updated' => 'Plik zaktualizowany pomyślnie',
'attachments_link_attached' => 'Link pomyślnie dodany do strony',
/**
* Profile View
*/
'profile_user_for_x' => 'Użytkownik od :time',
'profile_created_content' => 'Utworzona zawartość',
'profile_not_created_pages' => ':userName nie utworzył żadnych stron',
'profile_not_created_chapters' => ':userName nie utworzył żadnych rozdziałów',
'profile_not_created_books' => ':userName nie utworzył żadnych ksiąg',
];

View File

@ -0,0 +1,70 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'Nie masz uprawnień do wyświetlenia tej strony.',
'permissionJson' => 'Nie masz uprawnień do wykonania tej akcji.',
// Auth
'error_user_exists_different_creds' => 'Użytkownik o adresie :email już istnieje.',
'email_already_confirmed' => 'Email został potwierdzony, spróbuj się zalogować.',
'email_confirmation_invalid' => 'Ten token jest nieprawidłowy lub został już wykorzystany. Spróbuj zarejestrować się ponownie.',
'email_confirmation_expired' => 'Ten token potwierdzający wygasł. Wysłaliśmy Ci kolejny.',
'ldap_fail_anonymous' => 'Dostęp LDAP przy użyciu anonimowego powiązania nie powiódł się',
'ldap_fail_authed' => 'Dostęp LDAP przy użyciu tego dn i hasła nie powiódł się',
'ldap_extension_not_installed' => 'Rozszerzenie LDAP PHP nie zostało zainstalowane',
'ldap_cannot_connect' => 'Nie można połączyć z serwerem LDAP, połączenie nie zostało ustanowione',
'social_no_action_defined' => 'Brak zdefiniowanej akcji',
'social_account_in_use' => 'To konto :socialAccount jest już w użyciu, spróbuj zalogować się za pomocą opcji :socialAccount.',
'social_account_email_in_use' => 'Email :email jest już w użyciu. Jeśli masz już konto, połącz konto :socialAccount z poziomu ustawień profilu.',
'social_account_existing' => 'Konto :socialAccount jest już połączone z Twoim profilem',
'social_account_already_used_existing' => 'Konto :socialAccount jest już używane przez innego użytkownika.',
'social_account_not_used' => 'To konto :socialAccount nie jest połączone z żadnym użytkownikiem. Połącz je ze swoim kontem w ustawieniach profilu. ',
'social_account_register_instructions' => 'Jeśli nie masz jeszcze konta, możesz zarejestrować je używając opcji :socialAccount.',
'social_driver_not_found' => 'Funkcja społecznościowa nie została odnaleziona',
'social_driver_not_configured' => 'Ustawienia konta :socialAccount nie są poprawne.',
// System
'path_not_writable' => 'Zapis do ścieżki :filePath jest niemożliwy. Upewnij się że aplikacja ma prawa do zapisu w niej.',
'cannot_get_image_from_url' => 'Nie można pobrać obrazka z :url',
'cannot_create_thumbs' => 'Serwer nie może utworzyć miniaturek. Upewnij się że rozszerzenie GD PHP zostało zainstalowane.',
'server_upload_limit' => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj udostępnić coś o mniejszym rozmiarze.',
'image_upload_error' => 'Wystąpił błąd podczas udostępniania obrazka',
// Attachments
'attachment_page_mismatch' => 'Niezgodność stron podczas aktualizacji załącznika',
// Pages
'page_draft_autosave_fail' => 'Zapis szkicu nie powiódł się. Upewnij się że posiadasz połączenie z internetem.',
// Entities
'entity_not_found' => 'Encja nie została odnaleziona',
'book_not_found' => 'Księga nie została odnaleziona',
'page_not_found' => 'Strona nie została odnaleziona',
'chapter_not_found' => 'Rozdział nie został odnaleziony',
'selected_book_not_found' => 'Wybrana księga nie została odnaleziona',
'selected_book_chapter_not_found' => 'Wybrana księga lub rozdział nie zostały odnalezione',
'guests_cannot_save_drafts' => 'Goście nie mogą zapisywać szkiców',
// Users
'users_cannot_delete_only_admin' => 'Nie możesz usunąć jedynego administratora',
'users_cannot_delete_guest' => 'Nie możesz usunąć użytkownika-gościa',
// Roles
'role_cannot_be_edited' => 'Ta rola nie może być edytowana',
'role_system_cannot_be_deleted' => 'Ta rola jest rolą systemową i nie może zostać usunięta',
'role_registration_default_cannot_delete' => 'Ta rola nie może zostać usunięta jeśli jest ustawiona jako domyślna rola użytkownika',
// Error pages
'404_page_not_found' => 'Strona nie została odnaleziona',
'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została odnaleziona.',
'return_home' => 'Powrót do strony głównej',
'error_occurred' => 'Wystąpił błąd',
'app_down' => ':appName jest aktualnie wyłączona',
'back_soon' => 'Niedługo zostanie uruchomiona ponownie.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Poprzednia',
'next' => 'Następna &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Hasło musi zawierać co najmniej 6 znaków i być zgodne z powtórzeniem.',
'user' => "Nie znaleziono użytkownika o takim adresie email.",
'token' => 'Ten token resetowania hasła jest nieprawidłowy.',
'sent' => 'Wysłaliśmy Ci link do resetowania hasła!',
'reset' => 'Twoje hasło zostało zresetowane!',
];

View File

@ -0,0 +1,111 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Ustawienia',
'settings_save' => 'Zapisz ustawienia',
'settings_save_success' => 'Ustawienia zapisane',
/**
* App settings
*/
'app_settings' => 'Ustawienia aplikacji',
'app_name' => 'Nazwa aplikacji',
'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i emailach.',
'app_name_header' => 'Pokazać nazwę aplikacji w nagłówku?',
'app_public_viewing' => 'Zezwolić na publiczne przeglądanie?',
'app_secure_images' => 'Odblokować wyższe bezpieczeństwo obrazków?',
'app_secure_images_desc' => 'Ze względów wydajnościowych wszystkie obrazki są publiczne. Ta opcja dodaje dodatkowy, trudny do zgadnienia losowy ciąg na początku nazwy obrazka. Upewnij się że indeksowanie ścieżek jest zablokowane, by uniknąć problemów z dostępem do obrazka.',
'app_editor' => 'Edytor strony',
'app_editor_desc' => 'Wybierz edytor używany przez użytkowników do edycji zawartości.',
'app_custom_html' => 'Własna zawartość tagu <head>',
'app_custom_html_desc' => 'Zawartość dodana tutaj zostanie dołączona do sekcji <head> każdej strony. Przydatne przy nadpisywaniu styli lub dodawaniu analityki.',
'app_logo' => 'Logo aplikacji',
'app_logo_desc' => 'Ten obrazek powinien mieć nie więcej niż 43px w pionie. <br>Większe obrazki będą skalowane w dół.',
'app_primary_color' => 'Podstawowy kolor aplikacji',
'app_primary_color_desc' => 'To powinna być wartość HEX. <br>Zostaw to pole puste, by powrócić do podstawowego koloru.',
/**
* Registration settings
*/
'reg_settings' => 'Ustawienia rejestracji',
'reg_allow' => 'Zezwolić na rejestrację?',
'reg_default_role' => 'Domyślna rola użytkownika po rejestracji',
'reg_confirm_email' => 'Wymagać potwierdzenia adresu email?',
'reg_confirm_email_desc' => 'Jeśli restrykcje domenowe zostały uzupełnione potwierdzenie adresu stanie się konieczne, a poniższa wartośc zostanie zignorowana.',
'reg_confirm_restrict_domain' => 'Restrykcje domenowe dot. adresu email',
'reg_confirm_restrict_domain_desc' => 'Wprowadź listę domen adresów email rozdzieloną przecinkami, którym chciałbyś zezwolić na rejestrację. Wymusi to konieczność potwierdzenia adresu email przez użytkownika przed uzyskaniem dostępu do aplikacji. <br> Pamiętaj, że użytkownicy będą mogli zmienić adres email po rejestracji.',
'reg_confirm_restrict_domain_placeholder' => 'Brak restrykcji',
/**
* Role settings
*/
'roles' => 'Role',
'role_user_roles' => 'Role użytkownika',
'role_create' => 'Utwórz nową rolę',
'role_create_success' => 'Rola utworzona pomyślnie',
'role_delete' => 'Usuń rolę',
'role_delete_confirm' => 'To spowoduje usunięcie roli \':roleName\'.',
'role_delete_users_assigned' => 'Tę rolę ma przypisanych :userCount użytkowników. Jeśli chcesz zmigrować użytkowników z tej roli, wybierz nową poniżej.',
'role_delete_no_migration' => "Nie migruj użytkowników",
'role_delete_sure' => 'Czy na pewno chcesz usunąć tę rolę?',
'role_delete_success' => 'Rola usunięta pomyślnie',
'role_edit' => 'Edytuj rolę',
'role_details' => 'Szczegóły roli',
'role_name' => 'Nazwa roli',
'role_desc' => 'Krótki opis roli',
'role_system' => 'Uprawnienia systemowe',
'role_manage_users' => 'Zarządzanie użytkownikami',
'role_manage_roles' => 'Zarządzanie rolami i uprawnieniami ról',
'role_manage_entity_permissions' => 'Zarządzanie uprawnieniami ksiąg, rozdziałów i stron',
'role_manage_own_entity_permissions' => 'Zarządzanie uprawnieniami własnych ksiąg, rozdziałów i stron',
'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji',
'role_asset' => 'Zarządzanie zasobami',
'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia ksiąg, rozdziałów i stron nadpisują te ustawienia.',
'role_all' => 'Wszyscy',
'role_own' => 'Własne',
'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione',
'role_save' => 'Zapisz rolę',
'role_update_success' => 'Rola zapisana pomyślnie',
'role_users' => 'Użytkownicy w tej roli',
'role_users_none' => 'Brak użytkowników zapisanych do tej roli',
/**
* Users
*/
'users' => 'Użytkownicy',
'user_profile' => 'Profil użytkownika',
'users_add_new' => 'Dodaj użytkownika',
'users_search' => 'Wyszukaj użytkownika',
'users_role' => 'Role użytkownika',
'users_external_auth_id' => 'Zewnętrzne ID autentykacji',
'users_password_warning' => 'Wypełnij poniżej tylko jeśli chcesz zmienić swoje hasło:',
'users_system_public' => 'Ten użytkownik reprezentuje każdego gościa odwiedzającego tę aplikację. Nie można się na niego zalogować, lecz jest przyznawany automatycznie.',
'users_delete' => 'Usuń użytkownika',
'users_delete_named' => 'Usuń :userName',
'users_delete_warning' => 'To usunie użytkownika \':userName\' z systemu.',
'users_delete_confirm' => 'Czy na pewno chcesz usunąć tego użytkownika?',
'users_delete_success' => 'Użytkownik usunięty pomyślnie',
'users_edit' => 'Edytuj użytkownika',
'users_edit_profile' => 'Edytuj profil',
'users_edit_success' => 'Użytkownik zaktualizowany pomyśłnie',
'users_avatar' => 'Avatar użytkownika',
'users_avatar_desc' => 'Ten obrazek powinien mieć 25px x 256px.',
'users_preferred_language' => 'Preferowany język',
'users_social_accounts' => 'Konta społecznościowe',
'users_social_accounts_info' => 'Tutaj możesz połączyć kilka kont społecznościowych w celu łatwiejszego i szybszego logowania.',
'users_social_connect' => 'Podłącz konto',
'users_social_disconnect' => 'Odłącz konto',
'users_social_connected' => ':socialAccount zostało dodane do Twojego profilu.',
'users_social_disconnected' => ':socialAccount zostało odłączone od Twojego profilu.',
];

View File

@ -0,0 +1,108 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute musi zostać zaakceptowany.',
'active_url' => ':attribute nie jest prawidłowym adresem URL.',
'after' => ':attribute musi być datą następującą po :date.',
'alpha' => ':attribute może zawierać wyłącznie litery.',
'alpha_dash' => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.',
'alpha_num' => ':attribute może zawierać wyłącznie litery i cyfry.',
'array' => ':attribute musi być tablicą.',
'before' => ':attribute musi być datą poprzedzającą :date.',
'between' => [
'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.',
'file' => 'Waga :attribute musi zawierać się pomiędzy :min i :max kilobajtów.',
'string' => 'Długość :attribute musi zawierać się pomiędzy :min i :max.',
'array' => ':attribute musi mieć od :min do :max elementów.',
],
'boolean' => ':attribute musi być wartością prawda/fałsz.',
'confirmed' => ':attribute i potwierdzenie muszą być zgodne.',
'date' => ':attribute nie jest prawidłową datą.',
'date_format' => ':attribute musi mieć format :format.',
'different' => ':attribute i :other muszą się różnić.',
'digits' => ':attribute musi mieć :digits cyfr.',
'digits_between' => ':attribute musi mieć od :min do :max cyfr.',
'email' => ':attribute musi być prawidłowym adresem e-mail.',
'filled' => ':attribute jest wymagany.',
'exists' => 'Wybrana wartość :attribute jest nieprawidłowa.',
'image' => ':attribute musi być obrazkiem.',
'in' => 'Wybrana wartość :attribute jest nieprawidłowa.',
'integer' => ':attribute musi być liczbą całkowitą.',
'ip' => ':attribute musi być prawidłowym adresem IP.',
'max' => [
'numeric' => 'Wartość :attribute nie może być większa niż :max.',
'file' => 'Wielkość :attribute nie może być większa niż :max kilobajtów.',
'string' => 'Długość :attribute nie może być większa niż :max znaków.',
'array' => 'Rozmiar :attribute nie może być większy niż :max elementów.',
],
'mimes' => ':attribute musi być plikiem typu: :values.',
'min' => [
'numeric' => 'Wartość :attribute nie może być mniejsza od :min.',
'file' => 'Wielkość :attribute nie może być mniejsza niż :min kilobajtów.',
'string' => 'Długość :attribute nie może być mniejsza niż :min znaków.',
'array' => 'Rozmiar :attribute musi posiadać co najmniej :min elementy.',
],
'not_in' => 'Wartość :attribute jest nieprawidłowa.',
'numeric' => ':attribute musi być liczbą.',
'regex' => 'Format :attribute jest nieprawidłowy.',
'required' => 'Pole :attribute jest wymagane.',
'required_if' => 'Pole :attribute jest wymagane jeśli :other ma wartość :value.',
'required_with' => 'Pole :attribute jest wymagane jeśli :values zostało wprowadzone.',
'required_with_all' => 'Pole :attribute jest wymagane jeśli :values są obecne.',
'required_without' => 'Pole :attribute jest wymagane jeśli :values nie zostało wprowadzone.',
'required_without_all' => 'Pole :attribute jest wymagane jeśli żadna z wartości :values nie została podana.',
'same' => 'Pole :attribute i :other muszą być takie same.',
'size' => [
'numeric' => ':attribute musi mieć długość :size.',
'file' => ':attribute musi mieć :size kilobajtów.',
'string' => ':attribute mmusi mieć długość :size znaków.',
'array' => ':attribute musi posiadać :size elementów.',
],
'string' => ':attribute musi być ciągiem znaków.',
'timezone' => ':attribute musi być prawidłową strefą czasową.',
'unique' => ':attribute zostało już zajęte.',
'url' => 'Format :attribute jest nieprawidłowy.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'Potwierdzenie hasła jest wymagane.',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -77,7 +77,7 @@
@yield('content')
</section>
<div id="back-to-top">
<div back-to-top>
<div class="inner">
<i class="zmdi zmdi-chevron-up"></i> <span>{{ trans('common.back_to_top') }}</span>
</div>

View File

@ -1,8 +1,10 @@
<div class="book entity-list-item" data-entity-type="book" data-entity-id="{{$book->id}}">
<h4 class="text-book"><a class="text-book entity-list-item-link" href="{{$book->getUrl()}}"><i class="zmdi zmdi-book"></i><span class="entity-list-item-name">{{$book->name}}</span></a></h4>
<div class="entity-item-snippet">
@if(isset($book->searchSnippet))
<p class="text-muted">{!! $book->searchSnippet !!}</p>
@else
<p class="text-muted">{{ $book->getExcerpt() }}</p>
@endif
</div>
</div>

View File

@ -5,10 +5,10 @@
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-6 faded">
<div class="col-sm-6 col-xs-1 faded">
@include('books._breadcrumbs', ['book' => $book])
</div>
<div class="col-sm-6">
<div class="col-sm-6 col-xs-11">
<div class="action-buttons faded">
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div>
@ -50,13 +50,13 @@
</div>
<div class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
<div ng-non-bindable class="container" id="entity-dashboard" entity-id="{{ $book->id }}" entity-type="book">
<div class="row">
<div class="col-md-7">
<h1>{{$book->name}}</h1>
<div class="book-content" v-if="!searching">
<p class="text-muted" v-pre>{{$book->description}}</p>
<p class="text-muted" v-pre>{!! nl2br(e($book->description)) !!}</p>
<div class="page-list" v-pre>
<hr>
@ -72,9 +72,15 @@
@else
<p class="text-muted">{{ trans('entities.books_empty_contents') }}</p>
<p>
@if(userCan('page-create', $book))
<a href="{{ $book->getUrl('/page/create') }}" class="text-page"><i class="zmdi zmdi-file-text"></i>{{ trans('entities.books_empty_create_page') }}</a>
@endif
@if(userCan('page-create', $book) && userCan('chapter-create', $book))
&nbsp;&nbsp;<em class="text-muted">-{{ trans('entities.books_empty_or') }}-</em>&nbsp;&nbsp;&nbsp;
@endif
@if(userCan('chapter-create', $book))
<a href="{{ $book->getUrl('/chapter/create') }}" class="text-chapter"><i class="zmdi zmdi-collection-bookmark"></i>{{ trans('entities.books_empty_add_chapter') }}</a>
@endif
</p>
<hr>
@endif
@ -106,7 +112,7 @@
@endif
<div class="search-box">
<form v-on:submit="searchBook">
<form v-on:submit.prevent="searchBook">
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.books_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button>
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>

View File

@ -2,7 +2,7 @@
<h4>
@if (isset($showPath) && $showPath)
<a href="{{ $chapter->book->getUrl() }}" class="text-book">
<i class="zmdi zmdi-book"></i>{{ $chapter->book->name }}
<i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}
</a>
<span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span>
@endif
@ -10,14 +10,18 @@
<i class="zmdi zmdi-collection-bookmark"></i><span class="entity-list-item-name">{{ $chapter->name }}</span>
</a>
</h4>
<div class="entity-item-snippet">
@if(isset($chapter->searchSnippet))
<p class="text-muted">{!! $chapter->searchSnippet !!}</p>
@else
<p class="text-muted">{{ $chapter->getExcerpt() }}</p>
@endif
</div>
@if(!isset($hidePages) && count($chapter->pages) > 0)
<p class="text-muted chapter-toggle"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p>
<p chapter-toggle class="text-muted"><i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $chapter->pages->count()]) }}</span></p>
<div class="inset-list">
@foreach($chapter->pages as $page)
<h5 class="@if($page->draft) draft @endif"><a href="{{ $page->getUrl() }}" class="text-page @if($page->draft) draft @endif"><i class="zmdi zmdi-file-text"></i>{{$page->name}}</a></h5>

View File

@ -5,10 +5,10 @@
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-8 faded" ng-non-bindable>
<div class="col-sm-6 col-xs-3 faded" ng-non-bindable>
@include('chapters._breadcrumbs', ['chapter' => $chapter])
</div>
<div class="col-sm-4 faded">
<div class="col-sm-6 col-xs-9 faded">
<div class="action-buttons">
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div>
@ -47,12 +47,12 @@
</div>
<div class="container" id="entity-dashboard" entity-id="{{ $chapter->id }}" entity-type="chapter">
<div class="container" id="entity-dashboard" ng-non-bindable entity-id="{{ $chapter->id }}" entity-type="chapter">
<div class="row">
<div class="col-md-7">
<h1>{{ $chapter->name }}</h1>
<div class="chapter-content" v-if="!searching">
<p class="text-muted">{{ $chapter->description }}</p>
<p class="text-muted">{!! nl2br(e($chapter->description)) !!}</p>
@if(count($pages) > 0)
<div class="page-list">
@ -116,7 +116,7 @@
@endif
<div class="search-box">
<form v-on:submit="searchBook">
<form v-on:submit.prevent="searchBook">
<input v-model="searchTerm" v-on:change="checkSearchForm()" type="text" name="term" placeholder="{{ trans('entities.chapters_search_this') }}">
<button type="submit"><i class="zmdi zmdi-search"></i></button>
<button v-if="searching" v-cloak class="text-neg" v-on:click="clearSearch()" type="button"><i class="zmdi zmdi-close"></i></button>

View File

@ -0,0 +1,51 @@
<div id="code-editor">
<div overlay ref="overlay" v-cloak @click="hide()">
<div class="popup-body" @click.stop>
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.code_editor') }}</div>
<button class="overlay-close neg corner-button button" @click="hide()">x</button>
</div>
<div class="padded">
<div class="form-group">
<label for="code-editor-language">{{ trans('components.code_language') }}</label>
<div class="lang-options">
<small>
<a @click="updateLanguage('CSS')">CSS</a>
<a @click="updateLanguage('C')">C</a>
<a @click="updateLanguage('C++')">C++</a>
<a @click="updateLanguage('C#')">C#</a>
<a @click="updateLanguage('Go')">Go</a>
<a @click="updateLanguage('HTML')">HTML</a>
<a @click="updateLanguage('Java')">Java</a>
<a @click="updateLanguage('JavaScript')">JavaScript</a>
<a @click="updateLanguage('JSON')">JSON</a>
<a @click="updateLanguage('PHP')">PHP</a>
<a @click="updateLanguage('MarkDown')">MarkDown</a>
<a @click="updateLanguage('Nginx')">Nginx</a>
<a @click="updateLanguage('Python')">Python</a>
<a @click="updateLanguage('Ruby')">Ruby</a>
<a @click="updateLanguage('shell')">Shell/Bash</a>
<a @click="updateLanguage('SQL')">SQL</a>
<a @click="updateLanguage('XML')">XML</a>
<a @click="updateLanguage('YAML')">YAML</a>
</small>
</div>
<input @keypress.enter="save()" id="code-editor-language" type="text" @input="updateEditorMode(language)" v-model="language">
</div>
<div class="form-group">
<label for="code-editor-content">{{ trans('components.code_content') }}</label>
<textarea ref="editor" v-model="code"></textarea>
</div>
<div class="form-group">
<button type="button" class="button pos" @click="save()">{{ trans('components.code_save') }}</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,9 +1,9 @@
<div id="entity-selector-wrap">
<div class="overlay" entity-link-selector>
<div overlay entity-link-selector>
<div class="popup-body small flex-child">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('entities.entity_select') }}</div>
<button type="button" class="corner-button neg button popup-close">x</button>
<button type="button" class="corner-button neg button overlay-close">x</button>
</div>
@include('components.entity-selector', ['name' => 'entity-selector'])
<div class="popup-footer">

View File

@ -1,91 +1,89 @@
<div id="image-manager" image-type="{{ $imageType }}" ng-controller="ImageManagerController" uploaded-to="{{ $uploaded_to or 0 }}">
<div class="overlay" ng-cloak ng-click="hide()">
<div class="popup-body" ng-click="$event.stopPropagation()">
<div id="image-manager" image-type="{{ $imageType }}" uploaded-to="{{ $uploaded_to or 0 }}">
<div overlay v-cloak>
<div class="popup-body" @click.stop="">
<div class="popup-header primary-background">
<div class="popup-title">{{ trans('components.image_select') }}</div>
<button class="popup-close neg corner-button button">x</button>
<button class="overlay-close neg corner-button button">x</button>
</div>
<div class="flex-fill image-manager-body">
<div class="image-manager-content">
<div ng-if="imageType === 'gallery'" class="container">
<div v-if="imageType === 'gallery'" class="container">
<div class="image-manager-header row faded-small nav-tabs">
<div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" ng-class="{selected: (view=='all')}" ng-click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" ng-class="{selected: (view=='book')}" ng-click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" ng-class="{selected: (view=='page')}" ng-click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_all_title') }}" :class="{selected: (view=='all')}" @click="setView('all')"><i class="zmdi zmdi-collection-image"></i> {{ trans('components.image_all') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_book_title') }}" :class="{selected: (view=='book')}" @click="setView('book')"><i class="zmdi zmdi-book text-book"></i> {{ trans('entities.book') }}</div>
<div class="col-xs-4 tab-item" title="{{ trans('components.image_page_title') }}" :class="{selected: (view=='page')}" @click="setView('page')"><i class="zmdi zmdi-file-text text-page"></i> {{ trans('entities.page') }}</div>
</div>
</div>
<div ng-show="view === 'all'" >
<form ng-submit="searchImages()" class="contained-search-box">
<input type="text" placeholder="{{ trans('components.image_search_hint') }}" ng-model="searchTerm">
<button ng-class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" ng-click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
<button title="{{ trans('common.search') }}" class="text-button" type="submit"><i class="zmdi zmdi-search"></i></button>
<div v-show="view === 'all'" >
<form @submit="searchImages" class="contained-search-box">
<input placeholder="{{ trans('components.image_search_hint') }}" v-model="searchTerm">
<button :class="{active: searching}" title="{{ trans('common.search_clear') }}" type="button" @click="cancelSearch()" class="text-button cancel"><i class="zmdi zmdi-close-circle-o"></i></button>
<button title="{{ trans('common.search') }}" class="text-button"><i class="zmdi zmdi-search"></i></button>
</form>
</div>
<div class="image-manager-list">
<div ng-repeat="image in images">
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
<div v-if="images.length > 0" v-for="(image, idx) in images">
<div class="image anim fadeIn" :style="{animationDelay: (idx > 26) ? '160ms' : ((idx * 25) + 'ms')}"
:class="{selected: (image==selectedImage)}" @click="imageSelect(image)">
<img :src="image.thumbs.gallery" :alt="image.title" :title="image.name">
<div class="image-meta">
<span class="name" ng-bind="image.name"></span>
<span class="name" v-text="image.name"></span>
<span class="date">{{ trans('components.image_uploaded', ['uploadedDate' => "{{ getDate(image.created_at) }" . "}"]) }}</span>
</div>
</div>
</div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">{{ trans('components.image_load_more') }}</div>
<div class="load-more" v-show="hasMore" @click="fetchData">{{ trans('components.image_load_more') }}</div>
</div>
</div>
<div class="image-manager-sidebar">
<div class="inner">
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
<div class="image-manager-details anim fadeIn" v-if="selectedImage">
<form ng-submit="saveImageDetails($event)">
<form @submit.prevent="saveImageDetails">
<div>
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
<a :href="selectedImage.url" target="_blank" style="display: block;">
<img :src="selectedImage.thumbs.gallery" :alt="selectedImage.title"
:title="selectedImage.name">
</a>
</div>
<div class="form-group">
<label for="name">{{ trans('components.image_image_name') }}</label>
<input type="text" id="name" name="name" ng-model="selectedImage.name">
<input id="name" name="name" v-model="selectedImage.name">
</div>
</form>
<div ng-show="dependantPages">
<div v-show="dependantPages">
<p class="text-neg text-small">
{{ trans('components.image_delete_confirm') }}
</p>
<ul class="text-neg">
<li ng-repeat="page in dependantPages">
<a ng-href="@{{ page.url }}" target="_blank" class="text-neg" ng-bind="page.name"></a>
<li v-for="page in dependantPages">
<a :href="page.url" target="_blank" class="text-neg" v-text="page.name"></a>
</li>
</ul>
</div>
<div class="clearfix">
<form class="float left" ng-submit="deleteImage($event)">
<form class="float left" @submit.prevent="deleteImage">
<button class="button icon neg"><i class="zmdi zmdi-delete"></i></button>
</form>
<button class="button pos anim fadeIn float right" ng-show="selectedImage" ng-click="selectButtonClick()">
<button class="button pos anim fadeIn float right" v-show="selectedImage" @click="callbackAndHide(selectedImage)">
<i class="zmdi zmdi-square-right"></i>{{ trans('components.image_select_image') }}
</button>
</div>
</div>
<drop-zone message="{{ trans('components.image_dropzone') }}" upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
<dropzone placeholder="{{ trans('components.image_dropzone') }}" :upload-url="uploadUrl" :uploaded-to="uploadedTo" @success="uploadSuccess"></dropzone>
</div>
</div>
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="row">
<div class="col-sm-6 faded">
<div class="action-buttons text-left">
<a data-action="expand-entity-list-details" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>{{ trans('common.toggle_details') }}</a>
<a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button"><i class="zmdi zmdi-wrap-text"></i>{{ trans('common.toggle_details') }}</a>
</div>
</div>
</div>

View File

@ -21,6 +21,7 @@
</div>
@include('components.image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
@include('components.code-editor')
@include('components.entity-selector-popup')
@stop

View File

@ -9,31 +9,40 @@
@endif
</div>
<div toolbox-tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
<div toolbox-tab-content="tags" id="tag-manager" page-id="{{ $page->id or 0 }}">
<h4>{{ trans('entities.page_tags') }}</h4>
<div class="padded tags">
<p class="muted small">{!! nl2br(e(trans('entities.tags_explain'))) !!}</p>
<table class="no-style" tag-autosuggestions style="width: 100%;">
<tbody ui-sortable="sortOptions" ng-model="tags" >
<tr ng-repeat="tag in tags track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
<td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/names') }}" autosuggest-type="name" class="outline" ng-attr-name="tags[@{{$index}}][name]" type="text" ng-model="tag.name" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"></td>
<td><input autosuggest="{{ baseUrl('/ajax/tags/suggest/values') }}" autosuggest-type="value" class="outline" ng-attr-name="tags[@{{$index}}][value]" type="text" ng-model="tag.value" ng-change="tagChange(tag)" ng-blur="tagBlur(tag)" placeholder="{{ trans('entities.tag_value') }}"></td>
<td width="10" ng-show="tags.length != 1" class="text-center text-neg" style="padding: 0;" ng-click="removeTag(tag)"><i class="zmdi zmdi-close"></i></td>
</tr>
</tbody>
</table>
<draggable class="fake-table no-style tag-table" :options="{handle: '.handle'}" :list="tags" element="div" style="width: 100%;">
<transition-group name="test" tag="div">
<div v-for="(tag, i) in tags" :key="tag.key">
<div width="20" class="handle" ><i class="zmdi zmdi-menu"></i></div>
<div>
<autosuggest url="/ajax/tags/suggest/names" type="name" class="outline" :name="getTagFieldName(i, 'name')"
v-model="tag.name" @input="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
</div>
<div>
<autosuggest url="/ajax/tags/suggest/values" type="value" class="outline" :name="getTagFieldName(i, 'value')"
v-model="tag.value" @change="tagChange(tag)" @blur="tagBlur(tag)" placeholder="{{ trans('entities.tag') }}"/>
</div>
<div width="10" v-show="tags.length !== 1" class="text-center text-neg" style="padding: 0;" @click="removeTag(tag)"><i class="zmdi zmdi-close"></i></div>
</div>
</transition-group>
</draggable>
<table class="no-style" style="width: 100%;">
<tbody>
<tr class="unsortable">
<td width="34"></td>
<td ng-click="addEmptyTag()">
<td @click="addEmptyTag">
<button type="button" class="text-button">{{ trans('entities.tags_add') }}</button>
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
@ -81,15 +90,15 @@
<p class="muted small">{{ trans('entities.attachments_explain_link') }}</p>
<div class="form-group">
<label for="attachment-via-link">{{ trans('entities.attachments_link_name') }}</label>
<input type="text" placeholder="{{ trans('entities.attachments_link_name') }}" ng-model="file.name">
<input placeholder="{{ trans('entities.attachments_link_name') }}" ng-model="file.name">
<p class="small neg" ng-repeat="error in errors.link.name" ng-bind="error"></p>
</div>
<div class="form-group">
<label for="attachment-via-link">{{ trans('entities.attachments_link_url') }}</label>
<input type="text" placeholder="{{ trans('entities.attachments_link_url_hint') }}" ng-model="file.link">
<input placeholder="{{ trans('entities.attachments_link_url_hint') }}" ng-model="file.link">
<p class="small neg" ng-repeat="error in errors.link.link" ng-bind="error"></p>
</div>
<button type="submit" class="button pos">{{ trans('entities.attach') }}</button>
<button class="button pos">{{ trans('entities.attach') }}</button>
</div>
</div>
@ -117,14 +126,14 @@
<div tab-content="link">
<div class="form-group">
<label for="attachment-link-edit">{{ trans('entities.attachments_link_url') }}</label>
<input type="text" id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" ng-model="editFile.link">
<input id="attachment-link-edit" placeholder="{{ trans('entities.attachment_link') }}" ng-model="editFile.link">
<p class="small neg" ng-repeat="error in errors.edit.link" ng-bind="error"></p>
</div>
</div>
</div>
<button type="button" class="button" ng-click="cancelEdit()">{{ trans('common.back') }}</button>
<button type="submit" class="button pos">{{ trans('common.save') }}</button>
<button class="button pos">{{ trans('common.save') }}</button>
</div>
</div>

View File

@ -1,13 +1,27 @@
<div class="page {{$page->draft ? 'draft' : ''}} entity-list-item" data-entity-type="page" data-entity-id="{{$page->id}}">
<h4>
@if (isset($showPath) && $showPath)
<a href="{{ $page->book->getUrl() }}" class="text-book">
<i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}
</a>
<span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span>
@if($page->chapter)
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter">
<i class="zmdi zmdi-collection-bookmark"></i>{{ $page->chapter->getShortName() }}
</a>
<span class="text-muted">&nbsp;&nbsp;&raquo;&nbsp;&nbsp;</span>
@endif
@endif
<a href="{{ $page->getUrl() }}" class="text-page entity-list-item-link"><i class="zmdi zmdi-file-text"></i><span class="entity-list-item-name">{{ $page->name }}</span></a>
</h4>
<div class="entity-item-snippet">
@if(isset($page->searchSnippet))
<p class="text-muted">{!! $page->searchSnippet !!}</p>
@else
<p class="text-muted">{{ $page->getExcerpt() }}</p>
@endif
</div>
@if(isset($style) && $style === 'detailed')
<div class="row meta text-muted text-small">

View File

@ -5,10 +5,10 @@
<div class="faded-small toolbar">
<div class="container">
<div class="row">
<div class="col-sm-6 faded">
<div class="col-sm-8 col-xs-5 faded">
@include('pages._breadcrumbs', ['page' => $page])
</div>
<div class="col-sm-6 faded">
<div class="col-sm-4 col-xs-7 faded">
<div class="action-buttons">
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>{{ trans('entities.export') }}</div>

View File

@ -51,7 +51,7 @@
</a>
@if($bookChild->isA('chapter') && count($bookChild->pages) > 0)
<p class="text-muted chapter-toggle @if($bookChild->matchesOrContains($current)) open @endif">
<p chapter-toggle class="text-muted @if($bookChild->matchesOrContains($current)) open @endif">
<i class="zmdi zmdi-caret-right"></i> <i class="zmdi zmdi-file-text"></i> <span>{{ trans('entities.x_pages', ['count' => $bookChild->pages->count()]) }}</span>
</p>
<ul class="menu sub-menu inset-list @if($bookChild->matchesOrContains($current)) open @endif">

View File

@ -1,5 +1,5 @@
<style id="custom-styles" data-color="{{ setting('app-color') }}" data-color-light="{{ setting('app-color-light') }}">
header, #back-to-top, .primary-background {
header, [back-to-top], .primary-background {
background-color: {{ setting('app-color') }} !important;
}
.faded-small, .primary-background-light {

View File

@ -1,12 +1,12 @@
<div class="notification anim pos" @if(!session()->has('success')) style="display:none;" @endif>
<div notification="success" data-autohide class="pos" @if(session()->has('success')) data-show @endif>
<i class="zmdi zmdi-check-circle"></i> <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span>
</div>
<div class="notification anim warning stopped" @if(!session()->has('warning')) style="display:none;" @endif>
<div notification="warning" class="warning" @if(session()->has('warning')) data-show @endif>
<i class="zmdi zmdi-info"></i> <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span>
</div>
<div class="notification anim neg stopped" @if(!session()->has('error')) style="display:none;" @endif>
<div notification="error" class="neg" @if(session()->has('error')) data-show @endif>
<i class="zmdi zmdi-alert-circle"></i> <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span>
</div>

View File

@ -2,7 +2,7 @@
@if(count($entities) > 0)
@foreach($entities as $index => $entity)
@if($entity->isA('page'))
@include('pages/list-item', ['page' => $entity])
@include('pages/list-item', ['page' => $entity, 'showPath' => true])
@elseif($entity->isA('book'))
@include('books/list-item', ['book' => $entity])
@elseif($entity->isA('chapter'))

View File

@ -11,6 +11,7 @@ class LdapTest extends BrowserKitTest
public function setUp()
{
parent::setUp();
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
$this->mockLdap = \Mockery::mock(\BookStack\Services\Ldap::class);
$this->app['BookStack\Services\Ldap'] = $this->mockLdap;
@ -21,6 +22,7 @@ class LdapTest extends BrowserKitTest
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
@ -49,6 +51,7 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
$this->mockLdap->shouldReceive('setOption')->times(2);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
@ -72,6 +75,7 @@ class LdapTest extends BrowserKitTest
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(2);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [

View File

@ -1,5 +1,6 @@
<?php namespace Tests;
use BookStack\Entity;
use BookStack\Role;
use BookStack\Services\PermissionService;
use Illuminate\Contracts\Console\Kernel;
@ -117,6 +118,16 @@ abstract class BrowserKitTest extends TestCase
];
}
/**
* Helper for updating entity permissions.
* @param Entity $entity
*/
protected function updateEntityPermissions(Entity $entity)
{
$restrictionService = $this->app[PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($entity);
}
/**
* Quick way to create a new user
* @param array $attributes

View File

@ -1,6 +1,9 @@
<?php namespace Tests;
use BookStack\Chapter;
use BookStack\Page;
class EntitySearchTest extends TestCase
{
@ -75,10 +78,10 @@ class EntitySearchTest extends TestCase
])
];
$pageA = \BookStack\Page::first();
$pageA = Page::first();
$pageA->tags()->saveMany($newTags);
$pageB = \BookStack\Page::all()->last();
$pageB = Page::all()->last();
$pageB->tags()->create(['name' => 'animal', 'value' => 'dog']);
$this->asEditor();
@ -160,8 +163,8 @@ class EntitySearchTest extends TestCase
public function test_ajax_entity_search()
{
$page = \BookStack\Page::all()->last();
$notVisitedPage = \BookStack\Page::first();
$page = Page::all()->last();
$notVisitedPage = Page::first();
// Visit the page to make popular
$this->asEditor()->get($page->getUrl());
@ -176,4 +179,20 @@ class EntitySearchTest extends TestCase
$defaultListTest->assertSee($page->name);
$defaultListTest->assertDontSee($notVisitedPage->name);
}
public function test_ajax_entity_serach_shows_breadcrumbs()
{
$chapter = Chapter::first();
$page = $chapter->pages->first();
$this->asEditor();
$pageSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
$pageSearch->assertSee($page->name);
$pageSearch->assertSee($chapter->getShortName());
$pageSearch->assertSee($page->book->getShortName());
$chapterSearch = $this->get('/ajax/search/entities?term=' . urlencode($chapter->name));
$chapterSearch->assertSee($chapter->name);
$chapterSearch->assertSee($chapter->book->getShortName());
}
}

View File

@ -1,7 +1,10 @@
<?php namespace Tests;
use BookStack\Page;
use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
use Laravel\BrowserKitTesting\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RolesTest extends BrowserKitTest
{
@ -580,8 +583,6 @@ class RolesTest extends BrowserKitTest
->see('Cannot be deleted');
}
public function test_image_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['image-update-all']);
@ -620,6 +621,42 @@ class RolesTest extends BrowserKitTest
->dontSeeInDatabase('images', ['id' => $image->id]);
}
public function test_role_permission_removal()
{
// To cover issue fixed in f99c8ff99aee9beb8c692f36d4b84dc6e651e50a.
$page = Page::first();
$viewerRole = \BookStack\Role::getRole('viewer');
$viewer = $this->getViewer();
$this->actingAs($viewer)->visit($page->getUrl())->assertResponseOk();
$this->asAdmin()->put('/settings/roles/' . $viewerRole->id, [
'display_name' => $viewerRole->display_name,
'description' => $viewerRole->description,
'permission' => []
])->assertResponseStatus(302);
$this->expectException(HttpException::class);
$this->actingAs($viewer)->visit($page->getUrl())->assertResponseStatus(404);
}
public function test_empty_state_actions_not_visible_without_permission()
{
$admin = $this->getAdmin();
// Book links
$book = factory(\BookStack\Book::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->updateEntityPermissions($book);
$this->actingAs($this->getViewer())->visit($book->getUrl())
->dontSee('Create a new page')
->dontSee('Add a chapter');
// Chapter links
$chapter = factory(\BookStack\Chapter::class)->create(['created_by' => $admin->id, 'updated_by' => $admin->id, 'book_id' => $book->id]);
$this->updateEntityPermissions($chapter);
$this->actingAs($this->getViewer())->visit($chapter->getUrl())
->dontSee('Create a new page')
->dontSee('Sort the current book');
}
public function test_comment_create_permission () {
$ownPage = $this->createEntityChainBelongingToUser($this->user)['page'];

View File

@ -1 +1 @@
v0.16-dev
v0.18-dev