From 89be30ff0ed8a8a59d1bebb7f4e6828f179a35f5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 16 Oct 2018 18:49:16 +0100 Subject: [PATCH 001/160] Started on a design update - Added base of new grid system. - Added new margin/padding/visiblity helpers. - Made header collapse to overflow menu on mobile. --- app/Entities/Entity.php | 14 +++ app/Entities/Page.php | 11 -- resources/assets/icons/bookshelf.svg | 3 +- .../js/components/header-mobile-toggle.js | 31 +++++ resources/assets/js/components/index.js | 1 + resources/assets/sass/_blocks.scss | 71 ++++++----- resources/assets/sass/_colors.scss | 100 ++++++++++++++++ resources/assets/sass/_grid.scss | 68 ++++++++++- resources/assets/sass/_header.scss | 112 ++++++++++++------ resources/assets/sass/_lists.scss | 47 +++++--- resources/assets/sass/_mixins.scss | 3 + resources/assets/sass/_pages.scss | 27 +++++ resources/assets/sass/_text.scss | 100 ---------------- resources/assets/sass/_variables.scss | 6 +- resources/assets/sass/styles.scss | 1 + resources/views/base.blade.php | 61 ++-------- resources/views/common/header.blade.php | 58 +++++++++ resources/views/common/home.blade.php | 74 ++++++------ resources/views/pages/list-item.blade.php | 73 ++++++------ .../views/partials/_header-dropdown.blade.php | 17 --- .../views/partials/activity-item.blade.php | 12 +- .../views/partials/entity-list-item.blade.php | 13 ++ .../views/partials/entity-list.blade.php | 19 +-- resources/views/simple-layout.blade.php | 8 +- 24 files changed, 556 insertions(+), 374 deletions(-) create mode 100644 resources/assets/js/components/header-mobile-toggle.js create mode 100644 resources/assets/sass/_colors.scss create mode 100644 resources/views/common/header.blade.php delete mode 100644 resources/views/partials/_header-dropdown.blade.php create mode 100644 resources/views/partials/entity-list-item.blade.php diff --git a/app/Entities/Entity.php b/app/Entities/Entity.php index 21d172e70..7917f83f8 100644 --- a/app/Entities/Entity.php +++ b/app/Entities/Entity.php @@ -218,6 +218,20 @@ class Entity extends Ownable return $this->{$this->textField}; } + /** + * Get an excerpt of this entity's descriptive content to the specified length. + * @param int $length + * @return mixed + */ + public function getExcerpt(int $length = 100) + { + $text = $this->getText(); + if (mb_strlen($text) > $length) { + $text = mb_substr($text, 0, $length-3) . '...'; + } + return trim($text); + } + /** * Return a generalised, common raw query that can be 'unioned' across entities. * @return string diff --git a/app/Entities/Page.php b/app/Entities/Page.php index ea7df16f4..1c2cc5cff 100644 --- a/app/Entities/Page.php +++ b/app/Entities/Page.php @@ -102,17 +102,6 @@ class Page extends Entity return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent); } - /** - * Get an excerpt of this page's content to the specified length. - * @param int $length - * @return mixed - */ - public function getExcerpt($length = 100) - { - $text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text; - return mb_convert_encoding($text, 'UTF-8'); - } - /** * Return a generalised, common raw query that can be 'unioned' across entities. * @param bool $withContent diff --git a/resources/assets/icons/bookshelf.svg b/resources/assets/icons/bookshelf.svg index 03da68f96..f1e45eaf9 100644 --- a/resources/assets/icons/bookshelf.svg +++ b/resources/assets/icons/bookshelf.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/resources/assets/js/components/header-mobile-toggle.js b/resources/assets/js/components/header-mobile-toggle.js new file mode 100644 index 000000000..eccd4b8f0 --- /dev/null +++ b/resources/assets/js/components/header-mobile-toggle.js @@ -0,0 +1,31 @@ + +class HeaderMobileToggle { + + constructor(elem) { + this.elem = elem; + this.toggleButton = elem.querySelector('.mobile-menu-toggle'); + this.menu = elem.querySelector('.header-links'); + this.open = false; + + this.toggleButton.addEventListener('click', this.onToggle.bind(this)); + this.onWindowClick = this.onWindowClick.bind(this); + } + + onToggle(event) { + this.open = !this.open; + this.menu.classList.toggle('show', this.open); + if (this.open) { + window.addEventListener('click', this.onWindowClick) + } else { + window.removeEventListener('click', this.onWindowClick) + } + event.stopPropagation(); + } + + onWindowClick(event) { + this.onToggle(event); + } + +} + +module.exports = HeaderMobileToggle; \ No newline at end of file diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index 768e0983f..a9ce31362 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -20,6 +20,7 @@ let componentMapping = { 'page-display': require('./page-display'), 'shelf-sort': require('./shelf-sort'), 'homepage-control': require('./homepage-control'), + 'header-mobile-toggle': require('./header-mobile-toggle'), }; window.components = {}; diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index c0f02ed7d..a19ca0278 100644 --- a/resources/assets/sass/_blocks.scss +++ b/resources/assets/sass/_blocks.scss @@ -24,33 +24,9 @@ } } -/* -* Bordering -*/ -.bordered { - border: 1px solid #BBB; - &.pos { - border-color: $positive; - } - &.neg { - border-color: $negative; - } - &.primary { - border-color: $primary; - } - &.secondary { - border-color: $secondary; - } - &.thick { - border-width: 2px; - } -} -.rounded { - border-radius: 3px; -} - /* * Padding +* TODO - Remove these older styles */ .nopadding { padding: 0; @@ -94,6 +70,7 @@ /* * Margins +* TODO - Remove these older styles */ .margins { margin: $-l; @@ -126,6 +103,38 @@ } } +@mixin spacing($prop, $propLetter) { + @each $sizeLetter, $size in $spacing { + .#{$propLetter}-#{$sizeLetter} { + #{$prop}: $size; + } + .#{$propLetter}x-#{$sizeLetter} { + #{$prop}-left: $size; + #{$prop}-right: $size; + } + .#{$propLetter}y-#{$sizeLetter} { + #{$prop}-top: $size; + #{$prop}-bottom: $size; + } + .#{$propLetter}t-#{$sizeLetter} { + #{$prop}-top: $size; + } + .#{$propLetter}r-#{$sizeLetter} { + #{$prop}-right: $size; + } + .#{$propLetter}b-#{$sizeLetter} { + #{$prop}-bottom: $size; + } + .#{$propLetter}l-#{$sizeLetter} { + #{$prop}-left: $size; + } + } + +} + +@include spacing('margin', 'm') +@include spacing('padding', 'p') + /** * Callouts @@ -183,18 +192,18 @@ } .card { - margin: $-m; background-color: #FFF; - box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 6px -1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + padding-bottom: $-xs; h3 { padding: $-m; - border-bottom: 1px solid #E8E8E8; + padding-bottom: $-xs; margin: 0; font-size: $fs-s; - color: #888; - fill: #888; + color: #444; + fill: #666; font-weight: 400; - text-transform: uppercase; } h3 a { line-height: 1; diff --git a/resources/assets/sass/_colors.scss b/resources/assets/sass/_colors.scss new file mode 100644 index 000000000..044a9498b --- /dev/null +++ b/resources/assets/sass/_colors.scss @@ -0,0 +1,100 @@ +/* + * Text colors + */ +p.pos, p .pos, span.pos, .text-pos { + color: $positive; + fill: $positive; + &:hover { + color: $positive; + fill: $positive; + } +} + +p.neg, p .neg, span.neg, .text-neg { + color: $negative; + fill: $negative; + &:hover { + color: $negative; + fill: $negative; + } +} + +p.muted, p .muted, span.muted, .text-muted { + color: lighten($text-dark, 26%); + fill: lighten($text-dark, 26%); + &.small, .small { + color: lighten($text-dark, 32%); + fill: lighten($text-dark, 32%); + } +} + +p.primary, p .primary, span.primary, .text-primary { + color: $primary; + fill: $primary; + &:hover { + color: $primary; + fill: $primary; + } +} + +p.secondary, p .secondary, span.secondary, .text-secondary { + color: $secondary; + fill: $secondary; + &:hover { + color: $secondary; + fill: $secondary; + } +} + +.text-bookshelf { + color: $color-bookshelf; + fill: $color-bookshelf; + &:hover { + color: $color-bookshelf; + fill: $color-bookshelf; + } +} +.text-book { + color: $color-book; + fill: $color-book; + &:hover { + color: $color-book; + fill: $color-book; + } +} +.text-page { + color: $color-page; + fill: $color-page; + &:hover { + color: $color-page; + fill: $color-page; + } + &.draft { + color: $color-page-draft; + fill: $color-page-draft; + } + &.draft:hover { + color: $color-page-draft; + fill: $color-page-draft; + } +} +.text-chapter { + color: $color-chapter; + fill: $color-chapter; + &:hover { + color: $color-chapter; + fill: $color-chapter; + } +} +.faded .text-book:hover { + color: $color-book !important; + fill: $color-book !important; +} +.faded .text-chapter:hover { + color: $color-chapter !important; + fill: $color-chapter !important; +} +.faded .text-page:hover { + color: $color-page !important; + fill: $color-page !important; +} \ No newline at end of file diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index 0e1f85ce6..f4d155dde 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -270,6 +270,8 @@ div[class^="col-"] img { display: inline-block; } + +// TODO - Remove old BS grid system .col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { position: relative; min-height: 1px; @@ -908,18 +910,12 @@ div[class^="col-"] img { } .clearfix:before, .clearfix:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, .row:before, .row:after { content: " "; display: table; } .clearfix:after, -.container:after, -.container-fluid:after, .row:after { clear: both; } @@ -928,3 +924,63 @@ div[class^="col-"] img { margin-left: auto; margin-right: auto; } + + + + + + +.grid { + display: grid; + grid-column-gap: $-m; + grid-row-gap: 0; + &.contained { + max-width: $max-width; + padding-left: $-m; + padding-right: $-m; + margin-left: auto; + margin-right: auto; + } +} + +@each $sizeLetter, $size in $spacing { + .grid.contained.space-#{$sizeLetter} { + padding-left: $size; + padding-right: $size; + grid-column-gap: $size; + } +} + +@mixin grid-layout($name, $times) { + .grid.#{$name} { + grid-template-columns: repeat(#{$times}, 1fr); + } +} + +@include grid-layout('thirds', 3) + +@each $sizeLetter, $size in $screen-sizes { + @include smaller-than($size) { + .grid.break-#{$sizeLetter} { + grid-template-columns: 1fr; + } + } +} + + +/** + * Visibility + */ + +@each $sizeLetter, $size in $screen-sizes { + @include smaller-than($size) { + .hide-under-#{$sizeLetter} { + display: none !important; + } + } + @include larger-than($size) { + .hide-over-#{$sizeLetter} { + display: none !important; + } + } +} \ No newline at end of file diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index b66bab394..d42ec8064 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -2,21 +2,22 @@ * Includes the main navigation header and the faded toolbar. */ +header .grid { + grid-template-columns: auto min-content auto; +} + header { + position: relative; display: block; z-index: 2; top: 0; background-color: $primary-dark; color: #fff; fill: #fff; - .padded { - padding: $-m; - } border-bottom: 1px solid #DDD; .links { display: inline-block; vertical-align: top; - margin-left: $-m; } .links a { display: inline-block; @@ -28,15 +29,6 @@ header { padding-left: $-m; padding-right: 0; } - @include smaller-than($screen-md) { - .links a { - padding-left: $-s; - padding-right: $-s; - } - .dropdown-container { - padding-left: $-s; - } - } .avatar, .user-name { display: inline-block; } @@ -63,27 +55,17 @@ header { padding-top: 4px; font-size: 18px; } - @include smaller-than($screen-md) { + @include between($l, $xl) { padding-left: $-xs; .name { display: none; } } } - @include smaller-than($screen-sm) { - text-align: center; - .float.right { - float: none; - } - .links a { - padding: $-s; - } - .user-name { - padding-top: $-s; - } - } } + + .header-search { display: inline-block; } @@ -115,20 +97,11 @@ header .search-box { :-moz-placeholder { /* Firefox 18- */ color: #DDD; } - @include smaller-than($screen-lg) { - max-width: 250px; - } - @include smaller-than($l) { + @include between($l, $xl) { max-width: 200px; } } -@include smaller-than($s) { - .header-search { - display: block; - } -} - .logo { display: inline-block; &:hover { @@ -151,6 +124,73 @@ header .search-box { height: 43px; } +.mobile-menu-toggle { + color: #FFF; + fill: #FFF; + font-size: 2em; + border: 2px solid rgba(255, 255, 255, 0.8); + border-radius: 4px; + padding: 0 $-xs; + position: absolute; + right: $-m; + top: 8px; + line-height: 1; + cursor: pointer; + user-select: none; + svg { + margin: 0; + } +} + +@include smaller-than($l) { + header .header-links { + display: none; + background-color: #FFF; + z-index: 10; + right: $-m; + border-radius: 4px; + overflow: hidden; + position: absolute; + box-shadow: $bs-hover; + margin-top: -$-xs; + &.show { + display: block; + } + } + header .links a, header .dropdown-container ul li a { + text-align: left; + display: block; + padding: $-s $-m; + color: $text-dark; + fill: $text-dark; + svg { + margin-right: $-s; + } + &:hover { + background-color: #EEE; + color: #444; + fill: #444; + text-decoration: none; + } + } + header .dropdown-container { + display: block; + padding-left: 0; + } + header .links { + display: block; + } + header .dropdown-container ul { + display: block !important; + position: relative; + background-color: transparent; + border: 0; + padding: 0; + margin: 0; + box-shadow: none; + } +} + .breadcrumbs span.sep { color: #aaa; padding: 0 $-xs; diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index 18a7ea9ce..c28d7219f 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -227,20 +227,13 @@ } .activity-list-item { - padding: $-s 0; + padding: $-s $-m; + display: grid; + grid-template-columns: min-content 1fr; + grid-column-gap: $-m; color: #888; fill: #888; - border-bottom: 1px solid #EEE; font-size: 0.9em; - .left { - float: left; - } - .left + .right { - margin-left: 30px + $-s; - } - &:last-of-type { - border-bottom: 0; - } } ul.pagination { @@ -281,9 +274,6 @@ ul.pagination { } .entity-list { - > div { - padding: $-m 0; - } h4 { margin: 0; } @@ -304,13 +294,29 @@ ul.pagination { } } -.card .entity-list-item, .card .activity-list-item { - padding-left: $-m; - padding-right: $-m; +.entity-list-item { + padding: $-s $-m; + display: grid; + grid-template-columns: min-content 1fr; + grid-column-gap: $-m; + align-items: top; + > .content { + padding-top: 2px; + } + .icon { + font-size: 1rem; + } + h4 a { + color: #666; + } +} +a.entity-list-item:hover { + text-decoration: none; + background-color: #F2F2F2; } .entity-list.compact { - font-size: 0.6em; + font-size: 0.6 * $fs-m; h4, a { line-height: 1.2; } @@ -331,6 +337,11 @@ ul.pagination { hr { margin: 0; } + @include smaller-than($m) { + h4 { + font-size: 1.666em; + } + } } .dropdown-container { diff --git a/resources/assets/sass/_mixins.scss b/resources/assets/sass/_mixins.scss index 3d3101ca7..13c81ae9e 100644 --- a/resources/assets/sass/_mixins.scss +++ b/resources/assets/sass/_mixins.scss @@ -5,6 +5,9 @@ @mixin larger-than($size) { @media screen and (min-width: $size) { @content; } } +@mixin between($min, $max) { + @media screen and (min-width: $min) and (max-width: $max) { @content; } +} @mixin clearfix() { &:after { display: block; diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 21fdf90de..2d3a5852d 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -363,4 +363,31 @@ .mce-open { display: none; } +} + +.entity-icon { + font-size: 1.3em; + width: 1.88em; + height: 1.88em; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + border-radius: 1em; + position: relative; + overflow: hidden; + svg { + margin: 0; + bottom: 0; + } + &:after { + content: ''; + position: absolute; + background-color: currentColor; + opacity: 0.2; + left: 0; + top: 0; + width: 100%; + height: 100%; + } } \ No newline at end of file diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss index 0063c4672..fa34d7fc8 100644 --- a/resources/assets/sass/_text.scss +++ b/resources/assets/sass/_text.scss @@ -233,106 +233,6 @@ pre code { display: block; line-height: 1.6; } -/* - * Text colors - */ -p.pos, p .pos, span.pos, .text-pos { - color: $positive; - fill: $positive; - &:hover { - color: $positive; - fill: $positive; - } -} - -p.neg, p .neg, span.neg, .text-neg { - color: $negative; - fill: $negative; - &:hover { - color: $negative; - fill: $negative; - } -} - -p.muted, p .muted, span.muted, .text-muted { - color: lighten($text-dark, 26%); - fill: lighten($text-dark, 26%); - &.small, .small { - color: lighten($text-dark, 32%); - fill: lighten($text-dark, 32%); - } -} - -p.primary, p .primary, span.primary, .text-primary { - color: $primary; - fill: $primary; - &:hover { - color: $primary; - fill: $primary; - } -} - -p.secondary, p .secondary, span.secondary, .text-secondary { - color: $secondary; - fill: $secondary; - &:hover { - color: $secondary; - fill: $secondary; - } -} - -.text-bookshelf { - color: $color-bookshelf; - fill: $color-bookshelf; - &:hover { - color: $color-bookshelf; - fill: $color-bookshelf; - } -} -.text-book { - color: $color-book; - fill: $color-book; - &:hover { - color: $color-book; - fill: $color-book; - } -} -.text-page { - color: $color-page; - fill: $color-page; - &:hover { - color: $color-page; - fill: $color-page; - } - &.draft { - color: $color-page-draft; - fill: $color-page-draft; - } - &.draft:hover { - color: $color-page-draft; - fill: $color-page-draft; - } -} -.text-chapter { - color: $color-chapter; - fill: $color-chapter; - &:hover { - color: $color-chapter; - fill: $color-chapter; - } -} -.faded .text-book:hover { - color: $color-book !important; - fill: $color-book !important; -} -.faded .text-chapter:hover { - color: $color-chapter !important; - fill: $color-chapter !important; -} -.faded .text-page:hover { - color: $color-page !important; - fill: $color-page !important; -} span.highlight { //background-color: rgba($primary, 0.2); diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index 006d1b3f0..e32b2ab2c 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -8,7 +8,7 @@ $max-width: 1400px; $xl: 1100px; $ipad-width: 1028px; // Is actually 1024 but we go over to ensure functionality. $l: 1000px; -$m: 800px; +$m: 880px; $s: 600px; $xs: 400px; $xxs: 360px; @@ -16,6 +16,8 @@ $screen-lg: 1200px; $screen-md: 992px; $screen-sm: 768px; +$screen-sizes: (('xxs', $xxs), ('xs', $xs), ('s', $s), ('m', $m), ('l', $l), ('xl', $xl)); + // Spacing (Margins+Padding) $-xxxl: 64px; $-xxl: 48px; @@ -26,6 +28,8 @@ $-s: 12px; $-xs: 6px; $-xxs: 3px; +$spacing: (('xxs', $-xxs), ('xs', $-xs), ('s', $-s), ('m', $-m), ('l', $-l), ('xl', $-xl), ('xxl', $-xxl)); + // Fonts $text: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index 49ef77f39..2c657891a 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -3,6 +3,7 @@ @import "mixins"; @import "html"; @import "text"; +@import "colors"; @import "grid"; @import "blocks"; @import "buttons"; diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index e6d0b7761..bc139e17f 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -17,57 +17,14 @@ @yield('head') - - @include('partials/custom-styles') - + @include('partials.custom-styles') @include('partials.custom-head') + - + - @include('partials/notifications') - - + @include('partials.notifications') + @include('common.header')
@yield('content') @@ -78,8 +35,10 @@ @icon('chevron-up') {{ trans('common.back_to_top') }} -@yield('bottom') - -@yield('scripts') + + @yield('bottom') + + @yield('scripts') + diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php new file mode 100644 index 000000000..7f309e21f --- /dev/null +++ b/resources/views/common/header.blade.php @@ -0,0 +1,58 @@ + \ No newline at end of file diff --git a/resources/views/common/home.blade.php b/resources/views/common/home.blade.php index cc20fc68e..72db5f6d2 100644 --- a/resources/views/common/home.blade.php +++ b/resources/views/common/home.blade.php @@ -1,57 +1,53 @@ @extends('simple-layout') -@section('toolbar') - -@stop @section('body') -
-
+ -
- @if(count($draftPages) > 0) -
-

@icon('edit') {{ trans('entities.my_recent_drafts') }}

- @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact']) -
- @endif +
+
+ @if(count($draftPages) > 0) +
+

{{ trans('entities.my_recent_drafts') }}

+ @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact']) +
+ @endif -
-

@icon($signedIn ? 'view' : 'star-circle') {{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}

+
+

{{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}

+ @include('partials/entity-list', [ + 'entities' => $recents, + 'style' => 'compact', + 'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty') + ]) +
+
+ +
+
+

{{ trans('entities.recently_updated_pages') }}

+
@include('partials/entity-list', [ - 'entities' => $recents, - 'style' => 'compact', - 'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty') - ]) + 'entities' => $recentlyUpdatedPages, + 'style' => 'compact', + 'emptyText' => trans('entities.no_pages_recently_updated') + ])
+
-
-
-

@icon('file') {{ trans('entities.recently_updated_pages') }}

-
- @include('partials/entity-list', [ - 'entities' => $recentlyUpdatedPages, - 'style' => 'compact', - 'emptyText' => trans('entities.no_pages_recently_updated') - ]) -
-
-
- -
-
-

@icon('time') {{ trans('entities.recent_activity') }}

+
+
+
+

{{ trans('entities.recent_activity') }}

@include('partials/activity-list', ['activity' => $activity])
-
+
diff --git a/resources/views/pages/list-item.blade.php b/resources/views/pages/list-item.blade.php index b13bb0f12..f3c79791a 100644 --- a/resources/views/pages/list-item.blade.php +++ b/resources/views/pages/list-item.blade.php @@ -1,44 +1,51 @@
-

- @if (isset($showPath) && $showPath) - - @icon('book'){{ $page->book->getShortName() }} - -   »   - @if($page->chapter) - - @icon('chapter'){{ $page->chapter->getShortName() }} +
@icon('page')
+

-
- @if(isset($page->searchSnippet)) -

{!! $page->searchSnippet !!}

- @else -

{{ $page->getExcerpt() }}

+ +
+ @if(isset($page->searchSnippet)) +

{!! $page->searchSnippet !!}

+ @else +

{{ $page->getExcerpt() }}

+ @endif +
+ + @if(isset($style) && $style === 'detailed') +
+
+ @include('partials.entity-meta', ['entity' => $page]) +
+
+ @icon('book'){{ $page->book->getShortName(30) }} +
+ @if($page->chapter) + @icon('chapter'){{ $page->chapter->getShortName(30) }} + @else + @icon('chapter') {{ trans('entities.pages_not_in_chapter') }} + @endif +
+
@endif +
- @if(isset($style) && $style === 'detailed') -
-
- @include('partials.entity-meta', ['entity' => $page]) -
-
- @icon('book'){{ $page->book->getShortName(30) }} -
- @if($page->chapter) - @icon('chapter'){{ $page->chapter->getShortName(30) }} - @else - @icon('chapter') {{ trans('entities.pages_not_in_chapter') }} - @endif -
-
- @endif
\ No newline at end of file diff --git a/resources/views/partials/_header-dropdown.blade.php b/resources/views/partials/_header-dropdown.blade.php deleted file mode 100644 index 176e00739..000000000 --- a/resources/views/partials/_header-dropdown.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/partials/activity-item.blade.php b/resources/views/partials/activity-item.blade.php index 1dbfc9de8..39fb35fe2 100644 --- a/resources/views/partials/activity-item.blade.php +++ b/resources/views/partials/activity-item.blade.php @@ -1,13 +1,13 @@ {{--Requires an Activity item with the name $activity passed in--}} -@if($activity->user) -
- {{ $activity->user->name }} -
-@endif +
+ @if($activity->user) + {{ $activity->user->name }} + @endif +
-
+
@if($activity->user) {{ $activity->user->name }} @else diff --git a/resources/views/partials/entity-list-item.blade.php b/resources/views/partials/entity-list-item.blade.php new file mode 100644 index 000000000..32d22853f --- /dev/null +++ b/resources/views/partials/entity-list-item.blade.php @@ -0,0 +1,13 @@ +getType(); ?> + +
@icon($type)
+
+ +

{{ $entity->name }}

+ +
+

{{ $entity->getExcerpt() }}

+
+ +
+
\ No newline at end of file diff --git a/resources/views/partials/entity-list.blade.php b/resources/views/partials/entity-list.blade.php index 371f38d71..b2a26f1e4 100644 --- a/resources/views/partials/entity-list.blade.php +++ b/resources/views/partials/entity-list.blade.php @@ -1,25 +1,12 @@ -
+
@if(count($entities) > 0) @foreach($entities as $index => $entity) - @if($entity->isA('page')) - @include('pages/list-item', ['page' => $entity]) - @elseif($entity->isA('book')) - @include('books/list-item', ['book' => $entity]) - @elseif($entity->isA('chapter')) - @include('chapters/list-item', ['chapter' => $entity, 'hidePages' => true]) - @elseif($entity->isA('bookshelf')) - @include('shelves/list-item', ['bookshelf' => $entity]) - @endif - - @if($index !== count($entities) - 1) -
- @endif - + @include('partials.entity-list-item', ['entity' => $entity]) @endforeach @else

- {{ $emptyText or trans('common.no_items') }} + {{ $emptyText ?? trans('common.no_items') }}

@endif
\ No newline at end of file diff --git a/resources/views/simple-layout.blade.php b/resources/views/simple-layout.blade.php index eeb4129e0..b87cd37db 100644 --- a/resources/views/simple-layout.blade.php +++ b/resources/views/simple-layout.blade.php @@ -5,13 +5,7 @@ @section('content')
-
-
-
- @yield('toolbar') -
-
-
+ @yield('toolbar')
From 0e395b1e21171ca702bc5b2a4ae32617511ffdc4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 21 Oct 2018 20:05:11 +0100 Subject: [PATCH 002/160] Started reworking of page-show design - Updated core toolbar & breadcrumb design --- resources/assets/icons/chevron-right.svg | 1 + .../assets/js/components/page-display.js | 2 +- resources/assets/sass/_blocks.scss | 29 ++-------- resources/assets/sass/_grid.scss | 5 +- resources/assets/sass/_header.scss | 57 ++++++++++++------- resources/assets/sass/_html.scss | 9 +-- resources/assets/sass/_pages.scss | 24 +++++++- resources/assets/sass/_variables.scss | 4 +- resources/views/pages/_breadcrumbs.blade.php | 10 ++-- resources/views/pages/show.blade.php | 16 +++--- resources/views/partials/book-tree.blade.php | 2 +- resources/views/public.blade.php | 2 +- resources/views/sidebar-layout.blade.php | 14 ++--- 13 files changed, 93 insertions(+), 82 deletions(-) create mode 100644 resources/assets/icons/chevron-right.svg diff --git a/resources/assets/icons/chevron-right.svg b/resources/assets/icons/chevron-right.svg new file mode 100644 index 000000000..96540b9ea --- /dev/null +++ b/resources/assets/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/assets/js/components/page-display.js b/resources/assets/js/components/page-display.js index 3fd8fb8ef..cffaf373d 100644 --- a/resources/assets/js/components/page-display.js +++ b/resources/assets/js/components/page-display.js @@ -136,7 +136,7 @@ class PageDisplay { // Fix the tree as a sidebar function stickTree() { - $sidebar.width($bookTreeParent.width() + 15); + $sidebar.width($bookTreeParent.width() - 32); $sidebar.addClass("fixed"); isFixed = true; } diff --git a/resources/assets/sass/_blocks.scss b/resources/assets/sass/_blocks.scss index a19ca0278..1e4641338 100644 --- a/resources/assets/sass/_blocks.scss +++ b/resources/assets/sass/_blocks.scss @@ -1,29 +1,9 @@ /* -* This file container all block styling including background shading, -* margins, paddings & borders. +* This file container all block styling including margins, paddings & borders. */ -/* -* Background Shading -*/ -.shaded { - background-color: #f1f1f1; - &.pos { - background-color: lighten($positive, 40%); - } - &.neg { - background-color: lighten($negative, 20%); - } - &.primary { - background-color: lighten($primary, 40%); - } - &.secondary { - background-color: lighten($secondary, 30%); - } -} - /* * Padding * TODO - Remove these older styles @@ -193,7 +173,7 @@ .card { background-color: #FFF; - box-shadow: 0 1px 6px -1px rgba(0, 0, 0, 0.1); + box-shadow: $bs-card; border-radius: 3px; padding-bottom: $-xs; h3 { @@ -218,9 +198,12 @@ } .sidebar .card { - h3, .body, .empty-text { + .body, .empty-text { padding: $-s $-m; } + h3 + .body { + padding-top: $-xs; + } } .card.drag-card { diff --git a/resources/assets/sass/_grid.scss b/resources/assets/sass/_grid.scss index f4d155dde..aeb31460b 100644 --- a/resources/assets/sass/_grid.scss +++ b/resources/assets/sass/_grid.scss @@ -63,7 +63,8 @@ body.flexbox { flex: 3; background-color: #FFFFFF; padding: 0 $-l; - border-left: 1px solid #DDD; + box-shadow: $bs-card; + border-radius: 4px; max-width: 100%; } .flex.sidebar .sidebar-toggle { @@ -135,6 +136,7 @@ body.flexbox { position: fixed; top: 0; padding-right: $-m; + padding-top: $-m; width: 30%; left: 0; height: 100%; @@ -958,6 +960,7 @@ div[class^="col-"] img { } @include grid-layout('thirds', 3) +@include grid-layout('halves', 2) @each $sizeLetter, $size in $screen-sizes { @include smaller-than($size) { diff --git a/resources/assets/sass/_header.scss b/resources/assets/sass/_header.scss index d42ec8064..3f4841a7f 100644 --- a/resources/assets/sass/_header.scss +++ b/resources/assets/sass/_header.scss @@ -15,6 +15,8 @@ header { color: #fff; fill: #fff; border-bottom: 1px solid #DDD; + box-shadow: $bs-card; + padding: $-xxs 0; .links { display: inline-block; vertical-align: top; @@ -74,13 +76,16 @@ header .search-box { margin-top: 10px; input { background-color: rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 40px; color: #EEE; z-index: 2; + padding-left: 40px; } button { fill: #EEE; z-index: 1; + left: 16px; svg { margin-right: 0; } @@ -191,10 +196,20 @@ header .search-box { } } -.breadcrumbs span.sep { - color: #aaa; - padding: 0 $-xs; +.breadcrumbs { + display: flex; + flex-direction: row; + align-items: center; } + +.breadcrumbs .separator { + fill: #aaa; + font-size: 1.6em; + line-height: 0.8; + margin: 0 $-xs; + margin-top: -2px; +} + .faded { a, button, span, span > div { color: #666; @@ -222,13 +237,24 @@ header .search-box { background-color: $primary-faded; } -.toolbar-container { - background-color: #FFF; +.toolbar { + position: relative; + > .grid > div { + opacity: 0.8; + transition: opacity ease-in-out 120ms; + &:hover { + opacity: 1; + } + } + .text-button { + color: #666; + fill: #666; + } } -.breadcrumbs .text-button, .action-buttons .text-button { +.action-buttons .text-button { display: inline-block; - padding: $-s; + padding: $-xs $-s; &:last-child { padding-right: 0; } @@ -257,25 +283,12 @@ header .search-box { } @include smaller-than($m) { - .breadcrumbs .text-button, .action-buttons .text-button { + .action-buttons .text-button { padding: $-xs $-xs; } .action-buttons .dropdown-container:last-child a { padding-left: $-xs; } - .breadcrumbs .text-button { - font-size: 0; - } - .breadcrumbs .text-button svg { - font-size: $fs-m; - } - .breadcrumbs a i { - font-size: $fs-m; - padding-right: 0; - } - .breadcrumbs span.sep { - padding: 0 $-xxs; - } .toolbar .col-xs-1:first-child { padding-right: 0; } diff --git a/resources/assets/sass/_html.scss b/resources/assets/sass/_html.scss index 65f05a71d..32756a600 100644 --- a/resources/assets/sass/_html.scss +++ b/resources/assets/sass/_html.scss @@ -3,15 +3,12 @@ } html { - background-color: #FFFFFF; height: 100%; overflow-y: scroll; + background-color: #F2F2F2; &.flexbox { overflow-y: hidden; } - &.shaded { - background-color: #F2F2F2; - } } body { @@ -19,9 +16,7 @@ body { line-height: 1.6; color: #616161; -webkit-font-smoothing: antialiased; - &.shaded { - background-color: #F2F2F2; - } + background-color: #F2F2F2; } button { diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 2d3a5852d..bb604e188 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -41,7 +41,7 @@ margin-top: $-xxl; overflow-wrap: break-word; &.flex { - margin-top: $-m; + margin-top: $-xl; } .align-left { text-align: left; @@ -390,4 +390,26 @@ width: 100%; height: 100%; } +} + +.entity-chip { + display: inline-block; + align-items: center; + justify-content: center; + text-align: center; + font-size: 0.9em; + border-radius: 2em; + position: relative; + overflow: hidden; + padding: $-xs $-m; + &:after { + content: ''; + position: absolute; + background-color: currentColor; + opacity: 0.2; + left: 0; + top: 0; + width: 100%; + height: 100%; + } } \ No newline at end of file diff --git a/resources/assets/sass/_variables.scss b/resources/assets/sass/_variables.scss index e32b2ab2c..ea0fcba5b 100644 --- a/resources/assets/sass/_variables.scss +++ b/resources/assets/sass/_variables.scss @@ -53,7 +53,7 @@ $primary-faded: rgba(21, 101, 192, 0.15); // Item Colors $color-bookshelf: #af5a5a; $color-book: #009688; -$color-chapter: #ef7c3c; +$color-chapter: #e56236; $color-page: $primary; $color-page-draft: #9A60DA; @@ -64,5 +64,5 @@ $text-light: #EEE; // Shadows $bs-light: 0 0 4px 1px #CCC; $bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26); -$bs-card: 0 1px 3px 1px rgba(76, 76, 76, 0.26), 0 1px 12px 0px rgba(76, 76, 76, 0.2); +$bs-card: 0 1px 6px -1px rgba(0, 0, 0, 0.1); $bs-hover: 0 2px 2px 1px rgba(0,0,0,.13); \ No newline at end of file diff --git a/resources/views/pages/_breadcrumbs.blade.php b/resources/views/pages/_breadcrumbs.blade.php index 19bab40e0..8bbda0411 100644 --- a/resources/views/pages/_breadcrumbs.blade.php +++ b/resources/views/pages/_breadcrumbs.blade.php @@ -1,14 +1,14 @@ \ No newline at end of file diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index 0b6aa7d14..06c3529d5 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -1,10 +1,10 @@ @extends('sidebar-layout') @section('toolbar') -
- @include('pages._breadcrumbs', ['page' => $page]) -
-
+
+
+ @include('pages._breadcrumbs', ['page' => $page]) +
@icon('export'){{ trans('entities.export') }}
@@ -49,7 +49,7 @@ @endif @if ($page->attachments->count() > 0) -
+

@icon('attach') {{ trans('entities.pages_attachments') }}

@foreach($page->attachments as $attachment) @@ -62,7 +62,7 @@ @endif @if (isset($pageNav) && count($pageNav)) -
+ '},reset:function(){this.value(this._initValue).repaint()},postRender:function(){var e,t,n,i,r,o,s,a,l,u,c,d,f,h,m=this;e=m._minValue,t=m._maxValue,"v"===m.settings.orientation?(n="screenY",i="top",r="height",o="h"):(n="screenX",i="left",r="width",o="w"),m._super(),function(o,s){function t(e){var t,n,i,r;t=fr(t=(((t=m.value())+(r=n=o))/((i=s)-r)+.05*e)*(i-n)-n,o,s),m.value(t),m.fire("dragstart",{value:t}),m.fire("drag",{value:t}),m.fire("dragend",{value:t})}m.on("keydown",function(e){switch(e.keyCode){case 37:case 38:t(-1);break;case 39:case 40:t(1)}})}(e,t),s=e,a=t,l=m.getEl("handle"),m._dragHelper=new ft(m._id,{handle:m._id+"-handle",start:function(e){u=e[n],c=parseInt(m.getEl("handle").style[i],10),d=(m.layoutRect()[o]||100)-Re.getSize(l)[r],m.fire("dragstart",{value:h})},drag:function(e){var t=e[n]-u;f=fr(c+t,0,d),l.style[i]=f+"px",h=s+f/d*(a-s),m.value(h),m.tooltip().text(""+m.settings.previewFilter(h)).show().moveRel(l,"bc tc"),m.fire("drag",{value:h})},stop:function(){m.tooltip().hide(),m.fire("dragend",{value:h})}})},repaint:function(){this._super(),mr(this,this.value())},bindStates:function(){var t=this;return t.state.on("change:value",function(e){mr(t,e.value)}),t._super()}}),pr=Wt.extend({renderHtml:function(){return this.classes.add("spacer"),this.canFocus=!1,'
'}}),vr=rr.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var e,t,n=this.getEl(),i=this.layoutRect();return this._super(),e=n.firstChild,t=n.lastChild,we(e).css({width:i.w-Re.getSize(t).width,height:i.h-2}),we(t).css({height:i.h-2}),this},activeMenu:function(e){we(this.getEl().lastChild).toggleClass(this.classPrefix+"active",e)},renderHtml:function(){var e,t,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a=n.settings,l="";return(e=a.image)?(o="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",o=a.icon?r+"ico "+r+"i-"+o:"",s&&(n.classes.add("btn-has-text"),l=''+n.encode(s)+""),t="boolean"==typeof a.active?' aria-pressed="'+a.active+'"':"",'
'},postRender:function(){var n=this.settings.onclick;return this.on("click",function(e){var t=e.target;if(e.control===this)for(;t;){if(e.aria&&"down"!==e.aria.key||"BUTTON"===t.nodeName&&-1===t.className.indexOf("open"))return e.stopImmediatePropagation(),void(n&&n.call(this,e));t=t.parentNode}}),delete this.settings.onclick,this._super()}}),br=_i.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}}),yr=bt.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(n){var e;this.activeTabId&&(e=this.getEl(this.activeTabId),we(e).removeClass(this.classPrefix+"active"),e.setAttribute("aria-selected","false")),this.activeTabId="t"+n,(e=this.getEl("t"+n)).setAttribute("aria-selected","true"),we(e).addClass(this.classPrefix+"active"),this.items()[n].show().fire("showtab"),this.reflow(),this.items().each(function(e,t){n!==t&&e.hide()})},renderHtml:function(){var i=this,e=i._layout,r="",o=i.classPrefix;return i.preRender(),e.preRender(i),i.items().each(function(e,t){var n=i._id+"-t"+t;e.aria("role","tabpanel"),e.aria("labelledby",n),r+='"}),'
'+r+'
'+e.renderHtml(i)+"
"},postRender:function(){var i=this;i._super(),i.settings.activeTab=i.settings.activeTab||0,i.activateTab(i.settings.activeTab),this.on("click",function(e){var t=e.target.parentNode;if(t&&t.id===i._id+"-head")for(var n=t.childNodes.length;n--;)t.childNodes[n]===e.target&&i.activateTab(n)})},initLayoutRect:function(){var e,t,n,i=this;t=(t=Re.getSize(i.getEl("head")).width)<0?0:t,n=0,i.items().each(function(e){t=Math.max(t,e.layoutRect().minW),n=Math.max(n,e.layoutRect().minH)}),i.items().each(function(e){e.settings.x=0,e.settings.y=0,e.settings.w=t,e.settings.h=n,e.layoutRect({x:0,y:0,w:t,h:n})});var r=Re.getSize(i.getEl("head")).height;return i.settings.minWidth=t,i.settings.minHeight=n+r,(e=i._super()).deltaH+=r,e.innerH=e.h-e.deltaH,e}}),xr=Wt.extend({init:function(e){var n=this;n._super(e),n.classes.add("textbox"),e.multiline?n.classes.add("multiline"):(n.on("keydown",function(e){var t;13===e.keyCode&&(e.preventDefault(),n.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),n.fire("submit",{data:t.toJSON()}))}),n.on("keyup",function(e){n.state.set("value",e.target.value)}))},repaint:function(){var e,t,n,i,r,o=this,s=0;e=o.getEl().style,t=o._layoutRect,r=o._lastRepaintRect||{};var a=_.document;return!o.settings.multiline&&a.all&&(!a.documentMode||a.documentMode<=8)&&(e.lineHeight=t.h-s+"px"),i=(n=o.borderBox).left+n.right+8,s=n.top+n.bottom+(o.settings.multiline?8:0),t.x!==r.x&&(e.left=t.x+"px",r.x=t.x),t.y!==r.y&&(e.top=t.y+"px",r.y=t.y),t.w!==r.w&&(e.width=t.w-i+"px",r.w=t.w),t.h!==r.h&&(e.height=t.h-s+"px",r.h=t.h),o._lastRepaintRect=r,o.fire("repaint",{},!1),o},renderHtml:function(){var t,e,n=this,i=n.settings;return t={id:n._id,hidefocus:"1"},w.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(e){t[e]=i[e]}),n.disabled()&&(t.disabled="disabled"),i.subtype&&(t.type=i.subtype),(e=Re.create(i.multiline?"textarea":"input",t)).value=n.state.get("value"),e.className=n.classes.toString(),e.outerHTML},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var t=this;t.getEl().value=t.state.get("value"),t._super(),t.$el.on("change",function(e){t.state.set("value",e.target.value),t.fire("change",e)})},bindStates:function(){var t=this;return t.state.on("change:value",function(e){t.getEl().value!==e.value&&(t.getEl().value=e.value)}),t.state.on("change:disabled",function(e){t.getEl().disabled=e.value}),t._super()},remove:function(){this.$el.off(),this._super()}}),wr=function(){return{Selector:Ue,Collection:$e,ReflowQueue:Qe,Control:st,Factory:b,KeyboardNavigation:lt,Container:ct,DragHelper:ft,Scrollable:vt,Panel:bt,Movable:Te,Resizable:yt,FloatPanel:kt,Window:Ut,MessageBox:qt,Tooltip:Pt,Widget:Wt,Progress:Dt,Notification:At,Layout:jt,AbsoluteLayout:Jt,Button:Gt,ButtonGroup:Zt,Checkbox:Qt,ComboBox:tn,ColorBox:nn,PanelButton:rn,ColorButton:sn,ColorPicker:ln,Path:cn,ElementPath:dn,FormItem:fn,Form:hn,FieldSet:mn,FilePicker:yi,FitLayout:xi,FlexLayout:wi,FlowLayout:_i,FormatControls:Ki,GridLayout:Zi,Iframe:Qi,InfoBox:er,Label:tr,Toolbar:nr,MenuBar:ir,MenuButton:rr,MenuItem:ar,Throbber:Tt,Menu:or,ListBox:sr,Radio:lr,ResizeHandle:ur,SelectBox:dr,Slider:gr,Spacer:pr,SplitButton:vr,StackLayout:br,TabPanel:yr,TextBox:xr,DropZone:un,BrowseButton:Kt}},_r=function(n){n.ui?w.each(wr(),function(e,t){n.ui[t]=e}):n.ui=wr()};w.each(wr(),function(e,t){b.add(t,e)}),_r(window.tinymce?window.tinymce:{}),o.add("modern",function(e){return Ki.setup(e),Xt(e)})}(window); \ No newline at end of file diff --git a/public/libs/tinymce/tinymce.min.js b/public/libs/tinymce/tinymce.min.js index a935eeddd..f963a66a9 100644 --- a/public/libs/tinymce/tinymce.min.js +++ b/public/libs/tinymce/tinymce.min.js @@ -1,2 +1,2 @@ -// 4.9.2 (2018-12-17) -!function(){"use strict";var o=function(){for(var e=[],t=0;t+~]|"+ut+")"+ut+"*"),gt=new RegExp("="+ut+"*([^\\]'\"]*?)"+ut+"*\\]","g"),pt=new RegExp(lt),ht=new RegExp("^"+st+"$"),vt={ID:new RegExp("^#("+st+")"),CLASS:new RegExp("^\\.("+st+")"),TAG:new RegExp("^("+st+"|[*])"),ATTR:new RegExp("^"+ct),PSEUDO:new RegExp("^"+lt),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ut+"*(even|odd|(([+-]|)(\\d*)n|)"+ut+"*(?:([+-]|)"+ut+"*(\\d+)|))"+ut+"*\\)|)","i"),bool:new RegExp("^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$","i"),needsContext:new RegExp("^"+ut+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ut+"*((?:-\\d)?\\d*)"+ut+"*\\)|)(?=[^-]|$)","i")},bt=/^(?:input|select|textarea|button)$/i,yt=/^h\d$/i,Ct=/^[^{]+\{\s*\[native \w/,xt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,wt=/[+~]/,Nt=/'|\\/g,Et=new RegExp("\\\\([\\da-f]{1,6}"+ut+"?|("+ut+")|.)","ig"),St=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{ot.apply(tt=it.call(We.childNodes),We.childNodes),tt[We.childNodes.length].nodeType}catch(LN){ot={apply:tt.length?function(e,t){rt.apply(e,it.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}var kt=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m;if((t?t.ownerDocument||t:We)!==ze&&Fe(t),n=n||[],!e||"string"!=typeof e)return n;if(1!==(u=(t=t||ze).nodeType)&&9!==u)return[];if(Ve&&!r){if(o=xt.exec(e))if(a=o[1]){if(9===u){if(!(i=t.getElementById(a))||!i.parentNode)return n;if(i.id===a)return n.push(i),n}else if(t.ownerDocument&&(i=t.ownerDocument.getElementById(a))&&qe(t,i)&&i.id===a)return n.push(i),n}else{if(o[2])return ot.apply(n,t.getElementsByTagName(e)),n;if((a=o[3])&&Ae.getElementsByClassName)return ot.apply(n,t.getElementsByClassName(a)),n}if(Ae.qsa&&(!He||!He.test(e))){if(f=l=$e,d=t,m=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){for(c=Be(e),(l=t.getAttribute("id"))?f=l.replace(Nt,"\\$&"):t.setAttribute("id",f),f="[id='"+f+"'] ",s=c.length;s--;)c[s]=f+Lt(c[s]);d=wt.test(e)&&Ot(t.parentNode)||t,m=c.join(",")}if(m)try{return ot.apply(n,d.querySelectorAll(m)),n}catch(g){}finally{l||t.removeAttribute("id")}}}return Pe(e.replace(ft,"$1"),t,n,r)};function Tt(){var r=[];return function e(t,n){return r.push(t+" ")>Re.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function At(e){return e[$e]=!0,e}function Rt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function _t(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function Dt(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function Bt(a){return At(function(i){return i=+i,At(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function Ot(e){return e&&typeof e.getElementsByTagName!==Ze&&e}for(Te in Ae=kt.support={},De=kt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},Fe=kt.setDocument=function(e){var t,s=e?e.ownerDocument||e:We,n=s.defaultView;return s!==ze&&9===s.nodeType&&s.documentElement?(Ue=(ze=s).documentElement,Ve=!De(s),n&&n!==function(e){try{return e.top}catch(t){}return null}(n)&&(n.addEventListener?n.addEventListener("unload",function(){Fe()},!1):n.attachEvent&&n.attachEvent("onunload",function(){Fe()})),Ae.attributes=!0,Ae.getElementsByTagName=!0,Ae.getElementsByClassName=Ct.test(s.getElementsByClassName),Ae.getById=!0,Re.find.ID=function(e,t){if(typeof t.getElementById!==Ze&&Ve){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},Re.filter.ID=function(e){var t=e.replace(Et,St);return function(e){return e.getAttribute("id")===t}},Re.find.TAG=Ae.getElementsByTagName?function(e,t){if(typeof t.getElementsByTagName!==Ze)return t.getElementsByTagName(e)}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},Re.find.CLASS=Ae.getElementsByClassName&&function(e,t){if(Ve)return t.getElementsByClassName(e)},je=[],He=[],Ae.disconnectedMatch=!0,He=He.length&&new RegExp(He.join("|")),je=je.length&&new RegExp(je.join("|")),t=Ct.test(Ue.compareDocumentPosition),qe=t||Ct.test(Ue.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},Qe=t?function(e,t){if(e===t)return Me=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!Ae.sortDetached&&t.compareDocumentPosition(e)===n?e===s||e.ownerDocument===We&&qe(We,e)?-1:t===s||t.ownerDocument===We&&qe(We,t)?1:Ie?at.call(Ie,e)-at.call(Ie,t):0:4&n?-1:1)}:function(e,t){if(e===t)return Me=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],u=[t];if(!o||!i)return e===s?-1:t===s?1:o?-1:i?1:Ie?at.call(Ie,e)-at.call(Ie,t):0;if(o===i)return Rt(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?Rt(a[r],u[r]):a[r]===We?-1:u[r]===We?1:0},s):ze},kt.matches=function(e,t){return kt(e,null,null,t)},kt.matchesSelector=function(e,t){if((e.ownerDocument||e)!==ze&&Fe(e),t=t.replace(gt,"='$1']"),Ae.matchesSelector&&Ve&&(!je||!je.test(t))&&(!He||!He.test(t)))try{var n=(void 0).call(e,t);if(n||Ae.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(LN){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Et,St),e[3]=(e[3]||e[4]||e[5]||"").replace(Et,St),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||kt.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&kt.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return vt.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&pt.test(n)&&(t=Be(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Et,St).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=Ye[e+" "];return t||(t=new RegExp("(^|"+ut+")"+e+"("+ut+"|$)"))&&Ye(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==Ze&&e.getAttribute("class")||"")})},ATTR:function(n,r,o){return function(e){var t=kt.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===o:"!="===r?t!==o:"^="===r?o&&0===t.indexOf(o):"*="===r?o&&-1)[^>]*$|#([\w\-]*)$)/,en=ke.Event,tn=Yt.makeMap("children,contents,next,prev"),nn=function(e){return void 0!==e},rn=function(e){return"string"==typeof e},on=function(e,t){var n,r,o;for(o=(t=t||Gt).createElement("div"),n=t.createDocumentFragment(),o.innerHTML=e;r=o.firstChild;)n.appendChild(r);return n},an=function(e,t,n,r){var o;if(rn(t))t=on(t,Cn(e[0]));else if(t.length&&!t.nodeType){if(t=pn.makeArray(t),r)for(o=t.length-1;0<=o;o--)an(e,t[o],n,r);else for(o=0;o"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:Zt.exec(e)))return pn(t).find(e);if(n[1])for(r=on(e,Cn(t)).firstChild;r;)Jt.call(o,r),r=r.nextSibling;else{if(!(r=Cn(t).getElementById(n[2])))return o;if(r.id!==n[2])return o.find(e);o.length=1,o[0]=r}}else this.add(e,!1);return o},toArray:function(){return Yt.toArray(this)},add:function(e,t){var n,r,o=this;if(rn(e))return o.add(pn(e));if(!1!==t)for(n=pn.unique(o.toArray().concat(pn.makeArray(e))),o.length=n.length,r=0;r=a.length&&r(o)}))})})},Zr=function(e){return Qr(e,Jr.nu)},eo=function(n){return{is:function(e){return n===e},isValue:x,isError:C,getOr:j(n),getOrThunk:j(n),getOrDie:j(n),or:function(e){return eo(n)},orThunk:function(e){return eo(n)},fold:function(e,t){return t(n)},map:function(e){return eo(e(n))},mapError:function(e){return eo(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return A.some(n)}}},to=function(n){return{is:C,isValue:C,isError:x,getOr:q,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return to(n)},mapError:function(e){return to(e(n))},each:o,bind:function(e){return to(n)},exists:C,forall:x,toOption:A.none}},no={value:eo,error:to};function ro(e,u){var t=e,n=function(e,t,n,r){var o,i;if(e){if(!r&&e[t])return e[t];if(e!==u){if(o=e[n])return o;for(i=e.parentNode;i&&i!==u;i=i.parentNode)if(o=i[n])return o}}};this.current=function(){return t},this.next=function(e){return t=n(t,"firstChild","nextSibling",e)},this.prev=function(e){return t=n(t,"lastChild","previousSibling",e)},this.prev2=function(e){return t=function(e,t,n,r){var o,i,a;if(e){if(o=e[n],u&&o===u)return;if(o){if(!r)for(a=o[t];a;a=a[t])if(!a[t])return a;return o}if((i=e.parentNode)&&i!==u)return i}}(t,"lastChild","previousSibling",e)}}var oo,io,ao,uo=function(t){var n;return function(e){return(n=n||function(e,t){for(var n={},r=0,o=e.length;r\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Io=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Mo=/[<>&\"\']/g,Fo=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,zo={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};io={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},ao={"<":"<",">":">","&":"&",""":'"',"'":"'"};var Uo=function(e,t){var n,r,o,i={};if(e){for(e=e.split(","),t=t||10,n=0;n>10),56320+(1023&t))):zo[t]||String.fromCharCode(t):ao[e]||oo[e]||(n=e,(r=rr.fromTag("div").dom()).innerHTML=n,r.textContent||r.innerText||n);var n,r})}},$o={},Wo={},Ko=Yt.makeMap,Xo=Yt.each,Yo=Yt.extend,Go=Yt.explode,Jo=Yt.inArray,Qo=function(e,t){return(e=Yt.trim(e))?e.split(t||" "):[]},Zo=function(e){var u,t,n,r,o,i,s={},a=function(e,t,n){var r,o,i,a=function(e,t){var n,r,o={};for(n=0,r=e.length;n
').css(n).appendTo(a)[0];return c.set(A.some({caret:i,element:e,before:t})),c.get().each(function(e){t&&pn(e.caret).addClass("mce-visual-caret-before")}),f(),(r=e.ownerDocument.createRange()).setStart(s,0),r.setEnd(s,0),r},hide:l,getCss:function(){return".mce-visual-caret {position: absolute;background-color: black;background-color: currentcolor;}.mce-visual-caret-hidden {display: none;}*[data-mce-caret] {position: absolute;left: -1000px;right: auto;top: 0;margin: 0;padding: 0;}"},reposition:function(){c.get().each(function(e){var t=Qu(a,e.element,e.before);pn(e.caret).css(t)})},destroy:function(){return ve.clearInterval(t)}}},es=function(){return Gu.isIE()||Gu.isEdge()||Gu.isFirefox()},ts=function(e){return Ju(e)||_o.isTable(e)&&es()},ns=(mu="\xa0",function(e){return mu===e}),rs=function(e){return/^[\r\n\t ]$/.test(e)},os=function(e){return!rs(e)&&!ns(e)},is=_o.isContentEditableFalse,as=_o.matchStyleValues("display","block table table-cell table-caption list-item"),us=pa,ss=ma,cs=_o.isElement,ls=Da,fs=function(e){return 0=o.data.length-1)return 1===e&&(r=s(o))?Cs(r):n;if(Ca(o)&&i<=1)return-1===e&&(r=u(o))?xs(r):n;if(i===o.data.length)return(r=s(o))?Cs(r):n;if(0===i)return(r=u(o))?xs(r):n}return n},Ns=function(e,t){var n=vs(e,t);return is(n)&&!_o.isBogusAll(n)},Es=function(e,t){return _o.isTable(vs(e,t))},Ss=function(e,t){return A.from(vs(e?0:-1,t)).filter(is)},ks=function(e,t,n){var r=ws(e,t,n);return-1===e?du.fromRangeStart(r):du.fromRangeEnd(r)},Ts=d(Ns,0),As=d(Ns,-1),Rs=d(Es,0),_s=d(Es,-1),Ds=function(n,r,o){return A.from(o.container()).filter(_o.isText).exists(function(e){var t=n?0:-1;return r(e.data.charAt(o.offset()+t))})},Bs=d(Ds,!0,rs),Os=d(Ds,!1,rs),Ps=function(e){return A.from(e.getNode()).map(rr.fromDom)};(pu=gu||(gu={}))[pu.Backwards=-1]="Backwards",pu[pu.Forwards=1]="Forwards";var Ls,Is,Ms,Fs,zs,Us=_o.isContentEditableFalse,Vs=_o.isText,Hs=_o.isElement,js=_o.isBr,qs=Da,$s=function(e){return Aa(e)||!!Ba(t=e)&&!0!==U(ne(t.getElementsByTagName("*")),function(e,t){return e||Na(t)},!1);var t},Ws=Oa,Ks=function(e,t){return e.hasChildNodes()&&t'),t},uc=function(e,t){return ic.lastPositionIn(e).fold(function(){return!1},function(e){return t.setStart(e.container(),e.offset()),t.setEnd(e.container(),e.offset()),!0})},sc=function(e,t,n){return!(!1!==t.hasChildNodes()||!zu(e,t)||(o=n,i=(r=t).ownerDocument.createTextNode(ca),r.appendChild(i),o.setStart(i,0),o.setEnd(i,0),0));var r,o,i},cc=function(e,t,n,r){var o,i,a,u,s=n[t?"start":"end"],c=e.getRoot();if(s){for(a=s[0],i=c,o=s.length-1;1<=o;o--){if(u=i.childNodes,sc(c,i,r))return!0;if(s[o]>u.length-1)return!!sc(c,i,r)||uc(i,r);i=u[s[o]]}3===i.nodeType&&(a=Math.min(s[0],i.nodeValue.length)),1===i.nodeType&&(a=Math.min(s[0],i.childNodes.length)),t?r.setStart(i,a):r.setEnd(i,a)}return!0},lc=function(e){return _o.isText(e)&&0=t.nodeValue.length&&e.splice(0,1),t=e[e.length-1],0===m&&0h.length-1?p=h.length-1:p<0&&(p=0),d=h[p]||g),l===d)return o(v([l]));for(n=e.findCommonAncestor(l,d),a=l;a;a=a.parentNode){if(a===d)return C(l,n,!0);if(a===n)break}for(a=d;a;a=a.parentNode){if(a===l)return C(d,n);if(a===n)break}r=y(l,n)||l,i=y(d,n)||d,C(l,r,!0),(s=b(r===l?r:r.nextSibling,"nextSibling",i===d?i.nextSibling:i)).length&&o(v(s)),C(d,i)}},Pc=(Ls=lr,Is="text",Ms=function(e){return Ls(e)?A.from(e.dom().nodeValue):A.none()},Fs=tr.detect().browser,{get:function(e){if(!Ls(e))throw new Error("Can only get "+Is+" value of a "+Is+" node");return zs(e).getOr("")},getOption:zs=Fs.isIE()&&10===Fs.version.major?function(e){try{return Ms(e)}catch(LN){return A.none()}}:Ms,set:function(e,t){if(!Ls(e))throw new Error("Can only set raw "+Is+" value of a "+Is+" node");e.dom().nodeValue=t}}),Lc=function(e){return Pc.get(e)},Ic=function(r,o,i,a){return Ir(o).fold(function(){return"skipping"},function(e){return"br"===a||lr(n=o)&&"\ufeff"===Lc(n)?"valid":cr(t=o)&&zi(t,Yi())?"existing":Fu(o)?"caret":yc.isValid(r,i,a)&&yc.isValid(r,ur(e),i)?"valid":"invalid-child";var t,n})},Mc=function(e,t,n,r){var o,i,a=t.uid,u=void 0===a?(o="mce-annotation",i=(new Date).getTime(),o+"_"+Math.floor(1e9*Math.random())+ ++na+String(i)):a,s=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var o=0;for(r=Object.getOwnPropertySymbols(e);o=n.startOffset&&"\xa0"===n.startContainer.nodeValue[n.startOffset]),t.setStart(r.startContainer,r.startOffset),t.setEnd(r.endContainer,r.endOffset),e.selection.setRng(t)),s.selection.getRng().collapsed){var i=Mc(s.getDoc(),f,c,l.decorate);aa(i,"\xa0"),s.selection.getRng().insertNode(i.dom()),s.selection.select(i.dom())}else{var a=Iu.getPersistentBookmark(s.selection,!1),u=s.selection.getRng();Fc(s,u,c,l.decorate,f),s.selection.moveToBookmark(a)}})};function Uc(s){var n,r=(n={},{register:function(e,t){n[e]={name:e,settings:t}},lookup:function(e){return n.hasOwnProperty(e)?A.from(n[e]).map(function(e){return e.settings}):A.none()}});ta(s,r);var o=ea(s);return{register:function(e,t){r.register(e,t)},annotate:function(t,n){r.lookup(t).each(function(e){zc(s,t,e,n)})},annotationChanged:function(e,t){o.addListener(e,t)},remove:function(e){Qi(s,A.some(e)).each(function(e){var t=e.elements;F(t,_i)})},getAll:function(e){var t,n,r,o,i,a,u=(t=s,n=e,r=rr.fromDom(t.getBody()),o=Vi(r,"["+Gi()+'="'+n+'"]'),i={},F(o,function(e){var t=br(e,Ji()),n=i.hasOwnProperty(t)?i[t]:[];i[t]=n.concat([e])}),i);return a=function(e){return $(e,function(e){return e.dom()})},gr(u,function(e,t,n){return{k:t,v:a(e,t,n)}})}}}var Vc=function(e){return Yt.grep(e.childNodes,function(e){return"LI"===e.nodeName})},Hc=function(e){return e&&e.firstChild&&e.firstChild===e.lastChild&&("\xa0"===(t=e.firstChild).data||_o.isBr(t));var t},jc=function(e){return 0'))},el=function(n){qr(n).each(function(t){Mr(t).each(function(e){co(n)&&fo(t)&&co(e)&&Ri(t)})})},tl=Yt.makeMap;function nl(e){var u,s,c,l,f,d=[];return u=(e=e||{}).indent,s=tl(e.indent_before||""),c=tl(e.indent_after||""),l=qo.getEncodeFunc(e.entity_encoding||"raw",e.entities),f="html"===e.element_format,{start:function(e,t,n){var r,o,i,a;if(u&&s[e]&&0":" />",n&&u&&c[e]&&0"),u&&c[e]&&0")},comment:function(e){d.push("\x3c!--",e,"--\x3e")},pi:function(e,t){t?d.push(""):d.push(""),u&&d.push("\n")},doctype:function(e){d.push("",u?"\n":"")},reset:function(){d.length=0},getContent:function(){return d.join("").replace(/\n$/,"")}}}function rl(t,g){void 0===g&&(g=ti());var p=nl(t);return(t=t||{}).validate=!("validate"in t)||t.validate,{serialize:function(e){var f,d;d=t.validate,f={3:function(e){p.text(e.value,e.raw)},8:function(e){p.comment(e.value)},7:function(e){p.pi(e.name,e.value)},10:function(e){p.doctype(e.value)},4:function(e){p.cdata(e.value)},11:function(e){if(e=e.firstChild)for(;m(e),e=e.next;);}},p.reset();var m=function(e){var t,n,r,o,i,a,u,s,c,l=f[e.type];if(l)l(e);else{if(t=e.name,n=e.shortEnded,r=e.attributes,d&&r&&1|)$/," "):o("nextSibling")||(t=t.replace(/( | )(
|)$/," "))),t}(g.getRng(),t)),r=e.parser,m=n.merge,o=rl({validate:e.settings.validate},e.schema),d='​',s={content:t,format:"html",selection:!0,paste:n.paste},(s=e.fire("BeforeSetContent",s)).isDefaultPrevented())e.fire("SetContent",{content:s.content,format:"html",selection:!0,paste:n.paste});else{-1===(t=s.content).indexOf("{$caret}")&&(t+="{$caret}"),t=t.replace(/\{\$caret\}/,d);var h,v,b,y,C,x,w=(l=g.getRng()).startContainer||(l.parentElement?l.parentElement():null),N=e.getBody();w===N&&g.isCollapsed()&&p.isBlock(N.firstChild)&&(h=e,(v=N.firstChild)&&!h.schema.getShortEndedElements()[v.nodeName])&&p.isEmpty(N.firstChild)&&((l=p.createRng()).setStart(N.firstChild,0),l.setEnd(N.firstChild,0),g.setRng(l)),g.isCollapsed()||(e.selection.setRng(il(e.selection.getRng())),e.getDoc().execCommand("Delete",!1,null),b=e.selection.getRng(),y=t,C=b.startContainer,x=b.startOffset,3===C.nodeType&&b.collapsed&&("\xa0"===C.data[x]?(C.deleteData(x,1),/[\u00a0| ]$/.test(y)||(y+=" ")):"\xa0"===C.data[x-1]&&(C.deleteData(x-1,1),/[\u00a0| ]$/.test(y)||(y=" "+y))),t=y);var E,S,k,T={context:(i=g.getNode()).nodeName.toLowerCase(),data:n.data,insert:!0};if(u=r.parse(t,T),!0===n.paste&&Kc(e.schema,u)&&Yc(p,i))return l=Xc(o,p,e.selection.getRng(),u),e.selection.setRng(l),void e.fire("SetContent",s);if(function(e){for(var t=e;t=t.walk();)1===t.type&&t.attr("data-mce-fragment","1")}(u),"mce_marker"===(f=u.lastChild).attr("id"))for(f=(c=f).prev;f;f=f.walk(!0))if(3===f.type||!p.isBlock(f.name)){e.schema.isValidChild(f.parent.name,"span")&&f.parent.insert(c,f,"br"===f.name);break}if(e._selectionOverrides.showBlockCaretContainer(i),T.invalid){for(ul(e,d),i=g.getNode(),a=e.getBody(),9===i.nodeType?i=f=a:f=i;f!==a;)f=(i=f).parentNode;t=i===a?a.innerHTML:p.getOuterHTML(i),t=o.serialize(r.parse(t.replace(//i,function(){return o.serialize(u)}))),i===a?p.setHTML(a,t):p.setOuterHTML(i,t)}else!function(e,t,n){if("all"===n.getAttribute("data-mce-bogus"))n.parentNode.insertBefore(e.dom.createFragment(t),n);else{var r=n.firstChild,o=n.lastChild;!r||r===o&&"BR"===r.nodeName?e.dom.setHTML(n,t):ul(e,t)}}(e,t=o.serialize(u),i);!function(e,t){var n=e.schema.getTextInlineElements(),r=e.dom;if(t){var o=e.getBody(),i=new Jc(r);Yt.each(r.select("*[data-mce-fragment]"),function(e){for(var t=e.parentNode;t&&t!==o;t=t.parentNode)n[e.nodeName.toLowerCase()]&&i.compare(t,e)&&r.remove(e,!0)})}}(e,m),function(n,e){var t,r,o,i,a,u=n.dom,s=n.selection;if(e){if(n.selection.scrollIntoView(e),t=function(e){for(var t=n.getBody();e&&e!==t;e=e.parentNode)if("false"===n.dom.getContentEditable(e))return e;return null}(e))return u.remove(e),s.select(t);var c=u.createRng();(i=e.previousSibling)&&3===i.nodeType?(c.setStart(i,i.nodeValue.length),de.ie||(a=e.nextSibling)&&3===a.nodeType&&(i.appendData(a.data),a.parentNode.removeChild(a))):(c.setStartBefore(e),c.setEndBefore(e)),r=u.getParent(e,u.isBlock),u.remove(e),r&&u.isEmpty(r)&&(n.$(r).empty(),c.setStart(r,0),c.setEnd(r,0),al(r)||r.getAttribute("data-mce-fragment")||!(o=function(e){var t=hu.fromRangeStart(e);if(t=Gs(n.getBody()).next(t))return t.toRange()}(c))?u.add(r,u.create("br",{"data-mce-bogus":"1"})):(c=o,u.remove(r))),s.setRng(c)}}(e,p.get("mce_marker")),E=e.getBody(),Yt.each(E.getElementsByTagName("*"),function(e){e.removeAttribute("data-mce-fragment")}),S=e.dom,k=e.selection.getStart(),A.from(S.getParent(k,"td,th")).map(rr.fromDom).each(el),e.fire("SetContent",s),e.addVisual()}},cl=function(e,t){var n,r,o="string"!=typeof(n=t)?(r=Yt.extend({paste:n.paste,data:{paste:n.paste}},n),{content:n.content,details:r}):{content:n,details:{}};sl(e,o.content,o.details)},ll=Er("sections","settings"),fl=tr.detect().deviceType.isTouch(),dl=["lists","autolink","autosave"],ml={theme:"mobile"},gl=function(e){var t=D(e)?e.join(" "):e,n=$(R(t)?t.split(" "):[],Kn);return z(n,function(e){return 0=e.data.length,s=0===t;e.replaceData(t,n,(o=s,i=u,U((r=a).split(""),function(e,t){return-1!==" \f\n\r\t\x0B".indexOf(t)||"\xa0"===t?e.previousCharIsSpace||""===e.str&&o||e.str.length===r.length-1&&i?{previousCharIsSpace:!1,str:e.str+"\xa0"}:{previousCharIsSpace:!0,str:e.str+" "}:{previousCharIsSpace:!1,str:e.str+t}},{previousCharIsSpace:!1,str:""}).str))}},pf=function(e,t){var n,r=e.data.slice(t),o=r.length-(n=r,n.replace(/^\s+/g,"")).length;return gf(e,t,o)},hf=function(e,t){return r=e,o=(n=t).container(),i=n.offset(),!1===hu.isTextPosition(n)&&o===r.parentNode&&i>hu.before(r).offset()?hu(t.container(),t.offset()-1):t;var n,r,o,i},vf=function(e){return Da(e.previousSibling)?A.some((t=e.previousSibling,_o.isText(t)?hu(t,t.data.length):hu.after(t))):e.previousSibling?ic.lastPositionIn(e.previousSibling):A.none();var t},bf=function(e){return Da(e.nextSibling)?A.some((t=e.nextSibling,_o.isText(t)?hu(t,0):hu.before(t))):e.nextSibling?ic.firstPositionIn(e.nextSibling):A.none();var t},yf=function(r,o){return vf(o).orThunk(function(){return bf(o)}).orThunk(function(){return e=r,t=o,n=hu.before(t.previousSibling?t.previousSibling:t.parentNode),ic.prevPosition(e,n).fold(function(){return ic.nextPosition(e,hu.after(t))},A.some);var e,t,n})},Cf=function(n,r){return bf(r).orThunk(function(){return vf(r)}).orThunk(function(){return e=n,t=r,ic.nextPosition(e,hu.after(t)).fold(function(){return ic.prevPosition(e,hu.before(t))},A.some);var e,t})},xf=function(e,t,n){return(r=e,o=t,i=n,r?Cf(o,i):yf(o,i)).map(d(hf,n));var r,o,i},wf=function(t,n,e){e.fold(function(){t.focus()},function(e){t.selection.setRng(e.toRange(),n)})},Nf=function(e,t){return t&&e.schema.getBlockElements().hasOwnProperty(ur(t))},Ef=function(e){if(Ol(e)){var t=rr.fromHtml('
');return Ai(e),ki(e,t),A.some(hu.before(t.dom()))}return A.none()},Sf=function(e,t,l){var n=Mr(e).filter(function(e){return _o.isText(e.dom())}),r=Fr(e).filter(function(e){return _o.isText(e.dom())});return Ri(e),qa([n,r,t],function(e,t,n){var r,o,i,a,u=e.dom(),s=t.dom(),c=u.data.length;return o=s,i=l,a=Xn((r=u).data).length,r.appendData(o.data),Ri(rr.fromDom(o)),i&&pf(r,a),n.container()===s?hu(u,c):n}).orThunk(function(){return l&&(n.each(function(e){return t=e.dom(),n=e.dom().length,r=t.data.slice(0,n),o=r.length-Xn(r).length,gf(t,n-o,o);var t,n,r,o}),r.each(function(e){return pf(e.dom(),0)})),t})},kf=function(e,t){return n=e.schema.getTextInlineElements(),r=ur(t),dr.call(n,r);var n,r},Tf=function(t,n,e,r){void 0===r&&(r=!0);var o,i=xf(n,t.getBody(),e.dom()),a=qi(e,d(Nf,t),(o=t.getBody(),function(e){return e.dom()===o})),u=Sf(e,i,kf(t,e));t.dom.isEmpty(t.getBody())?(t.setContent(""),t.selection.setCursorLocation()):a.bind(Ef).fold(function(){r&&wf(t,n,u)},function(e){r&&wf(t,n,A.some(e))})},Af=function(a,u){var e,t,n,r,o,i;return(e=a.getBody(),t=u,n=a.selection.getRng(),r=ws(t?1:-1,e,n),o=hu.fromRangeStart(r),i=rr.fromDom(e),!1===t&&As(o)?A.some(lf.remove(o.getNode(!0))):t&&Ts(o)?A.some(lf.remove(o.getNode())):!1===t&&Ts(o)&&uf(i,o)?sf(i,o).map(function(e){return lf.remove(e.getNode())}):t&&As(o)&&af(i,o)?cf(i,o).map(function(e){return lf.remove(e.getNode())}):mf(e,t,o)).map(function(e){return e.fold((o=a,i=u,function(e){return o._selectionOverrides.hideFakeCaret(),Tf(o,i,rr.fromDom(e)),!0}),(n=a,r=u,function(e){var t=r?hu.before(e):hu.after(e);return n.selection.setRng(t.toRange()),!0}),(t=a,function(e){return t.selection.setRng(e.toRange()),!0}));var t,n,r,o,i}).getOr(!1)},Rf=function(e,t){var n,r=e.selection.getNode();return!!_o.isContentEditableFalse(r)&&(n=rr.fromDom(e.getBody()),F(Vi(n,".mce-offscreen-selection"),Ri),Tf(e,t,rr.fromDom(e.selection.getNode())),kl(e),!0)},_f=function(e,t){return e.selection.isCollapsed()?Af(e,t):Rf(e,t)},Df=function(e){var t,n=function(e,t){for(;t&&t!==e;){if(_o.isContentEditableTrue(t)||_o.isContentEditableFalse(t))return t;t=t.parentNode}return null}(e.getBody(),e.selection.getNode());return _o.isContentEditableTrue(n)&&e.dom.isBlock(n)&&e.dom.isEmpty(n)&&(t=e.dom.create("br",{"data-mce-bogus":"1"}),e.dom.setHTML(n,""),n.appendChild(t),e.selection.setRng(hu.before(t).toRange())),!0},Bf=_o.isText,Of=function(e){return Bf(e)&&e.data[0]===ca},Pf=function(e){return Bf(e)&&e.data[e.data.length-1]===ca},Lf=function(e){return e.ownerDocument.createTextNode(ca)},If=function(e,t){return e?function(e){if(Bf(e.previousSibling))return Pf(e.previousSibling)||e.previousSibling.appendData(ca),e.previousSibling;if(Bf(e))return Of(e)||e.insertData(0,ca),e;var t=Lf(e);return e.parentNode.insertBefore(t,e),t}(t):function(e){if(Bf(e.nextSibling))return Of(e.nextSibling)||e.nextSibling.insertData(0,ca),e.nextSibling;if(Bf(e))return Pf(e)||e.appendData(ca),e;var t=Lf(e);return e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t}(t)},Mf=d(If,!0),Ff=d(If,!1),zf=function(e,t){return _o.isText(e.container())?If(t,e.container()):If(t,e.getNode())},Uf=function(e,t){var n=t.get();return n&&e.container()===n&&ga(n)},Vf=function(n,e){return e.fold(function(e){Yu.remove(n.get());var t=Mf(e);return n.set(t),A.some(hu(t,t.length-1))},function(e){return ic.firstPositionIn(e).map(function(e){if(Uf(e,n))return hu(n.get(),1);Yu.remove(n.get());var t=zf(e,!0);return n.set(t),hu(t,1)})},function(e){return ic.lastPositionIn(e).map(function(e){if(Uf(e,n))return hu(n.get(),n.get().length-1);Yu.remove(n.get());var t=zf(e,!1);return n.set(t),hu(t,t.length-1)})},function(e){Yu.remove(n.get());var t=Ff(e);return n.set(t),A.some(hu(t,1))})},Hf=function(e,t){for(var n=0;n")},sm=function(e){return e.getParam("document_base_url","")},cm=function(e){return im(e,"body_id","tinymce")},lm=function(e){return im(e,"body_class","")},fm=function(e){return e.getParam("content_security_policy","")},dm=function(e){return e.getParam("br_in_pre",!0)},mm=function(e){if(e.getParam("force_p_newlines",!1))return"p";var t=e.getParam("forced_root_block","p");return!1===t?"":t},gm=function(e){return e.getParam("forced_root_block_attrs",{})},pm=function(e){return e.getParam("br_newline_selector",".mce-toc h2,figcaption,caption")},hm=function(e){return e.getParam("no_newline_selector","")},vm=function(e){return e.getParam("keep_styles",!0)},bm=function(e){return e.getParam("end_container_on_empty_block",!1)},ym=function(e){return Yt.explode(e.getParam("font_size_style_values",""))},Cm=function(e){return Yt.explode(e.getParam("font_size_classes",""))},xm=function(e){return e.getParam("images_dataimg_filter",j(!0),"function")},wm=function(e){return e.getParam("automatic_uploads",!0,"boolean")},Nm=function(e){return e.getParam("images_reuse_filename",!1,"boolean")},Em=function(e){return e.getParam("images_replace_blob_uris",!0,"boolean")},Sm=function(e){return e.getParam("images_upload_url","","string")},km=function(e){return e.getParam("images_upload_base_path","","string")},Tm=function(e){return e.getParam("images_upload_credentials",!1,"boolean")},Am=function(e){return e.getParam("images_upload_handler",null,"function")},Rm=function(e){return e.getParam("content_css_cors",!1,"boolean")},_m=function(o,t,e){var n=function(e){return t=o,n=e.dom(),r=Nr(n,t),A.from(r).filter(function(e){return 0o.childNodes.length-1&&(c=!1),_o.isDocument(o)&&(o=g,i=0),o===g){if(c&&(u=o.childNodes[0s.childNodes.length-1;s=s.childNodes[Math.min(u,s.childNodes.length-1)]||s,u=c&&3===s.nodeType?s.nodeValue.length:0}var l=i.getParent(s,i.isBlock),f=l?i.getParent(l.parentNode,i.isBlock):null,d=f?f.nodeName.toUpperCase():"",m=t&&t.ctrlKey;"LI"!==d||m||(l=f),s&&3===s.nodeType&&u>=s.nodeValue.length&&(function(e,t,n){for(var r,o=new ro(t,n),i=e.getNonEmptyElements();r=o.next();)if(i[r.nodeName.toLowerCase()]||0")},mceToggleVisualAid:function(){s.hasVisual=!s.hasVisual,s.addVisual()},mceReplaceContent:function(e,t,n){s.execCommand("mceInsertContent",!1,n.replace(/\{\$selection\}/g,i.getContent({format:"text"})))},mceInsertLink:function(e,t,n){var r;"string"==typeof n&&(n={href:n}),r=o.getParent(i.getNode(),"a"),n.href=n.href.replace(" ","%20"),r&&n.href||a.remove("link"),n.href&&a.apply("link",n,r)},selectAll:function(){var e=o.getParent(i.getStart(),_o.isContentEditableTrue);if(e){var t=o.createRng();t.selectNodeContents(e),i.setRng(t)}},"delete":function(){rm(s)},forwardDelete:function(){om(s)},mceNewDocument:function(){s.setContent("")},InsertLineBreak:function(e,t,n){return Zm(s,n),!0}});var p=function(n){return function(){var e=i.isCollapsed()?[o.getParent(i.getNode(),o.isBlock)]:i.getSelectedBlocks(),t=wg(e,function(e){return!!a.matchNode(e,n)});return-1!==Ng(t,!0)}};e({JustifyLeft:p("alignleft"),JustifyCenter:p("aligncenter"),JustifyRight:p("alignright"),JustifyFull:p("alignjustify"),"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){return f(e)},mceBlockQuote:function(){return f("blockquote")},Outdent:function(){var e;if(n.inline_styles){if((e=o.getParent(i.getStart(),o.isBlock))&&0"),u))[o.length-1]=Yt.extend(o[o.length-1],{func:n,scope:r||i}),Yt.extend(o[0],{desc:i.translate(t),subpatterns:o.slice(1)})},o=function(e,t){return!!t&&t.ctrl===e.ctrlKey&&t.meta===e.metaKey&&t.alt===e.altKey&&t.shift===e.shiftKey&&!!(e.keyCode===t.keyCode||e.charCode&&e.charCode===t.charCode)&&(e.preventDefault(),!0)},c=function(e){return e.func?e.func.call(e.scope):null};i.on("keyup keypress keydown",function(t){var e,n;((n=t).altKey||n.ctrlKey||n.metaKey||"keydown"===(e=t).type&&112<=e.keyCode&&e.keyCode<=123)&&!t.isDefaultPrevented()&&(Wg(a,function(e){if(o(t,e))return r=e.subpatterns.slice(0),"keydown"===t.type&&c(e),!0}),o(t,r[0])&&(1===r.length&&"keydown"===t.type&&c(r[0]),r.shift()))}),this.add=function(e,n,r,o){var t;return"string"==typeof(t=r)?r=function(){i.execCommand(t,!1,null)}:Yt.isArray(t)&&(r=function(){i.execCommand(t[0],t[1],t[2])}),Wg(Kg(Yt.trim(e.toLowerCase())),function(e){var t=s(e,n,r,o);a[t.id]=t}),!0},this.remove=function(e){var t=s(e);return!!a[t.id]&&(delete a[t.id],!0)}}var Jg=function(e){var t=Lr(e).dom();return e.dom()===t.activeElement},Qg=function(t){return(e=Lr(t),n=e!==undefined?e.dom():document,A.from(n.activeElement).map(rr.fromDom)).filter(function(e){return t.dom().contains(e.dom())});var e,n},Zg=function(t,e){return(n=e,n.collapsed?A.from(Va(n.startContainer,n.startOffset)).map(rr.fromDom):A.none()).bind(function(e){return vo(e)?A.some(e):!1===Pr(t,e)?A.some(t):A.none()});var n},ep=function(t,e){Zg(rr.fromDom(t.getBody()),e).bind(function(e){return ic.firstPositionIn(e.dom())}).fold(function(){t.selection.normalize()},function(e){return t.selection.setRng(e.toRange())})},tp=function(e){if(e.setActive)try{e.setActive()}catch(t){e.focus()}else e.focus()},np=function(e){var t,n=e.getBody();return n&&(t=rr.fromDom(n),Jg(t)||Qg(t).isSome())},rp=function(e){return e.inline?np(e):(t=e).iframeElement&&Jg(rr.fromDom(t.iframeElement));var t},op=function(e){return e.editorManager.setActive(e)},ip=function(e,t){e.removed||(t?op(e):function(t){var e=t.selection,n=t.settings.content_editable,r=t.getBody(),o=e.getRng();t.quirks.refreshContentEditable();var i,a,u=(i=t,a=e.getNode(),i.dom.getParent(a,function(e){return"true"===i.dom.getContentEditable(e)}));if(t.$.contains(r,u))return tp(u),ep(t,o),op(t);t.bookmark!==undefined&&!1===rp(t)&&hg(t).each(function(e){t.selection.setRng(e),o=e}),n||(de.opera||tp(r),t.getWin().focus()),(de.gecko||n)&&(tp(r),ep(t,o)),op(t)}(e))},ap=rp,up=function(e,t){return t.dom()[e]},sp=function(e,t){return parseInt(wr(t,e),10)},cp=d(up,"clientWidth"),lp=d(up,"clientHeight"),fp=d(sp,"margin-top"),dp=d(sp,"margin-left"),mp=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=rr.fromDom(e.getBody()),p=e.inline?g:(r=g,rr.fromDom(r.dom().ownerDocument.documentElement)),h=(o=e.inline,a=t,u=n,s=(i=p).dom().getBoundingClientRect(),{x:a-(o?s.left+i.dom().clientLeft+dp(i):0),y:u-(o?s.top+i.dom().clientTop+fp(i):0)});return l=h.x,f=h.y,d=cp(c=p),m=lp(c),0<=l&&0<=f&&l<=d&&f<=m},gp=function(e){var t,n=e.inline?e.getBody():e.getContentAreaContainer();return(t=n,A.from(t).map(rr.fromDom)).map(function(e){return Pr(Lr(e),e)}).getOr(!1)};function pp(n){var t,o=[],i=function(){var e,t=n.theme;return t&&t.getNotificationManagerImpl?t.getNotificationManagerImpl():{open:e=function(){throw new Error("Theme did not provide a NotificationManager implementation.")},close:e,reposition:e,getArgs:e}},a=function(){0i&&(u=n.pageX+r-i),n.pageY+o>a&&(s=n.pageY+o-a),t.style.width=r-u+"px",t.style.height=o-s+"px",v(e.clientX,e.clientY)}},o=uh(c,e),u=c,i=function(){u.dragging&&s.fire("dragend"),sh(u)},(s=e).on("mousedown",n),e.on("mousemove",r),e.on("mouseup",o),t.bind(a,"mousemove",r),t.bind(a,"mouseup",i),e.on("remove",function(){t.unbind(a,"mousemove",r),t.unbind(a,"mouseup",i)})},lh=function(e){var n;ch(e),(n=e).on("drop",function(e){var t="undefined"!=typeof e.clientX?n.getDoc().elementFromPoint(e.clientX,e.clientY):null;(rh(t)||rh(n.dom.getContentEditableParent(t)))&&e.preventDefault()})},fh=function(e){return U(e,function(e,t){return e.concat(function(t){var e=function(e){return $(e,function(e){return(e=La(e)).node=t,e})};if(_o.isElement(t))return e(t.getClientRects());if(_o.isText(t)){var n=t.ownerDocument.createRange();return n.setStart(t,0),n.setEnd(t,t.data.length),e(n.getClientRects())}}(t))},[])};(eh=Zp||(Zp={}))[eh.Up=-1]="Up",eh[eh.Down=1]="Down";var dh=function(o,i,a,e,u,t){var n,s,c=0,l=[],r=function(e){var t,n,r;for(r=fh([e]),-1===o&&(r=r.reverse()),t=0;tt;var t}},hh=function(n){return function(e){return t=n,e.line===t;var t}},vh=_o.isContentEditableFalse,bh=gs,yh=function(e,t){return Math.abs(e.left-t)},Ch=function(e,t){return Math.abs(e.right-t)},xh=function(e,t){return e>=t.left&&e<=t.right},wh=function(e,o){return qt.reduce(e,function(e,t){var n,r;return n=Math.min(yh(e,o),Ch(e,o)),r=Math.min(yh(t,o),Ch(t,o)),xh(o,t)?t:xh(o,e)?e:r===n&&vh(t.node)?t:r=e.top&&n<=e.bottom});return(r=wh(f,t))&&(r=wh((a=e,c=function(t,e){var n;return n=z(fh([e]),function(e){return!t(e,u)}),s=s.concat(n),0===n.length},(s=[]).push(u=r),Nh(Zp.Up,a,d(c,Fa),u.node),Nh(Zp.Down,a,d(c,za),u.node),s),t))&&ts(r.node)?(i=t,{node:(o=r).node,before:yh(o,i)=(n=t).left&&r<=n.right&&o>=n.top&&o<=n.bottom);var n,r,o},!1)},kh=_o.isContentEditableTrue,Th=_o.isContentEditableFalse,Ah=function(e,t,n,r,o){return t._selectionOverrides.showCaret(e,n,r,o)},Rh=function(e,t){var n,r;return e.fire("BeforeObjectSelected",{target:t}).isDefaultPrevented()?null:((r=(n=t).ownerDocument.createRange()).selectNode(n),r)},_h=function(e,t,n){var r=ws(1,e.getBody(),t),o=hu.fromRangeStart(r),i=o.getNode();if(Th(i))return Ah(1,e,i,!o.isAtEnd(),!1);var a=o.getNode(!0);if(Th(a))return Ah(1,e,a,!1,!1);var u=e.dom.getParent(o.getNode(),function(e){return Th(e)||kh(e)});return Th(u)?Ah(1,e,u,!1,n):null},Dh=function(e,t,n){if(!t||!t.collapsed)return t;var r=_h(e,t,n);return r||t},Bh=function(t){var e=Di(function(){if(!t.removed&&t.selection.getRng().collapsed){var e=Dh(t,t.selection.getRng(),!1);t.selection.setRng(e)}},0);t.on("focus",function(){e.throttle()}),t.on("blur",function(){e.cancel()})},Oh={BACKSPACE:8,DELETE:46,DOWN:40,ENTER:13,LEFT:37,RIGHT:39,SPACEBAR:32,TAB:9,UP:38,modifierPressed:function(e){return e.shiftKey||e.ctrlKey||e.altKey||this.metaKeyPressed(e)},metaKeyPressed:function(e){return de.mac?e.metaKey:e.ctrlKey&&!e.altKey}},Ph=_o.isContentEditableTrue,Lh=_o.isContentEditableFalse,Ih=As,Mh=Ts,Fh=function(e,t){for(var n=e.getBody();t&&t!==n;){if(Ph(t)||Lh(t))return t;t=t.parentNode}return null},zh=function(g){var p,e,t,a=g.getBody(),o=Zu(g.getBody(),function(e){return g.dom.isBlock(e)},function(){return ap(g)}),h="sel-"+g.dom.uniqueId(),u=function(e){e&&g.selection.setRng(e)},s=function(){return g.selection.getRng()},v=function(e,t,n,r){return void 0===r&&(r=!0),g.fire("ShowCaret",{target:t,direction:e,before:n}).isDefaultPrevented()?null:(r&&g.selection.scrollIntoView(t,-1===e),o.show(n,t))},b=function(e,t){return t=ws(e,a,t),-1===e?hu.fromRangeStart(t):hu.fromRangeEnd(t)},n=function(e){return pa(e)||Ca(e)||xa(e)},y=function(e){return n(e.startContainer)||n(e.endContainer)},c=function(e,t){var n,r,o,i,a,u,s,c,l,f,d=g.$,m=g.dom;if(!e)return null;if(e.collapsed){if(!y(e))if(!1===t){if(c=b(-1,e),ts(c.getNode(!0)))return v(-1,c.getNode(!0),!1,!1);if(ts(c.getNode()))return v(-1,c.getNode(),!c.isAtEnd(),!1)}else{if(c=b(1,e),ts(c.getNode()))return v(1,c.getNode(),!c.isAtEnd(),!1);if(ts(c.getNode(!0)))return v(1,c.getNode(!0),!1,!1)}return null}return i=e.startContainer,a=e.startOffset,u=e.endOffset,3===i.nodeType&&0===a&&Lh(i.parentNode)&&(i=i.parentNode,a=m.nodeIndex(i),i=i.parentNode),1!==i.nodeType?null:(u===a+1&&(n=i.childNodes[a]),Lh(n)?(l=f=n.cloneNode(!0),(s=g.fire("ObjectSelected",{target:n,targetClone:l})).isDefaultPrevented()?null:(r=Ki(rr.fromDom(g.getBody()),"#"+h).fold(function(){return d([])},function(e){return d([e.dom()])}),l=s.targetClone,0===r.length&&(r=d('
').attr("id",h)).appendTo(g.getBody()),e=g.dom.createRng(),l===f&&de.ie?(r.empty().append('

\xa0

').append(l),e.setStartAfter(r[0].firstChild.firstChild),e.setEndAfter(l)):(r.empty().append("\xa0").append(l).append("\xa0"),e.setStart(r[0].firstChild,1),e.setEnd(r[0].lastChild,0)),r.css({top:m.getPos(n,g.getBody()).y}),r[0].focus(),(o=g.selection.getSel()).removeAllRanges(),o.addRange(e),F(Vi(rr.fromDom(g.getBody()),"*[data-mce-selected]"),function(e){yr(e,"data-mce-selected")}),n.setAttribute("data-mce-selected","1"),p=n,C(),e)):null)},l=function(){p&&(p.removeAttribute("data-mce-selected"),Ki(rr.fromDom(g.getBody()),"#"+h).each(Ri),p=null),Ki(rr.fromDom(g.getBody()),"#"+h).each(Ri),p=null},C=function(){o.hide()};return de.ceFalse&&(function(){g.on("mouseup",function(e){var t=s();t.collapsed&&mp(g,e.clientX,e.clientY)&&u(_h(g,t,!1))}),g.on("click",function(e){var t;(t=Fh(g,e.target))&&(Lh(t)&&(e.preventDefault(),g.focus()),Ph(t)&&g.dom.isChildOf(t,g.selection.getNode())&&l())}),g.on("blur NewBlock",function(){l()}),g.on("ResizeWindow FullscreenStateChanged",function(){return o.reposition()});var n,r,i=function(e,t){var n,r,o=g.dom.getParent(e,g.dom.isBlock),i=g.dom.getParent(t,g.dom.isBlock);return!(!o||!g.dom.isChildOf(o,i)||!1!==Lh(Fh(g,o)))||o&&(n=o,r=i,!(g.dom.getParent(n,g.dom.isBlock)===g.dom.getParent(r,g.dom.isBlock)))&&function(e){var t=Gs(e);if(!e.firstChild)return!1;var n=hu.before(e.firstChild),r=t.next(n);return r&&!Mh(r)&&!Ih(r)}(o)};r=!1,(n=g).on("touchstart",function(){r=!1}),n.on("touchmove",function(){r=!0}),n.on("touchend",function(e){var t=Fh(n,e.target);Lh(t)&&(r||(e.preventDefault(),c(Rh(n,t))))}),g.on("mousedown",function(e){var t,n=e.target;if((n===a||"HTML"===n.nodeName||g.dom.isChildOf(n,a))&&!1!==mp(g,e.clientX,e.clientY))if(t=Fh(g,n))Lh(t)?(e.preventDefault(),c(Rh(g,t))):(l(),Ph(t)&&e.shiftKey||Sh(e.clientX,e.clientY,g.selection.getRng())||(C(),g.selection.placeCaretAt(e.clientX,e.clientY)));else if(!1===ts(n)){l(),C();var r=Eh(a,e.clientX,e.clientY);if(r&&!i(e.target,r.node)){e.preventDefault();var o=v(1,r.node,r.before,!1);g.getBody().focus(),u(o)}}}),g.on("keypress",function(e){Oh.modifierPressed(e)||(e.keyCode,Lh(g.selection.getNode())&&e.preventDefault())}),g.on("getSelectionRange",function(e){var t=e.range;if(p){if(!p.parentNode)return void(p=null);(t=t.cloneRange()).selectNode(p),e.range=t}}),g.on("setSelectionRange",function(e){var t;(t=c(e.range,e.forward))&&(e.range=t)}),g.on("AfterSetSelectionRange",function(e){var t,n=e.range;y(n)||"mcepastebin"===n.startContainer.parentNode.id||C(),t=n.startContainer.parentNode,g.dom.hasClass(t,"mce-offscreen-selection")||l()}),g.on("copy",function(e){var t,n=e.clipboardData;if(!e.isDefaultPrevented()&&e.clipboardData&&!de.ie){var r=(t=g.dom.get(h))?t.getElementsByTagName("*")[0]:t;r&&(e.preventDefault(),n.clearData(),n.setData("text/html",r.outerHTML),n.setData("text/plain",r.outerText))}}),lh(g),Bh(g)}(),e=g.contentStyles,t=".mce-content-body",e.push(o.getCss()),e.push(t+" .mce-offscreen-selection {position: absolute;left: -9999999999px;max-width: 1000000px;}"+t+" *[contentEditable=false] {cursor: default;}"+t+" *[contentEditable=true] {cursor: text;}")),{showCaret:v,showBlockCaretContainer:function(e){e.hasAttribute("data-mce-caret")&&(wa(e),u(s()),g.selection.scrollIntoView(e[0]))},hideFakeCaret:C,destroy:function(){o.destroy(),p=null}}},Uh=function(e,t,n){var r,o,i,a,u=1;for(a=e.getShortEndedElements(),(i=/<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g).lastIndex=r=n;o=i.exec(t);){if(r=i.lastIndex,"/"===o[1])u--;else if(!o[1]){if(o[2]in a)continue;u++}if(0===u)break}return r};function Vh(F,z){void 0===z&&(z=ti());var e=function(){};!1!==(F=F||{}).fix_self_closing&&(F.fix_self_closing=!0);var U=F.comment?F.comment:e,V=F.cdata?F.cdata:e,H=F.text?F.text:e,j=F.start?F.start:e,q=F.end?F.end:e,$=F.pi?F.pi:e,W=F.doctype?F.doctype:e;return{parse:function(e){var t,n,r,d,o,i,a,m,u,s,g,c,p,l,f,h,v,b,y,C,x,w,N,E,S,k,T,A,R,_=0,D=[],B=0,O=qo.decode,P=Yt.makeMap("src,href,data,background,formaction,poster,xlink:href"),L=/((java|vb)script|mhtml):/i,I=function(e){var t,n;for(t=D.length;t--&&D[t].name!==e;);if(0<=t){for(n=D.length-1;t<=n;n--)(e=D[n]).valid&&q(e.name);D.length=t}},M=function(e,t,n,r,o){var i,a,u,s,c;if(n=(t=t.toLowerCase())in g?t:O(n||r||o||""),p&&!m&&0==(0===(u=t).indexOf("data-")||0===u.indexOf("aria-"))){if(!(i=b[t])&&y){for(a=y.length;a--&&!(i=y[a]).pattern.test(t););-1===a&&(i=null)}if(!i)return;if(i.validValues&&!(n in i.validValues))return}if(P[t]&&!F.allow_script_urls){var l=n.replace(/[\s\u0000-\u001F]+/g,"");try{l=decodeURIComponent(l)}catch(f){l=unescape(l)}if(L.test(l))return;if(c=l,!(s=F).allow_html_data_urls&&(/^data:image\//i.test(c)?!1===s.allow_svg_data_urls&&/^data:image\/svg\+xml/i.test(c):/^data:/i.test(c)))return}m&&(t in P||0===t.indexOf("on"))||(d.map[t]=n,d.push({name:t,value:n}))};for(S=new RegExp("<(?:(?:!--([\\w\\W]*?)--\x3e)|(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|(?:!DOCTYPE([\\w\\W]*?)>)|(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^\"'>]+(?:(?:\"[^\"]*\")|(?:'[^']*')|[^>]*))*|\\/|\\s+)>))","g"),k=/([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g,s=z.getShortEndedElements(),E=F.self_closing_elements||z.getSelfClosingElements(),g=z.getBoolAttrs(),p=F.validate,u=F.remove_internals,R=F.fix_self_closing,T=z.getSpecialElements(),N=e+">";t=S.exec(N);){if(_e.length){H(O(e.substr(t.index))),_=t.index+t[0].length;continue}if(":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),c=n in s,R&&E[n]&&0"===n.charAt(0)&&(n=" "+n),F.allow_conditional_comments||"[if"!==n.substr(0,3).toLowerCase()||(n=" "+n),U(n)):(n=t[2])?V(n.replace(//g,"")):(n=t[3])?W(n):(n=t[4])&&$(n,t[5]);_=t.index+t[0].length}for(_]*data-mce-bogus="all"[^>]*>/g,d=e.schema;for(u=e.getTempAttrs(),s=l,c=new RegExp(["\\s?("+u.join("|")+')="[^"]+"'].join("|"),"gi"),l=s.replace(c,""),a=d.getShortEndedElements();i=f.exec(l);)r=f.lastIndex,o=i[0].length,n=a[i[1]]?r:Hh.findEndTag(d,l,r),l=l.substring(0,r-o)+l.substring(n),f.lastIndex=r-o;return la(l)},qh={trimExternal:jh,trimInternal:jh},$h=0,Wh=2,Kh=1,Xh=function(g,p){var e=g.length+p.length+2,h=new Array(e),v=new Array(e),c=function(e,t,n,r,o){var i=l(e,t,n,r);if(null===i||i.start===t&&i.diag===t-r||i.end===e&&i.diag===e-n)for(var a=e,u=n;a")?ev(r):tv(t)},iv=function(e,t,n){"fragmented"===t.type?Qh(t.fragments,e.getBody()):e.setContent(t.content,{format:"raw"}),e.selection.moveToBookmark(n?t.beforeBookmark:t.bookmark)},av=function(e,t){return!(!e||!t)&&(r=t,nv(e)===nv(r)||(n=t,rv(e)===rv(n)));var n,r};function uv(u){var s,r,o=this,c=0,l=[],t=0,f=function(){return 0===t},i=function(e){f()&&(o.typing=e)},d=function(e){u.setDirty(e)},a=function(e){i(!1),o.add({},e)},n=function(){o.typing&&(i(!1),o.add())};return u.on("init",function(){o.add()}),u.on("BeforeExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&(n(),o.beforeChange())}),u.on("ExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&a(e)}),u.on("ObjectResizeStart Cut",function(){o.beforeChange()}),u.on("SaveContent ObjectResized blur",a),u.on("DragEnd",a),u.on("KeyUp",function(e){var t=e.keyCode;e.isDefaultPrevented()||((33<=t&&t<=36||37<=t&&t<=40||45===t||e.ctrlKey)&&(a(),u.nodeChanged()),46!==t&&8!==t||u.nodeChanged(),r&&o.typing&&!1===av(ov(u),l[0])&&(!1===u.isDirty()&&(d(!0),u.fire("change",{level:l[0],lastLevel:null})),u.fire("TypingUndo"),r=!1,u.nodeChanged()))}),u.on("KeyDown",function(e){var t=e.keyCode;if(!e.isDefaultPrevented())if(33<=t&&t<=36||37<=t&&t<=40||45===t)o.typing&&a(e);else{var n=e.ctrlKey&&!e.altKey||e.metaKey;!(t<16||20i.custom_undo_redo_levels){for(n=0;n
").append(n.childNodes)}))},Dv[sv="pre"]||(Dv[sv]=[]),Dv[sv].push(cv);var Pv=function(e,t){Ov(Dv[e],function(e){e(t)})},Lv=/^(src|href|style)$/,Iv=Yt.each,Mv=yc.isEq,Fv=function(e,t,n){return e.isChildOf(t,n)&&t!==n&&!e.isBlock(n)},zv=function(e,t,n){var r,o,i;return r=t[n?"startContainer":"endContainer"],o=t[n?"startOffset":"endOffset"],_o.isElement(r)&&(i=r.childNodes.length-1,!n&&o&&o--,r=r.childNodes[i=r.nodeValue.length&&(r=new ro(r,e.getBody()).next()||r),_o.isText(r)&&!n&&0===o&&(r=new ro(r,e.getBody()).prev()||r),r},Uv=function(e,t,n,r){var o=e.create(n,r);return t.parentNode.insertBefore(o,t),o.appendChild(t),o},Vv=function(e,t,n,r,o){var i=rr.fromDom(t),a=rr.fromDom(e.create(r,o)),u=n?Ur(i):zr(i);return Ti(a,u),n?(Ni(i,a),Si(a,i)):(Ei(i,a),ki(a,i)),a.dom()},Hv=function(e,t,n,r){return!(t=yc.getNonWhiteSpaceSibling(t,n,r))||"BR"===t.nodeName||e.isBlock(t)},jv=function(e,n,r,o,i){var t,a,u,s,c,l,f,d,m,g,p,h,v,b,y=e.dom;if(c=y,!(Mv(l=o,(f=n).inline)||Mv(l,f.block)||(f.selector?_o.isElement(l)&&c.is(l,f.selector):void 0)||(s=o,n.links&&"A"===s.tagName)))return!1;if("all"!==n.remove)for(Iv(n.styles,function(e,t){e=yc.normalizeStyleValue(y,yc.replaceVars(e,r),t),"number"==typeof t&&(t=e,i=0),(n.remove_similar||!i||Mv(yc.getStyle(y,i,t),e))&&y.setStyle(o,t,""),u=1}),u&&""===y.getAttrib(o,"style")&&(o.removeAttribute("style"),o.removeAttribute("data-mce-style")),Iv(n.attributes,function(e,t){var n;if(e=yc.replaceVars(e,r),"number"==typeof t&&(t=e,i=0),!i||Mv(y.getAttrib(i,t),e)){if("class"===t&&(e=y.getAttrib(o,t))&&(n="",Iv(e.split(/\s+/),function(e){/mce\-\w+/.test(e)&&(n+=(n?" ":"")+e)}),n))return void y.setAttrib(o,t,n);"class"===t&&o.removeAttribute("className"),Lv.test(t)&&o.removeAttribute("data-mce-"+t),o.removeAttribute(t)}}),Iv(n.classes,function(e){e=yc.replaceVars(e,r),i&&!y.hasClass(i,e)||y.removeClass(o,e)}),a=y.getAttribs(o),t=0;t)\s*/g,"$1"),Yt.map(e.split(/(?:>|\s+(?![^\[\]]+\]))/),function(e){var t=Yt.map(e.split(/(?:~\+|~|\+)/),hb),n=t.pop();return t.length&&(n.siblings=t),n}).reverse()):[]},bb=function(n,e){var t,r,o,i,a,u,s="";if(!1===(u=n.settings.preview_styles))return"";"string"!=typeof u&&(u="font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow");var c=function(e){return e.replace(/%(\w+)/g,"")};if("string"==typeof e){if(!(e=n.formatter.get(e)))return;e=e[0]}return"preview"in e&&!1===(u=e.preview)?"":(t=e.block||e.inline||"span",(i=vb(e.selector)).length?(i[0].name||(i[0].name=t),t=e.selector,r=pb(i,n)):r=pb([t],n),o=gb.select(t,r)[0]||r.firstChild,mb(e.styles,function(e,t){(e=c(e))&&gb.setStyle(o,t,e)}),mb(e.attributes,function(e,t){(e=c(e))&&gb.setAttrib(o,t,e)}),mb(e.classes,function(e){e=c(e),gb.hasClass(o,e)||gb.addClass(o,e)}),n.fire("PreviewFormats"),gb.setStyles(r,{position:"absolute",left:-65535}),n.getBody().appendChild(r),a=gb.getStyle(n.getBody(),"fontSize",!0),a=/px$/.test(a)?parseInt(a,10):0,mb(u.split(" "),function(e){var t=gb.getStyle(o,e,!0);if(!("background-color"===e&&/transparent|rgba\s*\([^)]+,\s*0\)/.test(t)&&(t=gb.getStyle(n.getBody(),e,!0),"#ffffff"===gb.toHex(t).toLowerCase())||"color"===e&&"#000000"===gb.toHex(t).toLowerCase())){if("font-size"===e&&/em|%$/.test(t)){if(0===a)return;t=parseFloat(t)/(/%$/.test(t)?100:1)*a+"px"}"border"===e&&t&&(s+="padding:0 2px;"),s+=e+":"+t+";"}}),n.fire("AfterPreviewFormats"),gb.remove(r),s)},yb=function(e,t,n,r,o){var i=t.get(n);!hv.match(e,n,r,o)||"toggle"in i[0]&&!i[0].toggle?cb.applyFormat(e,n,r,o):$v(e,n,r,o)},Cb=function(e){e.addShortcut("meta+b","","Bold"),e.addShortcut("meta+i","","Italic"),e.addShortcut("meta+u","","Underline");for(var t=1;t<=6;t++)e.addShortcut("access+"+t,"",["FormatBlock",!1,"h"+t]);e.addShortcut("access+7","",["FormatBlock",!1,"p"]),e.addShortcut("access+8","",["FormatBlock",!1,"div"]),e.addShortcut("access+9","",["FormatBlock",!1,"address"])};function xb(e){var t,n,r,o=(t=e,n={},(r=function(e,t){e&&("string"!=typeof e?Yt.each(e,function(e,t){r(t,e)}):(t=t.length?t:[t],Yt.each(t,function(e){"undefined"==typeof e.deep&&(e.deep=!e.selector),"undefined"==typeof e.split&&(e.split=!e.selector||e.inline),"undefined"==typeof e.remove&&e.selector&&!e.inline&&(e.remove="none"),e.selector&&e.inline&&(e.mixed=!0,e.block_expand=!0),"string"==typeof e.classes&&(e.classes=e.classes.split(/\s+/))}),n[e]=t))})(db.get(t.dom)),r(t.settings.formats),{get:function(e){return e?n[e]:n},register:r,unregister:function(e){return e&&n[e]&&delete n[e],n}}),i=Bi(null);return Cb(e),Rv(e),{get:o.get,register:o.register,unregister:o.unregister,apply:d(cb.applyFormat,e),remove:d($v,e),toggle:d(yb,e,o),match:d(hv.match,e),matchAll:d(hv.matchAll,e),matchNode:d(hv.matchNode,e),canApply:d(hv.canApply,e),formatChanged:d(fb,e,i),getCssText:d(bb,e)}}var wb,Nb=Object.prototype.hasOwnProperty,Eb=(wb=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t)/g,"\n").replace(/^[\r\n]*|[\r\n]*$/g,"").replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g,"")};i--;)r=(n=e[i]).firstChild?n.firstChild.value:"","script"===t?((o=n.attr("type"))&&n.attr("type","mce-no/type"===o?null:o.replace(/^mce\-/,"")),"xhtml"===s.element_format&&0")):"xhtml"===s.element_format&&0T(n)?(C=A(y*b),y=A(C/b)):(y=A(C/b),C=A(y*b))),N.setStyles(D(s),{width:y,height:C}),r=0<(r=f.startPos.x+t)?r:0,o=0<(o=f.startPos.y+n)?o:0,N.setStyles(c,{left:r,top:o,display:"block"}),c.innerHTML=y+" × "+C,f[2]<0&&s.clientWidth<=y&&N.setStyle(s,"left",g+(h-y)),f[3]<0&&s.clientHeight<=C&&N.setStyle(s,"top",p+(v-C)),(t=R.scrollWidth-x)+(n=R.scrollHeight-w)!=0&&N.setStyles(c,{left:r-t,top:o-n}),i||(Pg(a,u,h,v),i=!0)},P=function(){i=!1;var e=function(e,t){t&&(u.style[e]||!a.schema.isValid(u.nodeName.toLowerCase(),e)?N.setStyle(D(u),e,t):N.setAttrib(D(u),e,t))};e("width",y),e("height",C),N.unbind(S,"mousemove",O),N.unbind(S,"mouseup",P),k!==S&&(N.unbind(k,"mousemove",O),N.unbind(k,"mouseup",P)),N.remove(s),N.remove(c),o(u),Lg(a,u,y,C),N.setAttrib(u,"style",N.getAttrib(u,"style")),a.nodeChanged()},o=function(e){var t,r,o,n,i;L(),F(),t=N.getPos(e,R),g=t.x,p=t.y,i=e.getBoundingClientRect(),r=i.width||i.right-i.left,o=i.height||i.bottom-i.top,u!==e&&(u=e,y=C=0),n=a.fire("ObjectSelected",{target:e}),B(e)&&!n.isDefaultPrevented()?E(l,function(n,e){var t;(t=N.get("mceResizeHandle"+e))&&N.remove(t),t=N.add(R,"div",{id:"mceResizeHandle"+e,"data-mce-bogus":"all","class":"mce-resizehandle",unselectable:!0,style:"cursor:"+e+"-resize; margin:0; padding:0"}),11===de.ie&&(t.contentEditable=!1),N.bind(t,"mousedown",function(e){var t;e.stopImmediatePropagation(),e.preventDefault(),d=(t=e).screenX,m=t.screenY,h=D(u).clientWidth,v=D(u).clientHeight,b=v/h,(f=n).startPos={x:r*n[0]+g,y:o*n[1]+p},x=R.scrollWidth,w=R.scrollHeight,s=u.cloneNode(!0),N.addClass(s,"mce-clonedresizable"),N.setAttrib(s,"data-mce-bogus","all"),s.contentEditable=!1,s.unSelectabe=!0,N.setStyles(s,{left:g,top:p,margin:0}),s.removeAttribute("data-mce-selected"),R.appendChild(s),N.bind(S,"mousemove",O),N.bind(S,"mouseup",P),k!==S&&(N.bind(k,"mousemove",O),N.bind(k,"mouseup",P)),c=N.add(R,"div",{"class":"mce-resize-helper","data-mce-bogus":"all"},h+" × "+v)}),n.elm=t,N.setStyles(t,{left:r*n[0]+g-t.offsetWidth/2,top:o*n[1]+p-t.offsetHeight/2})}):L(),u.setAttribute("data-mce-selected","1")},L=function(){var e,t;for(e in F(),u&&u.removeAttribute("data-mce-selected"),l)(t=N.get("mceResizeHandle"+e))&&(N.unbind(t),N.remove(t))},I=function(e){var t,n=function(e,t){if(e)do{if(e===t)return!0}while(e=e.parentNode)};i||a.removed||(E(N.select("img[data-mce-selected],hr[data-mce-selected]"),function(e){e.removeAttribute("data-mce-selected")}),t="mousedown"===e.type?e.target:r.getNode(),n(t=N.$(t).closest("table,img,figure.image,hr")[0],R)&&(z(),n(r.getStart(!0),t)&&n(r.getEnd(!0),t))?o(t):L())},M=function(e){return ey(function(e,t){for(;t&&t!==e;){if(ty(t)||ey(t))return t;t=t.parentNode}return null}(a.getBody(),e))},F=function(){for(var e in l){var t=l[e];t.elm&&(N.unbind(t.elm),delete t.elm)}},z=function(){try{a.getDoc().execCommand("enableObjectResizing",!1,!1)}catch(e){}};return a.on("init",function(){z(),de.ie&&11<=de.ie&&(a.on("mousedown click",function(e){var t=e.target,n=t.nodeName;i||!/^(TABLE|IMG|HR)$/.test(n)||M(t)||(2!==e.button&&a.selection.select(t,"TABLE"===n),"mousedown"===e.type&&a.nodeChanged())}),a.dom.bind(R,"mscontrolselect",function(e){var t=function(e){ve.setEditorTimeout(a,function(){a.selection.select(e)})};if(M(e.target))return e.preventDefault(),void t(e.target);/^(TABLE|IMG|HR)$/.test(e.target.nodeName)&&(e.preventDefault(),"IMG"===e.target.tagName&&t(e.target))}));var t=ve.throttle(function(e){a.composing||I(e)});a.on("nodechange ResizeEditor ResizeWindow drop FullscreenStateChanged",t),a.on("keyup compositionend",function(e){u&&"TABLE"===u.nodeName&&t(e)}),a.on("hide blur",L),a.on("contextmenu",n)}),a.on("remove",F),{isResizable:B,showResizeRect:o,hideResizeRect:L,updateResizeRect:I,destroy:function(){u=s=null}}},ry=function(e){for(var t=0,n=0,r=e;r&&r.nodeType;)t+=r.offsetLeft||0,n+=r.offsetTop||0,r=r.offsetParent;return{x:t,y:n}},oy=function(e,t,n){var r,o,i,a,u,s=e.dom,c=s.getRoot(),l=0;if(u={elm:t,alignToTop:n},e.fire("scrollIntoView",u),!u.isDefaultPrevented()&&_o.isElement(t)){if(!1===n&&(l=t.offsetHeight),"BODY"!==c.nodeName){var f=e.selection.getScrollContainer();if(f)return r=ry(t).y-ry(f).y+l,a=f.clientHeight,void((r<(i=f.scrollTop)||i+ai.left&&a.rightr.top&&o.bottome?t.cells().length:e},0)},py=function(e,t){for(var n=e.rows(),r=0;r_
',a.startContainer===u&&a.endContainer===u?u.body.innerHTML=t:(a.deleteContents(),0===u.body.childNodes.length?u.body.innerHTML=t:a.createContextualFragment?a.insertNode(a.createContextualFragment(t)):(o=u.createDocumentFragment(),i=u.createElement("div"),o.appendChild(i),i.outerHTML=t,a.insertNode(o))),r=e.dom.get("__caret"),(a=u.createRange()).setStartBefore(r),a.setEndBefore(r),e.selection.setRng(a),e.dom.remove("__caret");try{e.selection.setRng(a)}catch(s){}}else a.item&&(u.execCommand("Delete",!1,null),a=e.getRng()),/^\s+/.test(t)?(a.pasteHTML('_'+t),e.dom.remove("__mce_tmp")):a.pasteHTML(t);n.no_events||e.fire("SetContent",n)}else e.fire("SetContent",n)},Ay=function(e,t,n,r,o){var i=n?t.startContainer:t.endContainer,a=n?t.startOffset:t.endOffset;return A.from(i).map(rr.fromDom).map(function(e){return r&&t.collapsed?e:Hr(e,o(e,a)).getOr(e)}).bind(function(e){return cr(e)?A.some(e):Ir(e)}).map(function(e){return e.dom()}).getOr(e)},Ry=function(e,t,n){return Ay(e,t,!0,n,function(e,t){return Math.min(e.dom().childNodes.length,t)})},_y=function(e,t,n){return Ay(e,t,!1,n,function(e,t){return 0t.clientHeight){e=t;break}t=t.parentNode}return e},scrollIntoView:function(e,t){return oy(c,e,t)},placeCaretAt:function(e,t){return i(uy(e,t,c.getDoc()))},getBoundingClientRect:function(){var e=m();return e.collapsed?hu.fromRangeStart(e).getClientRects()[0]:e.getBoundingClientRect()},destroy:function(){s=l=f=null,t.destroy()}};return n=Zb(p),t=ny(p,c),p.bookmarkManager=n,p.controlSelection=t,p},Iy=_o.isContentEditableFalse,My=Ua,Fy=As,zy=Ts,Uy=function(e,t){for(;t=e(t);)if(t.isVisible())return t;return t},Vy=function(e,t,n,r){var o,i,a,u,s,c,l=e===gu.Forwards,f=l?zy:Fy;return!r.collapsed&&(o=My(r),Iy(o))?Ah(e,t,o,e===gu.Backwards,!0):(u=ma(r.startContainer),f(i=ks(e,t.getBody(),r))?Rh(t,i.getNode(!l)):(i=n(i))?f(i)?Ah(e,t,i.getNode(!l),l,!0):f(a=n(i))&&(!(c=hs(s=i,a))&&_o.isBr(s.getNode())||c)?Ah(e,t,a.getNode(!l),l,!0):u?Dh(t,i.toRange(),!0):null:u?r:null)},Hy=function(e,t,n,r){var o,i,a,u,s,c,l,f,d;if(d=My(r),o=ks(e,t.getBody(),r),i=n(t.getBody(),ph(1),o),a=z(i,hh(1)),s=qt.last(o.getClientRects()),(zy(o)||Rs(o))&&(d=o.getNode()),(Fy(o)||_s(o))&&(d=o.getNode(!0)),!s)return null;if(c=s.left,(u=wh(a,c))&&Iy(u.node))return l=Math.abs(c-u.left),f=Math.abs(c-u.right),Ah(e,t,u.node,l'),o=a,1===t?e.$(r).after(o):e.$(r).before(o),e.selection.select(o,!0),e.selection.collapse())}},qy=function(l,f){return function(){var e,t,n,r,o,i,a,u,s,c=(t=f,r=Gs((e=l).getBody()),o=d(Uy,r.next),i=d(Uy,r.prev),a=t?gu.Forwards:gu.Backwards,u=t?o:i,s=e.selection.getRng(),(n=Vy(a,e,u,s))?n:(n=jy(e,a,s))||null);return!!c&&(l.selection.setRng(c),!0)}},$y=function(u,s){return function(){var e,t,n,r,o,i,a=(r=(t=s)?1:-1,o=t?gh:mh,i=(e=u).selection.getRng(),(n=Hy(r,e,o,i))?n:(n=jy(e,r,i))||null);return!!a&&(u.selection.setRng(a),!0)}};(Qb=Jb||(Jb={}))[Qb.Br=0]="Br",Qb[Qb.Block=1]="Block",Qb[Qb.Wrap=2]="Wrap",Qb[Qb.Eol=3]="Eol";var Wy=function(e,t){return e===gu.Backwards?t.reverse():t},Ky=function(e,t,n,r){for(var o,i,a,u,s,c,l=Gs(n),f=r,d=[];f&&(s=l,c=f,o=t===gu.Forwards?s.next(c):s.prev(c));){if(_o.isBr(o.getNode(!1)))return t===gu.Forwards?{positions:Wy(t,d).concat([o]),breakType:Jb.Br,breakAt:A.some(o)}:{positions:Wy(t,d),breakType:Jb.Br,breakAt:A.some(o)};if(o.isVisible()){if(e(f,o)){var m=(i=t,a=f,u=o,_o.isBr(u.getNode(i===gu.Forwards))?Jb.Br:!1===hs(a,u)?Jb.Block:Jb.Wrap);return{positions:Wy(t,d),breakType:m,breakAt:A.some(o)}}d.push(o),f=o}else f=o}return{positions:Wy(t,d),breakType:Jb.Eol,breakAt:A.none()}},Xy=function(n,r,o,e){return r(o,e).breakAt.map(function(e){var t=r(o,e).positions;return n===gu.Backwards?t.concat(e):[e].concat(t)}).getOr([])},Yy=function(e,i){return U(e,function(e,o){return e.fold(function(){return A.some(o)},function(r){return qa([ee(r.getClientRects()),ee(o.getClientRects())],function(e,t){var n=Math.abs(i-e.left);return Math.abs(i-t.left)<=n?o:r}).or(e)})},A.none())},Gy=function(t,e){return ee(e.getClientRects()).bind(function(e){return Yy(t,e.left)})},Jy=d(Ky,du.isAbove,-1),Qy=d(Ky,du.isBelow,1),Zy=d(Xy,-1,Jy),eC=d(Xy,1,Qy),tC=function(e,t,n,r,o){var i,a,u,s,c=Vi(rr.fromDom(n),"td,th,caption").map(function(e){return e.dom()}),l=z((i=e,G(c,function(e){var t,n,r=(t=La(e.getBoundingClientRect()),n=-1,{left:t.left-n,top:t.top-n,right:t.right+2*n,bottom:t.bottom+2*n,width:t.width+n,height:t.height+n});return[{x:r.left,y:i(r),cell:e},{x:r.right,y:i(r),cell:e}]})),function(e){return t(e,o)});return(a=l,u=r,s=o,U(a,function(e,r){return e.fold(function(){return A.some(r)},function(e){var t=Math.sqrt(Math.abs(e.x-u)+Math.abs(e.y-s)),n=Math.sqrt(Math.abs(r.x-u)+Math.abs(r.y-s));return A.some(nt}),oC=function(t,n){return ee(n.getClientRects()).bind(function(e){return nC(t,e.left,e.top)}).bind(function(e){return Gy((t=e,ic.lastPositionIn(t).map(function(e){return Jy(t,e).positions.concat(e)}).getOr([])),n);var t})},iC=function(t,n){return te(n.getClientRects()).bind(function(e){return rC(t,e.left,e.top)}).bind(function(e){return Gy((t=e,ic.firstPositionIn(t).map(function(e){return[e].concat(Qy(t,e).positions)}).getOr([])),n);var t})},aC=function(e,t){e.selection.setRng(t),iy(e,t)},uC=function(e,t,n){var r,o,i,a,u=e(t,n);return(a=u).breakType===Jb.Wrap&&0===a.positions.length||!_o.isBr(n.getNode())&&(i=u).breakType===Jb.Br&&1===i.positions.length?(r=e,o=t,!u.breakAt.map(function(e){return r(o,e).breakAt.isSome()}).getOr(!1)):u.breakAt.isNone()},sC=d(uC,Jy),cC=d(uC,Qy),lC=function(e,t,n,r){var o,i,a,u,s=e.selection.getRng(),c=t?1:-1;if(es()&&(o=t,i=s,a=n,u=hu.fromRangeStart(i),ic.positionIn(!o,a).map(function(e){return e.isEqual(u)}).getOr(!1))){var l=Ah(c,e,n,!t,!0);return aC(e,l),!0}return!1},fC=function(e,t){var n=t.getNode(e);return _o.isElement(n)&&"TABLE"===n.nodeName?A.some(n):A.none()},dC=function(u,s,c){var e=fC(!!s,c),t=!1===s;e.fold(function(){return aC(u,c.toRange())},function(a){return ic.positionIn(t,u.getBody()).filter(function(e){return e.isEqual(c)}).fold(function(){return aC(u,c.toRange())},function(e){return n=s,o=a,t=c,void((i=mm(r=u))?r.undoManager.transact(function(){var e=rr.fromTag(i);vr(e,gm(r)),ki(e,rr.fromTag("br")),n?Ei(rr.fromDom(o),e):Ni(rr.fromDom(o),e);var t=r.dom.createRng();t.setStart(e.dom(),0),t.setEnd(e.dom(),0),aC(r,t)}):aC(r,t.toRange()));var n,r,o,t,i})})},mC=function(e,t,n,r){var o,i,a,u,s,c,l=e.selection.getRng(),f=hu.fromRangeStart(l),d=e.getBody();if(!t&&sC(r,f)){var m=(u=d,oC(s=n,c=f).orThunk(function(){return ee(c.getClientRects()).bind(function(e){return Yy(Zy(u,hu.before(s)),e.left)})}).getOr(hu.before(s)));return dC(e,t,m),!0}return!(!t||!cC(r,f))&&(o=d,m=iC(i=n,a=f).orThunk(function(){return ee(a.getClientRects()).bind(function(e){return Yy(eC(o,hu.after(i)),e.left)})}).getOr(hu.after(i)),dC(e,t,m),!0)},gC=function(t,n){return function(){return A.from(t.dom.getParent(t.selection.getNode(),"td,th")).bind(function(e){return A.from(t.dom.getParent(e,"table")).map(function(e){return lC(t,n,e)})}).getOr(!1)}},pC=function(n,r){return function(){return A.from(n.dom.getParent(n.selection.getNode(),"td,th")).bind(function(t){return A.from(n.dom.getParent(t,"table")).map(function(e){return mC(n,r,e,t)})}).getOr(!1)}},hC=function(e){return M(["figcaption"],ur(e))},vC=function(e){var t=document.createRange();return t.setStartBefore(e.dom()),t.setEndBefore(e.dom()),t},bC=function(e,t,n){n?ki(e,t):Si(e,t)},yC=function(e,t,n,r){return""===t?(l=e,f=r,d=rr.fromTag("br"),bC(l,d,f),vC(d)):(o=e,i=r,a=t,u=n,s=rr.fromTag(a),c=rr.fromTag("br"),vr(s,u),ki(s,c),bC(o,s,i),vC(c));var o,i,a,u,s,c,l,f,d},CC=function(e,t,n){return t?(o=e.dom(),Qy(o,n).breakAt.isNone()):(r=e.dom(),Jy(r,n).breakAt.isNone());var r,o},xC=function(t,n){var e,r,o,i=rr.fromDom(t.getBody()),a=hu.fromRangeStart(t.selection.getRng()),u=mm(t),s=gm(t);return(e=a,r=i,o=d(Or,r),$i(rr.fromDom(e.container()),co,o).filter(hC)).exists(function(){if(CC(i,n,a)){var e=yC(i,u,s,n);return t.selection.setRng(e),!0}return!1})},wC=function(e,t){return function(){return!!e.selection.isCollapsed()&&xC(e,t)}},NC=function(e,r){return G($(e,function(e){return Eb({shiftKey:!1,altKey:!1,ctrlKey:!1,metaKey:!1,keyCode:0,action:o},e)}),function(e){return t=e,(n=r).keyCode===t.keyCode&&n.shiftKey===t.shiftKey&&n.altKey===t.altKey&&n.ctrlKey===t.ctrlKey&&n.metaKey===t.metaKey?[e]:[];var t,n})},EC=function(e){for(var t=[],n=1;n'},qC=function(e,t){return e.nodeName===t||e.previousSibling&&e.previousSibling.nodeName===t},$C=function(e,t){return t&&e.isBlock(t)&&!/^(TD|TH|CAPTION|FORM)$/.test(t.nodeName)&&!/^(fixed|absolute)/i.test(t.style.position)&&"true"!==e.getContentEditable(t)},WC=function(e,t,n){return!1===_o.isText(t)?n:e?1===n&&t.data.charAt(n-1)===ca?0:n:n===t.data.length-1&&t.data.charAt(n)===ca?t.data.length:n},KC=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},XC=function(e,t){var n=mm(e);n&&n.toLowerCase()===t.tagName.toLowerCase()&&e.dom.setAttribs(t,gm(e))},YC=function(a,e){var t,u,s,i,c,n,r,o,l,f,d,m,g,p,h,v,b,y,C,x=a.dom,w=a.schema,N=w.getNonEmptyElements(),E=a.selection.getRng(),S=function(e){var t,n,r,o=s,i=w.getTextInlineElements();if(e||"TABLE"===f||"HR"===f?(t=x.create(e||m),XC(a,t)):t=c.cloneNode(!1),r=t,!1===vm(a))x.setAttrib(t,"style",null),x.setAttrib(t,"class",null);else do{if(i[o.nodeName]){if(Fu(o))continue;n=o.cloneNode(!1),x.setAttrib(n,"id",""),t.hasChildNodes()?n.appendChild(t.firstChild):r=n,t.appendChild(n)}}while((o=o.parentNode)&&o!==u);return jC(r),t},k=function(e){var t,n,r,o;if(o=WC(e,s,i),_o.isText(s)&&(e?0s.childNodes.length-1,s=s.childNodes[Math.min(i,s.childNodes.length-1)]||s,i=g&&_o.isText(s)?s.nodeValue.length:0),(u=KC(x,s))&&((m&&!n||!m&&n)&&(s=function(e,t,n,r,o){var i,a,u,s,c,l,f,d=t||"P",m=e.dom,g=KC(m,r);if(!(a=m.getParent(r,m.isBlock))||!$C(m,a)){if(l=(a=a||g)===e.getBody()||(f=a)&&/^(TD|TH|CAPTION)$/.test(f.nodeName)?a.nodeName.toLowerCase():a.parentNode.nodeName.toLowerCase(),!a.hasChildNodes())return i=m.create(d),XC(e,i),a.appendChild(i),n.setStart(i,0),n.setEnd(i,0),i;for(s=r;s.parentNode!==a;)s=s.parentNode;for(;s&&!m.isBlock(s);)s=(u=s).previousSibling;if(u&&e.schema.isValidChild(l,d.toLowerCase())){for(i=m.create(d),XC(e,i),u.parentNode.insertBefore(i,u),s=u;s&&!m.isBlock(s);)c=s.nextSibling,i.appendChild(s),s=c;n.setStart(r,o),n.setEnd(r,o)}}return r}(a,m,E,s,i)),c=x.getParent(s,x.isBlock),l=c?x.getParent(c.parentNode,x.isBlock):null,f=c?c.nodeName.toUpperCase():"","LI"!==(d=l?l.nodeName.toUpperCase():"")||e.ctrlKey||(l=(c=l).parentNode,f=d),/^(LI|DT|DD)$/.test(f)&&x.isEmpty(c)?HC(a,S,l,c,m):m&&c===a.getBody()||(m=m||"P",ma(c)?(r=wa(c),x.isEmpty(c)&&jC(c),OC(a,r)):k()?T():k(!0)?(r=c.parentNode.insertBefore(S(),c),OC(a,qC(c,"HR")?r:c)):((t=(y=E,C=y.cloneRange(),C.setStart(y.startContainer,WC(!0,y.startContainer,y.startOffset)),C.setEnd(y.endContainer,WC(!1,y.endContainer,y.endOffset)),C).cloneRange()).setEndAfter(c),o=t.extractContents(),b=o,F(Ui(rr.fromDom(b),lr),function(e){var t=e.dom();t.nodeValue=la(t.nodeValue)}),function(e){for(;_o.isText(e)&&(e.nodeValue=e.nodeValue.replace(/^[\r\n]+/,"")),e=e.firstChild;);}(o),r=o.firstChild,x.insertAfter(o,c),function(e,t,n){var r,o=n,i=[];if(o){for(;o=o.firstChild;){if(e.isBlock(o))return;_o.isElement(o)&&!t[o.nodeName.toLowerCase()]&&i.push(o)}for(r=i.length;r--;)!(o=i[r]).hasChildNodes()||o.firstChild===o.lastChild&&""===o.firstChild.nodeValue?e.remove(o):(a=e,(u=o)&&"A"===u.nodeName&&a.isEmpty(u)&&e.remove(o));var a,u}}(x,N,r),p=x,(h=c).normalize(),(v=h.lastChild)&&!/^(left|right)$/gi.test(p.getStyle(v,"float",!0))||p.add(h,"br"),x.isEmpty(c)&&jC(c),r.normalize(),x.isEmpty(r)?(x.remove(r),T()):OC(a,r)),x.setAttrib(r,"id",""),a.fire("NewBlock",{newBlock:r})))},GC=function(e,t){return LC(e).filter(function(e){return 0",sm(f)!==f.documentBaseUrl&&(g+=''),g+='',d=cm(f),m=lm(f),fm(f)&&(g+=''),g+='
'),Kx.add(t.iframeContainer,l),p},Yx=function(e,t){var n=Xx(e,t);t.editorContainer&&(Kx.get(t.editorContainer).style.display=e.orgDisplay,e.hidden=Kx.isHidden(t.editorContainer)),e.getElement().style.display="none",Kx.setAttrib(e.id,"aria-hidden","true"),n||Wx(e)},Gx=gi.DOM,Jx=function(t,n,e){var r,o,i=Sp.get(e);if(r=Sp.urls[e]||t.documentBaseUrl.replace(/\/$/,""),e=Yt.trim(e),i&&-1===Yt.inArray(n,e)){if(Yt.each(Sp.dependencies(e),function(e){Jx(t,n,e)}),t.plugins[e])return;o=new i(t,r,t.$),(t.plugins[e]=o).init&&(o.init(t,r),n.push(e))}},Qx=function(e){return e.replace(/^\-/,"")},Zx=function(e){return{editorContainer:e,iframeContainer:e}},ew=function(e){var t,n,r=e.getElement();return e.inline?Zx(null):(t=r,n=Gx.create("div"),Gx.insertAfter(n,t),Zx(n))},tw=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.getElement();return e.orgDisplay=m.style.display,R(d.theme)?(l=(o=e).settings,f=o.getElement(),i=l.width||Gx.getStyle(f,"width")||"100%",a=l.height||Gx.getStyle(f,"height")||f.offsetHeight,u=l.min_height||100,(s=/^[0-9\.]+(|px)$/i).test(""+i)&&(i=Math.max(parseInt(i,10),100)),s.test(""+a)&&(a=Math.max(parseInt(a,10),u)),c=o.theme.renderUI({targetNode:f,width:i,height:a,deltaWidth:l.delta_width,deltaHeight:l.delta_height}),l.content_editable||(a=(c.iframeHeight||a)+("number"==typeof a?c.deltaHeight||0:""))=n.length)for(r=0,o=e.length;r=n.length||e[r]!==n[r]){i=r+1;break}if(e.length=e.length||e[r]!==n[r]){i=r+1;break}if(1===i)return t;for(r=0,o=e.length-(i-1);r]*>( | |\\s|\xa0|
|)<\\/"+a+">[\r\n]*|
[\r\n]*)$"),r=i.replace(u,"")}return"text"===t.format||yo(rr.fromDom(n))?t.content=r:t.content=Yt.trim(r),t.no_events||e.fire("GetContent",t),t.content},gw=function(e,t){t(e),e.firstChild&&gw(e.firstChild,t),e.next&&gw(e.next,t)},pw=function(e,t,n){var r=function(e,n,t){var r={},o={},i=[];for(var a in t.firstChild&&gw(t.firstChild,function(t){F(e,function(e){e.name===t.name&&(r[e.name]?r[e.name].nodes.push(t):r[e.name]={filter:e,nodes:[t]})}),F(n,function(e){"string"==typeof t.attr(e.name)&&(o[e.name]?o[e.name].nodes.push(t):o[e.name]={filter:e,nodes:[t]})})}),r)r.hasOwnProperty(a)&&i.push(r[a]);for(var a in o)o.hasOwnProperty(a)&&i.push(o[a]);return i}(e,t,n);F(r,function(t){F(t.filter.callbacks,function(e){e(t.nodes,t.filter.name,{})})})},hw=function(e){return e instanceof Ob},vw=function(e,t){var r;e.dom.setHTML(e.getBody(),t),ap(r=e)&&ic.firstPositionIn(r.getBody()).each(function(e){var t=e.getNode(),n=_o.isTable(t)?ic.firstPositionIn(t).getOr(e):e;r.selection.setRng(n.toRange())})},bw=function(u,s,c){return void 0===c&&(c={}),c.format=c.format?c.format:"html",c.set=!0,c.content=hw(s)?"":s,hw(s)||c.no_events||(u.fire("BeforeSetContent",c),s=c.content),A.from(u.getBody()).fold(j(s),function(e){return hw(s)?function(e,t,n,r){pw(e.parser.getNodeFilters(),e.parser.getAttributeFilters(),n);var o=rl({validate:e.validate},e.schema).serialize(n);return r.content=yo(rr.fromDom(t))?o:Yt.trim(o),vw(e,r.content),r.no_events||e.fire("SetContent",r),n}(u,e,s,c):(t=u,n=e,o=c,0===(r=s).length||/^\s+$/.test(r)?(a='
',"TABLE"===n.nodeName?r=""+a+"":/^(UL|OL)$/.test(n.nodeName)&&(r="
  • "+a+"
  • "),(i=mm(t))&&t.schema.isValidChild(n.nodeName.toLowerCase(),i.toLowerCase())?(r=a,r=t.dom.createHTML(i,t.settings.forced_root_block_attrs,r)):r||(r='
    '),vw(t,r),t.fire("SetContent",o)):("raw"!==o.format&&(r=rl({validate:t.validate},t.schema).serialize(t.parser.parse(r,{isRootContent:!0,insert:!0}))),o.content=yo(rr.fromDom(n))?r:Yt.trim(r),vw(t,o.content),o.no_events||t.fire("SetContent",o)),o.content);var t,n,r,o,i,a})},yw=gi.DOM,Cw=function(e){return A.from(e).each(function(e){return e.destroy()})},xw=function(e){if(!e.removed){var t=e._selectionOverrides,n=e.editorUpload,r=e.getBody(),o=e.getElement();r&&e.save({is_removing:!0}),e.removed=!0,e.unbindAllNativeEvents(),e.hasHiddenInput&&o&&yw.remove(o.nextSibling),!e.inline&&r&&(i=e,yw.setStyle(i.id,"display",i.orgDisplay)),Bg(e),e.editorManager.remove(e),yw.remove(e.getContainer()),Cw(t),Cw(n),e.destroy()}var i},ww=function(e,t){var n,r,o,i=e.selection,a=e.dom;e.destroyed||(t||e.removed?(t||(e.editorManager.off("beforeunload",e._beforeUnload),e.theme&&e.theme.destroy&&e.theme.destroy(),Cw(i),Cw(a)),(r=(n=e).formElement)&&(r._mceOldSubmit&&(r.submit=r._mceOldSubmit,r._mceOldSubmit=null),yw.unbind(r,"submit reset",n.formEventDelegate)),(o=e).contentAreaContainer=o.formElement=o.container=o.editorContainer=null,o.bodyElement=o.contentDocument=o.contentWindow=null,o.iframeElement=o.targetElm=null,o.selection&&(o.selection=o.selection.win=o.selection.dom=o.selection.dom.doc=null),e.destroyed=!0):e.remove())},Nw=gi.DOM,Ew=Yt.extend,Sw=Yt.each,kw=Yt.resolve,Tw=de.ie,Aw=function(e,t,n){var r,o,i,a,u,s,c,l=this,f=l.documentBaseUrl=n.documentBaseURL,d=n.baseURI;r=l,o=e,i=f,a=n.defaultSettings,u=t,c={id:o,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:i,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,render_ui:!0,indentation:"40px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",entity_encoding:"named",url_converter:(s=r).convertURL,url_converter_scope:s,ie7_compat:!0},t=bl(fl,c,a,u),l.settings=t,wi.language=t.language||"en",wi.languageLoad=t.language_load,wi.baseURL=n.baseURL,l.id=e,l.setDirty(!1),l.plugins={},l.documentBaseURI=new dw(t.document_base_url,{base_uri:d}),l.baseURI=d,l.contentCSS=[],l.contentStyles=[],l.shortcuts=new Gg(l),l.loadedCSS={},l.editorCommands=new Eg(l),l.suffix=n.suffix,l.editorManager=n,l.inline=t.inline,l.buttons={},l.menuItems={},t.cache_suffix&&(de.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),!1===t.override_viewport&&(de.overrideViewPort=!1),n.fire("SetupEditor",{editor:l}),l.execCallback("setup",l),l.$=pn.overrideDefaults(function(){return{context:l.inline?l.getBody():l.getDoc(),element:l.getBody()}})};Ew(Aw.prototype={render:function(){aw(this)},focus:function(e){ip(this,e)},hasFocus:function(){return ap(this)},execCallback:function(e){for(var t=[],n=1;n=n.x&&o.x+o.w<=n.w+n.x&&o.y>=n.y&&o.y+o.h<=n.h+n.y)return r[i];return null},intersect:function(e,t){var n,r,o,i;return n=sN(e.x,t.x),r=sN(e.y,t.y),o=uN(e.x+e.w,t.x+t.w),i=uN(e.y+e.h,t.y+t.h),o-n<0||i-r<0?null:fN(n,r,o-n,i-r)},clamp:function(e,t,n){var r,o,i,a,u,s,c,l,f,d;return u=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h,f=t.x+t.w,d=t.y+t.h,r=sN(0,t.x-u),o=sN(0,t.y-s),i=sN(0,c-f),a=sN(0,l-d),u+=r,s+=o,n&&(c+=r,l+=o,u-=i,s-=a),fN(u,s,(c-=i)-u,(l-=a)-s)},create:fN,fromClientRect:function(e){return fN(e.left,e.top,e.width,e.height)}},mN={},gN={add:function(e,t){mN[e.toLowerCase()]=t},has:function(e){return!!mN[e.toLowerCase()]},get:function(e){var t=e.toLowerCase(),n=mN.hasOwnProperty(t)?mN[t]:null;if(null===n)throw new Error("Could not find module for type: "+e);return n},create:function(e,t){var n;if("string"==typeof e?(t=t||{}).type=e:e=(t=e).type,e=e.toLowerCase(),!(n=mN[e]))throw new Error("Could not find control by type: "+e);return(n=new n(t)).type=e,n}},pN=Yt.each,hN=Yt.extend,vN=function(){};vN.extend=oN=function(n){var e,t,r,o=this.prototype,i=function(){var e,t,n;if(!iN&&(this.init&&this.init.apply(this,arguments),t=this.Mixins))for(e=t.length;e--;)(n=t[e]).init&&n.init.apply(this,arguments)},a=function(){return this},u=function(n,r){return function(){var e,t=this._super;return this._super=o[n],e=r.apply(this,arguments),this._super=t,e}};for(t in iN=!0,e=new this,iN=!1,n.Mixins&&(pN(n.Mixins,function(e){for(var t in e)"init"!==t&&(n[t]=e[t])}),o.Mixins&&(n.Mixins=o.Mixins.concat(n.Mixins))),n.Methods&&pN(n.Methods.split(","),function(e){n[e]=a}),n.Properties&&pN(n.Properties.split(","),function(e){var t="_"+e;n[e]=function(e){return e!==undefined?(this[t]=e,this):this[t]}}),n.Statics&&pN(n.Statics,function(e,t){i[t]=e}),n.Defaults&&o.Defaults&&(n.Defaults=hN({},o.Defaults,n.Defaults)),n)"function"==typeof(r=n[t])&&o[t]?e[t]=u(t,r):e[t]=r;return i.prototype=e,(i.constructor=i).extend=oN,i};var bN=Math.min,yN=Math.max,CN=Math.round,xN=function(e,n){var r,o,t,i;if(n=n||'"',null===e)return"null";if("string"==(t=typeof e))return o="\bb\tt\nn\ff\rr\"\"''\\\\",n+e.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g,function(e,t){return'"'===n&&"'"===e?e:(r=o.indexOf(t))+1?"\\"+o.charAt(r+1):(e=t.charCodeAt().toString(16),"\\u"+"0000".substring(e.length)+e)})+n;if("object"===t){if(e.hasOwnProperty&&"[object Array]"===Object.prototype.toString.call(e)){for(r=0,o="[";r+~]|"+st+")"+st+"*"),pt=new RegExp("="+st+"*([^\\]'\"]*?)"+st+"*\\]","g"),ht=new RegExp(ft),vt=new RegExp("^"+ct+"$"),bt={ID:new RegExp("^#("+ct+")"),CLASS:new RegExp("^\\.("+ct+")"),TAG:new RegExp("^("+ct+"|[*])"),ATTR:new RegExp("^"+lt),PSEUDO:new RegExp("^"+ft),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+st+"*(even|odd|(([+-]|)(\\d*)n|)"+st+"*(?:([+-]|)"+st+"*(\\d+)|))"+st+"*\\)|)","i"),bool:new RegExp("^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$","i"),needsContext:new RegExp("^"+st+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+st+"*((?:-\\d)?\\d*)"+st+"*\\)|)(?=[^-]|$)","i")},yt=/^(?:input|select|textarea|button)$/i,Ct=/^h\d$/i,xt=/^[^{]+\{\s*\[native \w/,wt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Nt=/[+~]/,Et=/'|\\/g,St=new RegExp("\\\\([\\da-f]{1,6}"+st+"?|("+st+")|.)","ig"),kt=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{it.apply(nt=at.call(Ke.childNodes),Ke.childNodes),nt[Ke.childNodes.length].nodeType}catch(jN){it={apply:nt.length?function(e,t){ot.apply(e,at.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}var Tt=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m;if((t?t.ownerDocument||t:Ke)!==Ue&&ze(t),n=n||[],!e||"string"!=typeof e)return n;if(1!==(u=(t=t||Ue).nodeType)&&9!==u)return[];if(He&&!r){if(o=wt.exec(e))if(a=o[1]){if(9===u){if(!(i=t.getElementById(a))||!i.parentNode)return n;if(i.id===a)return n.push(i),n}else if(t.ownerDocument&&(i=t.ownerDocument.getElementById(a))&&$e(t,i)&&i.id===a)return n.push(i),n}else{if(o[2])return it.apply(n,t.getElementsByTagName(e)),n;if((a=o[3])&&Re.getElementsByClassName)return it.apply(n,t.getElementsByClassName(a)),n}if(Re.qsa&&(!je||!je.test(e))){if(f=l=We,d=t,m=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){for(c=Oe(e),(l=t.getAttribute("id"))?f=l.replace(Et,"\\$&"):t.setAttribute("id",f),f="[id='"+f+"'] ",s=c.length;s--;)c[s]=f+It(c[s]);d=Nt.test(e)&&Pt(t.parentNode)||t,m=c.join(",")}if(m)try{return it.apply(n,d.querySelectorAll(m)),n}catch(g){}finally{l||t.removeAttribute("id")}}}return Le(e.replace(dt,"$1"),t,n,r)};function At(){var r=[];return function e(t,n){return r.push(t+" ")>_e.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function Rt(e){return e[We]=!0,e}function _t(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function Dt(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function Bt(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function Ot(a){return Rt(function(i){return i=+i,Rt(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function Pt(e){return e&&typeof e.getElementsByTagName!==et&&e}for(Ae in Re=Tt.support={},Be=Tt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},ze=Tt.setDocument=function(e){var t,s=e?e.ownerDocument||e:Ke,n=s.defaultView;return s!==Ue&&9===s.nodeType&&s.documentElement?(Ve=(Ue=s).documentElement,He=!Be(s),n&&n!==function(e){try{return e.top}catch(t){}return null}(n)&&(n.addEventListener?n.addEventListener("unload",function(){ze()},!1):n.attachEvent&&n.attachEvent("onunload",function(){ze()})),Re.attributes=!0,Re.getElementsByTagName=!0,Re.getElementsByClassName=xt.test(s.getElementsByClassName),Re.getById=!0,_e.find.ID=function(e,t){if(typeof t.getElementById!==et&&He){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},_e.filter.ID=function(e){var t=e.replace(St,kt);return function(e){return e.getAttribute("id")===t}},_e.find.TAG=Re.getElementsByTagName?function(e,t){if(typeof t.getElementsByTagName!==et)return t.getElementsByTagName(e)}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},_e.find.CLASS=Re.getElementsByClassName&&function(e,t){if(He)return t.getElementsByClassName(e)},qe=[],je=[],Re.disconnectedMatch=!0,je=je.length&&new RegExp(je.join("|")),qe=qe.length&&new RegExp(qe.join("|")),t=xt.test(Ve.compareDocumentPosition),$e=t||xt.test(Ve.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},Ze=t?function(e,t){if(e===t)return Fe=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!Re.sortDetached&&t.compareDocumentPosition(e)===n?e===s||e.ownerDocument===Ke&&$e(Ke,e)?-1:t===s||t.ownerDocument===Ke&&$e(Ke,t)?1:Me?ut.call(Me,e)-ut.call(Me,t):0:4&n?-1:1)}:function(e,t){if(e===t)return Fe=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],u=[t];if(!o||!i)return e===s?-1:t===s?1:o?-1:i?1:Me?ut.call(Me,e)-ut.call(Me,t):0;if(o===i)return _t(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?_t(a[r],u[r]):a[r]===Ke?-1:u[r]===Ke?1:0},s):Ue},Tt.matches=function(e,t){return Tt(e,null,null,t)},Tt.matchesSelector=function(e,t){if((e.ownerDocument||e)!==Ue&&ze(e),t=t.replace(pt,"='$1']"),Re.matchesSelector&&He&&(!qe||!qe.test(t))&&(!je||!je.test(t)))try{var n=(void 0).call(e,t);if(n||Re.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(jN){}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(St,kt),e[3]=(e[3]||e[4]||e[5]||"").replace(St,kt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||Tt.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&Tt.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return bt.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ht.test(n)&&(t=Oe(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(St,kt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=Ge[e+" "];return t||(t=new RegExp("(^|"+st+")"+e+"("+st+"|$)"))&&Ge(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==et&&e.getAttribute("class")||"")})},ATTR:function(n,r,o){return function(e){var t=Tt.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===o:"!="===r?t!==o:"^="===r?o&&0===t.indexOf(o):"*="===r?o&&-1)[^>]*$|#([\w\-]*)$)/,tn=Te.Event,nn=Gt.makeMap("children,contents,next,prev"),rn=function(e){return void 0!==e},on=function(e){return"string"==typeof e},an=function(e,t){var n,r,o;for(o=(t=t||Jt).createElement("div"),n=t.createDocumentFragment(),o.innerHTML=e;r=o.firstChild;)n.appendChild(r);return n},un=function(e,t,n,r){var o;if(on(t))t=an(t,xn(e[0]));else if(t.length&&!t.nodeType){if(t=hn.makeArray(t),r)for(o=t.length-1;0<=o;o--)un(e,t[o],n,r);else for(o=0;o"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:en.exec(e)))return hn(t).find(e);if(n[1])for(r=an(e,xn(t)).firstChild;r;)Qt.call(o,r),r=r.nextSibling;else{if(!(r=xn(t).getElementById(n[2])))return o;if(r.id!==n[2])return o.find(e);o.length=1,o[0]=r}}else this.add(e,!1);return o},toArray:function(){return Gt.toArray(this)},add:function(e,t){var n,r,o=this;if(on(e))return o.add(hn(e));if(!1!==t)for(n=hn.unique(o.toArray().concat(hn.makeArray(e))),o.length=n.length,r=0;r=a.length&&r(o)}))})})},eo=function(e){return Zr(e,Qr.nu)},to=function(n){return{is:function(e){return n===e},isValue:x,isError:C,getOr:q(n),getOrThunk:q(n),getOrDie:q(n),or:function(e){return to(n)},orThunk:function(e){return to(n)},fold:function(e,t){return t(n)},map:function(e){return to(e(n))},mapError:function(e){return to(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return A.some(n)}}},no=function(n){return{is:C,isValue:C,isError:x,getOr:$,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return no(n)},mapError:function(e){return no(e(n))},each:o,bind:function(e){return no(n)},exists:C,forall:x,toOption:A.none}},ro={value:to,error:no};function oo(e,u){var t=e,n=function(e,t,n,r){var o,i;if(e){if(!r&&e[t])return e[t];if(e!==u){if(o=e[n])return o;for(i=e.parentNode;i&&i!==u;i=i.parentNode)if(o=i[n])return o}}};this.current=function(){return t},this.next=function(e){return t=n(t,"firstChild","nextSibling",e)},this.prev=function(e){return t=n(t,"lastChild","previousSibling",e)},this.prev2=function(e){return t=function(e,t,n,r){var o,i,a;if(e){if(o=e[n],u&&o===u)return;if(o){if(!r)for(a=o[t];a;a=a[t])if(!a[t])return a;return o}if((i=e.parentNode)&&i!==u)return i}}(t,"lastChild","previousSibling",e)}}var io,ao,uo,so=function(t){var n;return function(e){return(n=n||function(e,t){for(var n={},r=0,o=e.length;r\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Fo=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,zo=/[<>&\"\']/g,Uo=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,Vo={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};ao={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},uo={"<":"<",">":">","&":"&",""":'"',"'":"'"};var Ho=function(e,t){var n,r,o,i={};if(e){for(e=e.split(","),t=t||10,n=0;n>10),56320+(1023&t))):Vo[t]||String.fromCharCode(t):uo[e]||io[e]||(n=e,(r=or.fromTag("div").dom()).innerHTML=n,r.textContent||r.innerText||n);var n,r})}},Ko={},Xo={},Yo=Gt.makeMap,Go=Gt.each,Jo=Gt.extend,Qo=Gt.explode,Zo=Gt.inArray,ei=function(e,t){return(e=Gt.trim(e))?e.split(t||" "):[]},ti=function(e){var u,t,n,r,o,i,s={},a=function(e,t,n){var r,o,i,a=function(e,t){var n,r,o={};for(n=0,r=e.length;n
    ').css(n).appendTo(a)[0];return c.set(A.some({caret:i,element:e,before:t})),c.get().each(function(e){t&&hn(e.caret).addClass("mce-visual-caret-before")}),f(),(r=e.ownerDocument.createRange()).setStart(s,0),r.setEnd(s,0),r},hide:l,getCss:function(){return".mce-visual-caret {position: absolute;background-color: black;background-color: currentcolor;}.mce-visual-caret-hidden {display: none;}*[data-mce-caret] {position: absolute;left: -1000px;right: auto;top: 0;margin: 0;padding: 0;}"},reposition:function(){c.get().each(function(e){var t=es(a,e.element,e.before);hn(e.caret).css(t)})},destroy:function(){return be.clearInterval(t)}}},ns=function(){return Qu.isIE()||Qu.isEdge()||Qu.isFirefox()},rs=function(e){return Zu(e)||Bo.isTable(e)&&ns()},os=Bo.isContentEditableFalse,is=Bo.matchStyleValues("display","block table table-cell table-caption list-item"),as=va,us=pa,ss=Bo.isElement,cs=Oa,ls=function(e){return 0=o.data.length-1)return 1===e&&(r=s(o))?ys(r):n;if(wa(o)&&i<=1)return-1===e&&(r=u(o))?Cs(r):n;if(i===o.data.length)return(r=s(o))?ys(r):n;if(0===i)return(r=u(o))?Cs(r):n}return n},ws=function(e,t){return A.from(hs(e?0:-1,t)).filter(os)},Ns=function(e,t,n){var r=xs(e,t,n);return-1===e?gu.fromRangeStart(r):gu.fromRangeEnd(r)},Es=function(e){return A.from(e.getNode()).map(or.fromDom)},Ss=function(e,t){for(;t=e(t);)if(t.isVisible())return t;return t},ks=function(e,t){var n=ps(e,t);return!(n||!Bo.isBr(e.getNode()))||n};(hu=pu||(pu={}))[hu.Backwards=-1]="Backwards",hu[hu.Forwards=1]="Forwards";var Ts,As,Rs,_s,Ds,Bs=Bo.isContentEditableFalse,Os=Bo.isText,Ps=Bo.isElement,Ls=Bo.isBr,Is=Oa,Ms=function(e){return _a(e)||!!Pa(t=e)&&!0!==U(re(t.getElementsByTagName("*")),function(e,t){return e||Sa(t)},!1);var t},Fs=La,zs=function(e,t){return e.hasChildNodes()&&t'),t},tc=function(e,t){return Zs.lastPositionIn(e).fold(function(){return!1},function(e){return t.setStart(e.container(),e.offset()),t.setEnd(e.container(),e.offset()),!0})},nc=function(e,t,n){return!(!1!==t.hasChildNodes()||!Vu(e,t)||(o=n,i=(r=t).ownerDocument.createTextNode(fa),r.appendChild(i),o.setStart(i,0),o.setEnd(i,0),0));var r,o,i},rc=function(e,t,n,r){var o,i,a,u,s=n[t?"start":"end"],c=e.getRoot();if(s){for(a=s[0],i=c,o=s.length-1;1<=o;o--){if(u=i.childNodes,nc(c,i,r))return!0;if(s[o]>u.length-1)return!!nc(c,i,r)||tc(i,r);i=u[s[o]]}3===i.nodeType&&(a=Math.min(s[0],i.nodeValue.length)),1===i.nodeType&&(a=Math.min(s[0],i.childNodes.length)),t?r.setStart(i,a):r.setEnd(i,a)}return!0},oc=function(e){return Bo.isText(e)&&0=t.nodeValue.length&&e.splice(0,1),t=e[e.length-1],0===m&&0h.length-1?p=h.length-1:p<0&&(p=0),d=h[p]||g),l===d)return o(v([l]));for(n=e.findCommonAncestor(l,d),a=l;a;a=a.parentNode){if(a===d)return C(l,n,!0);if(a===n)break}for(a=d;a;a=a.parentNode){if(a===l)return C(d,n);if(a===n)break}r=y(l,n)||l,i=y(d,n)||d,C(l,r,!0),(s=b(r===l?r:r.nextSibling,"nextSibling",i===d?i.nextSibling:i)).length&&o(v(s)),C(d,i)}},Ac=(Ts=fr,As="text",Rs=function(e){return Ts(e)?A.from(e.dom().nodeValue):A.none()},_s=nr.detect().browser,{get:function(e){if(!Ts(e))throw new Error("Can only get "+As+" value of a "+As+" node");return Ds(e).getOr("")},getOption:Ds=_s.isIE()&&10===_s.version.major?function(e){try{return Rs(e)}catch(jN){return A.none()}}:Rs,set:function(e,t){if(!Ts(e))throw new Error("Can only set raw "+As+" value of a "+As+" node");e.dom().nodeValue=t}}),Rc=function(e){return Ac.get(e)},_c=function(r,o,i,a){return Mr(o).fold(function(){return"skipping"},function(e){return"br"===a||fr(n=o)&&"\ufeff"===Rc(n)?"valid":lr(t=o)&&Vi(t,Ji())?"existing":Uu(o)?"caret":mc.isValid(r,i,a)&&mc.isValid(r,sr(e),i)?"valid":"invalid-child";var t,n})},Dc=function(e,t,n,r){var o,i,a=t.uid,u=void 0===a?(o="mce-annotation",i=(new Date).getTime(),o+"_"+Math.floor(1e9*Math.random())+ ++oa+String(i)):a,s=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var o=0;for(r=Object.getOwnPropertySymbols(e);o=n.startOffset&&"\xa0"===n.startContainer.nodeValue[n.startOffset]),t.setStart(r.startContainer,r.startOffset),t.setEnd(r.endContainer,r.endOffset),e.selection.setRng(t)),s.selection.getRng().collapsed){var i=Dc(s.getDoc(),f,c,l.decorate);sa(i,"\xa0"),s.selection.getRng().insertNode(i.dom()),s.selection.select(i.dom())}else{var a=Fu.getPersistentBookmark(s.selection,!1),u=s.selection.getRng();Bc(s,u,c,l.decorate,f),s.selection.moveToBookmark(a)}})};function Pc(s){var n,r=(n={},{register:function(e,t){n[e]={name:e,settings:t}},lookup:function(e){return n.hasOwnProperty(e)?A.from(n[e]).map(function(e){return e.settings}):A.none()}});ra(s,r);var o=na(s);return{register:function(e,t){r.register(e,t)},annotate:function(t,n){r.lookup(t).each(function(e){Oc(s,t,e,n)})},annotationChanged:function(e,t){o.addListener(e,t)},remove:function(e){ea(s,A.some(e)).each(function(e){var t=e.elements;F(t,Bi)})},getAll:function(e){var t,n,r,o,i,a,u=(t=s,n=e,r=or.fromDom(t.getBody()),o=ji(r,"["+Qi()+'="'+n+'"]'),i={},F(o,function(e){var t=Cr(e,Zi()),n=i.hasOwnProperty(t)?i[t]:[];i[t]=n.concat([e])}),i);return a=function(e){return W(e,function(e){return e.dom()})},pr(u,function(e,t,n){return{k:t,v:a(e,t,n)}})}}}var Lc=function(e){return Gt.grep(e.childNodes,function(e){return"LI"===e.nodeName})},Ic=function(e){return e&&e.firstChild&&e.firstChild===e.lastChild&&("\xa0"===(t=e.firstChild).data||Bo.isBr(t));var t},Mc=function(e){return 0'))},Xc=function(n){$r(n).each(function(t){Fr(t).each(function(e){lo(n)&&mo(t)&&lo(e)&&Di(t)})})},Yc=Gt.makeMap;function Gc(e){var u,s,c,l,f,d=[];return u=(e=e||{}).indent,s=Yc(e.indent_before||""),c=Yc(e.indent_after||""),l=Wo.getEncodeFunc(e.entity_encoding||"raw",e.entities),f="html"===e.element_format,{start:function(e,t,n){var r,o,i,a;if(u&&s[e]&&0":" />",n&&u&&c[e]&&0"),u&&c[e]&&0")},comment:function(e){d.push("\x3c!--",e,"--\x3e")},pi:function(e,t){t?d.push(""):d.push(""),u&&d.push("\n")},doctype:function(e){d.push("",u?"\n":"")},reset:function(){d.length=0},getContent:function(){return d.join("").replace(/\n$/,"")}}}function Jc(t,g){void 0===g&&(g=ri());var p=Gc(t);return(t=t||{}).validate=!("validate"in t)||t.validate,{serialize:function(e){var f,d;d=t.validate,f={3:function(e){p.text(e.value,e.raw)},8:function(e){p.comment(e.value)},7:function(e){p.pi(e.name,e.value)},10:function(e){p.doctype(e.value)},4:function(e){p.cdata(e.value)},11:function(e){if(e=e.firstChild)for(;m(e),e=e.next;);}},p.reset();var m=function(e){var t,n,r,o,i,a,u,s,c,l=f[e.type];if(l)l(e);else{if(t=e.name,n=e.shortEnded,r=e.attributes,d&&r&&1|)$/," "):o("nextSibling")||(t=t.replace(/( | )(
    |)$/," "))),t}(g.getRng(),t)),r=e.parser,m=n.merge,o=Jc({validate:e.settings.validate},e.schema),d='​',s={content:t,format:"html",selection:!0,paste:n.paste},(s=e.fire("BeforeSetContent",s)).isDefaultPrevented())e.fire("SetContent",{content:s.content,format:"html",selection:!0,paste:n.paste});else{-1===(t=s.content).indexOf("{$caret}")&&(t+="{$caret}"),t=t.replace(/\{\$caret\}/,d);var h,v,b,y,C,x,w=(l=g.getRng()).startContainer||(l.parentElement?l.parentElement():null),N=e.getBody();w===N&&g.isCollapsed()&&p.isBlock(N.firstChild)&&(h=e,(v=N.firstChild)&&!h.schema.getShortEndedElements()[v.nodeName])&&p.isEmpty(N.firstChild)&&((l=p.createRng()).setStart(N.firstChild,0),l.setEnd(N.firstChild,0),g.setRng(l)),g.isCollapsed()||(e.selection.setRng(el(e.selection.getRng())),e.getDoc().execCommand("Delete",!1,null),b=e.selection.getRng(),y=t,C=b.startContainer,x=b.startOffset,3===C.nodeType&&b.collapsed&&("\xa0"===C.data[x]?(C.deleteData(x,1),/[\u00a0| ]$/.test(y)||(y+=" ")):"\xa0"===C.data[x-1]&&(C.deleteData(x-1,1),/[\u00a0| ]$/.test(y)||(y=" "+y))),t=y);var E,S,k,T={context:(i=g.getNode()).nodeName.toLowerCase(),data:n.data,insert:!0};if(u=r.parse(t,T),!0===n.paste&&Vc(e.schema,u)&&jc(p,i))return l=Hc(o,p,e.selection.getRng(),u),e.selection.setRng(l),void e.fire("SetContent",s);if(function(e){for(var t=e;t=t.walk();)1===t.type&&t.attr("data-mce-fragment","1")}(u),"mce_marker"===(f=u.lastChild).attr("id"))for(f=(c=f).prev;f;f=f.walk(!0))if(3===f.type||!p.isBlock(f.name)){e.schema.isValidChild(f.parent.name,"span")&&f.parent.insert(c,f,"br"===f.name);break}if(e._selectionOverrides.showBlockCaretContainer(i),T.invalid){for(nl(e,d),i=g.getNode(),a=e.getBody(),9===i.nodeType?i=f=a:f=i;f!==a;)f=(i=f).parentNode;t=i===a?a.innerHTML:p.getOuterHTML(i),t=o.serialize(r.parse(t.replace(//i,function(){return o.serialize(u)}))),i===a?p.setHTML(a,t):p.setOuterHTML(i,t)}else!function(e,t,n){if("all"===n.getAttribute("data-mce-bogus"))n.parentNode.insertBefore(e.dom.createFragment(t),n);else{var r=n.firstChild,o=n.lastChild;!r||r===o&&"BR"===r.nodeName?e.dom.setHTML(n,t):nl(e,t)}}(e,t=o.serialize(u),i);!function(e,t){var n=e.schema.getTextInlineElements(),r=e.dom;if(t){var o=e.getBody(),i=new $c(r);Gt.each(r.select("*[data-mce-fragment]"),function(e){for(var t=e.parentNode;t&&t!==o;t=t.parentNode)n[e.nodeName.toLowerCase()]&&i.compare(t,e)&&r.remove(e,!0)})}}(e,m),function(n,e){var t,r,o,i,a,u=n.dom,s=n.selection;if(e){if(n.selection.scrollIntoView(e),t=function(e){for(var t=n.getBody();e&&e!==t;e=e.parentNode)if("false"===n.dom.getContentEditable(e))return e;return null}(e))return u.remove(e),s.select(t);var c=u.createRng();(i=e.previousSibling)&&3===i.nodeType?(c.setStart(i,i.nodeValue.length),me.ie||(a=e.nextSibling)&&3===a.nodeType&&(i.appendData(a.data),a.parentNode.removeChild(a))):(c.setStartBefore(e),c.setEndBefore(e)),r=u.getParent(e,u.isBlock),u.remove(e),r&&u.isEmpty(r)&&(n.$(r).empty(),c.setStart(r,0),c.setEnd(r,0),tl(r)||r.getAttribute("data-mce-fragment")||!(o=function(e){var t=vu.fromRangeStart(e);if(t=Hs(n.getBody()).next(t))return t.toRange()}(c))?u.add(r,u.create("br",{"data-mce-bogus":"1"})):(c=o,u.remove(r))),s.setRng(c)}}(e,p.get("mce_marker")),E=e.getBody(),Gt.each(E.getElementsByTagName("*"),function(e){e.removeAttribute("data-mce-fragment")}),S=e.dom,k=e.selection.getStart(),A.from(S.getParent(k,"td,th")).map(or.fromDom).each(Xc),e.fire("SetContent",s),e.addVisual()}},ol=function(e,t){var n,r,o="string"!=typeof(n=t)?(r=Gt.extend({paste:n.paste,data:{paste:n.paste}},n),{content:n.content,details:r}):{content:n,details:{}};rl(e,o.content,o.details)},il=Sr("sections","settings"),al=nr.detect().deviceType.isTouch(),ul=["lists","autolink","autosave"],sl={theme:"mobile"},cl=function(e){var t=D(e)?e.join(" "):e,n=W(R(t)?t.split(" "):[],Xn);return z(n,function(e){return 0=e.data.length,s=0===t;e.replaceData(t,n,(o=s,i=u,U((r=a).split(""),function(e,t){return-1!==" \f\n\r\t\x0B".indexOf(t)||"\xa0"===t?e.previousCharIsSpace||""===e.str&&o||e.str.length===r.length-1&&i?{previousCharIsSpace:!1,str:e.str+"\xa0"}:{previousCharIsSpace:!0,str:e.str+" "}:{previousCharIsSpace:!1,str:e.str+t}},{previousCharIsSpace:!1,str:""}).str))}},Nf=function(e,t){var n,r=e.data.slice(t),o=r.length-(n=r,n.replace(/^\s+/g,"")).length;return wf(e,t,o)},Ef=function(e,t){return r=e,o=(n=t).container(),i=n.offset(),!1===vu.isTextPosition(n)&&o===r.parentNode&&i>vu.before(r).offset()?vu(t.container(),t.offset()-1):t;var n,r,o,i},Sf=function(e){return Oa(e.previousSibling)?A.some((t=e.previousSibling,Bo.isText(t)?vu(t,t.data.length):vu.after(t))):e.previousSibling?Zs.lastPositionIn(e.previousSibling):A.none();var t},kf=function(e){return Oa(e.nextSibling)?A.some((t=e.nextSibling,Bo.isText(t)?vu(t,0):vu.before(t))):e.nextSibling?Zs.firstPositionIn(e.nextSibling):A.none();var t},Tf=function(r,o){return Sf(o).orThunk(function(){return kf(o)}).orThunk(function(){return e=r,t=o,n=vu.before(t.previousSibling?t.previousSibling:t.parentNode),Zs.prevPosition(e,n).fold(function(){return Zs.nextPosition(e,vu.after(t))},A.some);var e,t,n})},Af=function(n,r){return kf(r).orThunk(function(){return Sf(r)}).orThunk(function(){return e=n,t=r,Zs.nextPosition(e,vu.after(t)).fold(function(){return Zs.prevPosition(e,vu.before(t))},A.some);var e,t})},Rf=function(e,t,n){return(r=e,o=t,i=n,r?Af(o,i):Tf(o,i)).map(d(Ef,n));var r,o,i},_f=function(t,n,e){e.fold(function(){t.focus()},function(e){t.selection.setRng(e.toRange(),n)})},Df=function(e,t){return t&&e.schema.getBlockElements().hasOwnProperty(sr(t))},Bf=function(e){if(Al(e)){var t=or.fromHtml('
    ');return _i(e),Ai(e,t),A.some(vu.before(t.dom()))}return A.none()},Of=function(e,t,l){var n=Fr(e).filter(function(e){return Bo.isText(e.dom())}),r=zr(e).filter(function(e){return Bo.isText(e.dom())});return Di(e),Wa([n,r,t],function(e,t,n){var r,o,i,a,u=e.dom(),s=t.dom(),c=u.data.length;return o=s,i=l,a=Yn((r=u).data).length,r.appendData(o.data),Di(or.fromDom(o)),i&&Nf(r,a),n.container()===s?vu(u,c):n}).orThunk(function(){return l&&(n.each(function(e){return t=e.dom(),n=e.dom().length,r=t.data.slice(0,n),o=r.length-Yn(r).length,wf(t,n-o,o);var t,n,r,o}),r.each(function(e){return Nf(e.dom(),0)})),t})},Pf=function(e,t){return n=e.schema.getTextInlineElements(),r=sr(t),mr.call(n,r);var n,r},Lf=function(t,n,e,r){void 0===r&&(r=!0);var o,i=Rf(n,t.getBody(),e.dom()),a=Wi(e,d(Df,t),(o=t.getBody(),function(e){return e.dom()===o})),u=Of(e,i,Pf(t,e));t.dom.isEmpty(t.getBody())?(t.setContent(""),t.selection.setCursorLocation()):a.bind(Bf).fold(function(){r&&_f(t,n,u)},function(e){r&&_f(t,n,A.some(e))})},If=function(a,u){var e,t,n,r,o,i;return(e=a.getBody(),t=u,n=a.selection.getRng(),r=xs(t?1:-1,e,n),o=vu.fromRangeStart(r),i=or.fromDom(e),!1===t&&gf(o)?A.some(bf.remove(o.getNode(!0))):t&&mf(o)?A.some(bf.remove(o.getNode())):!1===t&&mf(o)&&tf(i,o)?nf(i,o).map(function(e){return bf.remove(e.getNode())}):t&&gf(o)&&ef(i,o)?rf(i,o).map(function(e){return bf.remove(e.getNode())}):xf(e,t,o)).map(function(e){return e.fold((o=a,i=u,function(e){return o._selectionOverrides.hideFakeCaret(),Lf(o,i,or.fromDom(e)),!0}),(n=a,r=u,function(e){var t=r?vu.before(e):vu.after(e);return n.selection.setRng(t.toRange()),!0}),(t=a,function(e){return t.selection.setRng(e.toRange()),!0}));var t,n,r,o,i}).getOr(!1)},Mf=function(e,t){var n,r=e.selection.getNode();return!!Bo.isContentEditableFalse(r)&&(n=or.fromDom(e.getBody()),F(ji(n,".mce-offscreen-selection"),Di),Lf(e,t,or.fromDom(e.selection.getNode())),xl(e),!0)},Ff=function(e,t){return e.selection.isCollapsed()?If(e,t):Mf(e,t)},zf=function(e){var t,n=function(e,t){for(;t&&t!==e;){if(Bo.isContentEditableTrue(t)||Bo.isContentEditableFalse(t))return t;t=t.parentNode}return null}(e.getBody(),e.selection.getNode());return Bo.isContentEditableTrue(n)&&e.dom.isBlock(n)&&e.dom.isEmpty(n)&&(t=e.dom.create("br",{"data-mce-bogus":"1"}),e.dom.setHTML(n,""),n.appendChild(t),e.selection.setRng(vu.before(t).toRange())),!0},Uf=Bo.isText,Vf=function(e){return Uf(e)&&e.data[0]===fa},Hf=function(e){return Uf(e)&&e.data[e.data.length-1]===fa},jf=function(e){return e.ownerDocument.createTextNode(fa)},qf=function(e,t){return e?function(e){if(Uf(e.previousSibling))return Hf(e.previousSibling)||e.previousSibling.appendData(fa),e.previousSibling;if(Uf(e))return Vf(e)||e.insertData(0,fa),e;var t=jf(e);return e.parentNode.insertBefore(t,e),t}(t):function(e){if(Uf(e.nextSibling))return Vf(e.nextSibling)||e.nextSibling.insertData(0,fa),e.nextSibling;if(Uf(e))return Hf(e)||e.appendData(fa),e;var t=jf(e);return e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t}(t)},$f=d(qf,!0),Wf=d(qf,!1),Kf=function(e,t){return Bo.isText(e.container())?qf(t,e.container()):qf(t,e.getNode())},Xf=function(e,t){var n=t.get();return n&&e.container()===n&&ha(n)},Yf=function(n,e){return e.fold(function(e){Ju.remove(n.get());var t=$f(e);return n.set(t),A.some(vu(t,t.length-1))},function(e){return Zs.firstPositionIn(e).map(function(e){if(Xf(e,n))return vu(n.get(),1);Ju.remove(n.get());var t=Kf(e,!0);return n.set(t),vu(t,1)})},function(e){return Zs.lastPositionIn(e).map(function(e){if(Xf(e,n))return vu(n.get(),n.get().length-1);Ju.remove(n.get());var t=Kf(e,!1);return n.set(t),vu(t,t.length-1)})},function(e){Ju.remove(n.get());var t=Wf(e);return n.set(t),A.some(vu(t,1))})},Gf=function(e,t){for(var n=0;n")},Ym=function(e){return e.getParam("document_base_url","")},Gm=function(e){return Wm(e,"body_id","tinymce")},Jm=function(e){return Wm(e,"body_class","")},Qm=function(e){return e.getParam("content_security_policy","")},Zm=function(e){return e.getParam("br_in_pre",!0)},eg=function(e){if(e.getParam("force_p_newlines",!1))return"p";var t=e.getParam("forced_root_block","p");return!1===t?"":t},tg=function(e){return e.getParam("forced_root_block_attrs",{})},ng=function(e){return e.getParam("br_newline_selector",".mce-toc h2,figcaption,caption")},rg=function(e){return e.getParam("no_newline_selector","")},og=function(e){return e.getParam("keep_styles",!0)},ig=function(e){return e.getParam("end_container_on_empty_block",!1)},ag=function(e){return Gt.explode(e.getParam("font_size_style_values",""))},ug=function(e){return Gt.explode(e.getParam("font_size_classes",""))},sg=function(e){return e.getParam("images_dataimg_filter",q(!0),"function")},cg=function(e){return e.getParam("automatic_uploads",!0,"boolean")},lg=function(e){return e.getParam("images_reuse_filename",!1,"boolean")},fg=function(e){return e.getParam("images_replace_blob_uris",!0,"boolean")},dg=function(e){return e.getParam("images_upload_url","","string")},mg=function(e){return e.getParam("images_upload_base_path","","string")},gg=function(e){return e.getParam("images_upload_credentials",!1,"boolean")},pg=function(e){return e.getParam("images_upload_handler",null,"function")},hg=function(e){return e.getParam("content_css_cors",!1,"boolean")},vg=function(o,t,e){var n=function(e){return t=o,n=e.dom(),r=Er(n,t),A.from(r).filter(function(e){return 0o.childNodes.length-1&&(c=!1),Bo.isDocument(o)&&(o=g,i=0),o===g){if(c&&(u=o.childNodes[0s.childNodes.length-1;s=s.childNodes[Math.min(u,s.childNodes.length-1)]||s,u=c&&3===s.nodeType?s.nodeValue.length:0}var l=i.getParent(s,i.isBlock),f=l?i.getParent(l.parentNode,i.isBlock):null,d=f?f.nodeName.toUpperCase():"",m=t&&t.ctrlKey;"LI"!==d||m||(l=f),s&&3===s.nodeType&&u>=s.nodeValue.length&&(function(e,t,n){for(var r,o=new oo(t,n),i=e.getNonEmptyElements();r=o.next();)if(i[r.nodeName.toLowerCase()]||0")},mceToggleVisualAid:function(){s.hasVisual=!s.hasVisual,s.addVisual()},mceReplaceContent:function(e,t,n){s.execCommand("mceInsertContent",!1,n.replace(/\{\$selection\}/g,i.getContent({format:"text"})))},mceInsertLink:function(e,t,n){var r;"string"==typeof n&&(n={href:n}),r=o.getParent(i.getNode(),"a"),n.href=n.href.replace(" ","%20"),r&&n.href||a.remove("link"),n.href&&a.apply("link",n,r)},selectAll:function(){var e=o.getParent(i.getStart(),Bo.isContentEditableTrue);if(e){var t=o.createRng();t.selectNodeContents(e),i.setRng(t)}},"delete":function(){qm(s)},forwardDelete:function(){$m(s)},mceNewDocument:function(){s.setContent("")},InsertLineBreak:function(e,t,n){return Ug(s,n),!0}});var p=function(n){return function(){var e=i.isCollapsed()?[o.getParent(i.getNode(),o.isBlock)]:i.getSelectedBlocks(),t=sp(e,function(e){return!!a.matchNode(e,n)});return-1!==cp(t,!0)}};e({JustifyLeft:p("alignleft"),JustifyCenter:p("aligncenter"),JustifyRight:p("alignright"),JustifyFull:p("alignjustify"),"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){return f(e)},mceBlockQuote:function(){return f("blockquote")},Outdent:function(){var e;if(n.inline_styles){if((e=o.getParent(i.getStart(),o.isBlock))&&0"),u))[o.length-1]=Gt.extend(o[o.length-1],{func:n,scope:r||i}),Gt.extend(o[0],{desc:i.translate(t),subpatterns:o.slice(1)})},o=function(e,t){return!!t&&t.ctrl===e.ctrlKey&&t.meta===e.metaKey&&t.alt===e.altKey&&t.shift===e.shiftKey&&!!(e.keyCode===t.keyCode||e.charCode&&e.charCode===t.charCode)&&(e.preventDefault(),!0)},c=function(e){return e.func?e.func.call(e.scope):null};i.on("keyup keypress keydown",function(t){var e,n;((n=t).altKey||n.ctrlKey||n.metaKey||"keydown"===(e=t).type&&112<=e.keyCode&&e.keyCode<=123)&&!t.isDefaultPrevented()&&(Op(a,function(e){if(o(t,e))return r=e.subpatterns.slice(0),"keydown"===t.type&&c(e),!0}),o(t,r[0])&&(1===r.length&&"keydown"===t.type&&c(r[0]),r.shift()))}),this.add=function(e,n,r,o){var t;return"string"==typeof(t=r)?r=function(){i.execCommand(t,!1,null)}:Gt.isArray(t)&&(r=function(){i.execCommand(t[0],t[1],t[2])}),Op(Pp(Gt.trim(e.toLowerCase())),function(e){var t=s(e,n,r,o);a[t.id]=t}),!0},this.remove=function(e){var t=s(e);return!!a[t.id]&&(delete a[t.id],!0)}}var Fp=function(e){var t=Ir(e).dom();return e.dom()===t.activeElement},zp=function(t){return(e=Ir(t),n=e!==undefined?e.dom():H.document,A.from(n.activeElement).map(or.fromDom)).filter(function(e){return t.dom().contains(e.dom())});var e,n},Up=function(t,e){return(n=e,n.collapsed?A.from(ja(n.startContainer,n.startOffset)).map(or.fromDom):A.none()).bind(function(e){return bo(e)?A.some(e):!1===Lr(t,e)?A.some(t):A.none()});var n},Vp=function(t,e){Up(or.fromDom(t.getBody()),e).bind(function(e){return Zs.firstPositionIn(e.dom())}).fold(function(){t.selection.normalize()},function(e){return t.selection.setRng(e.toRange())})},Hp=function(e){if(e.setActive)try{e.setActive()}catch(t){e.focus()}else e.focus()},jp=function(e){var t,n=e.getBody();return n&&(t=or.fromDom(n),Fp(t)||zp(t).isSome())},qp=function(e){return e.inline?jp(e):(t=e).iframeElement&&Fp(or.fromDom(t.iframeElement));var t},$p=function(e){return e.editorManager.setActive(e)},Wp=function(e,t){e.removed||(t?$p(e):function(t){var e=t.selection,n=t.settings.content_editable,r=t.getBody(),o=e.getRng();t.quirks.refreshContentEditable();var i,a,u=(i=t,a=e.getNode(),i.dom.getParent(a,function(e){return"true"===i.dom.getContentEditable(e)}));if(t.$.contains(r,u))return Hp(u),Vp(t,o),$p(t);t.bookmark!==undefined&&!1===qp(t)&&np(t).each(function(e){t.selection.setRng(e),o=e}),n||(me.opera||Hp(r),t.getWin().focus()),(me.gecko||n)&&(Hp(r),Vp(t,o)),$p(t)}(e))},Kp=qp,Xp=function(e,t){return t.dom()[e]},Yp=function(e,t){return parseInt(Nr(t,e),10)},Gp=d(Xp,"clientWidth"),Jp=d(Xp,"clientHeight"),Qp=d(Yp,"margin-top"),Zp=d(Yp,"margin-left"),eh=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=or.fromDom(e.getBody()),p=e.inline?g:(r=g,or.fromDom(r.dom().ownerDocument.documentElement)),h=(o=e.inline,a=t,u=n,s=(i=p).dom().getBoundingClientRect(),{x:a-(o?s.left+i.dom().clientLeft+Zp(i):0),y:u-(o?s.top+i.dom().clientTop+Qp(i):0)});return l=h.x,f=h.y,d=Gp(c=p),m=Jp(c),0<=l&&0<=f&&l<=d&&f<=m},th=function(e){var t,n=e.inline?e.getBody():e.getContentAreaContainer();return(t=n,A.from(t).map(or.fromDom)).map(function(e){return Lr(Ir(e),e)}).getOr(!1)};function nh(n){var t,o=[],i=function(){var e,t=n.theme;return t&&t.getNotificationManagerImpl?t.getNotificationManagerImpl():{open:e=function(){throw new Error("Theme did not provide a NotificationManager implementation.")},close:e,reposition:e,getArgs:e}},a=function(){0i&&(u=n.pageX+r-i),n.pageY+o>a&&(s=n.pageY+o-a),t.style.width=r-u+"px",t.style.height=o-s+"px",v(e.clientX,e.clientY)}},o=Yh(c,e),u=c,i=function(){u.dragging&&s.fire("dragend"),Gh(u)},(s=e).on("mousedown",n),e.on("mousemove",r),e.on("mouseup",o),t.bind(a,"mousemove",r),t.bind(a,"mouseup",i),e.on("remove",function(){t.unbind(a,"mousemove",r),t.unbind(a,"mouseup",i)})},Qh=function(e){var n;Jh(e),(n=e).on("drop",function(e){var t="undefined"!=typeof e.clientX?n.getDoc().elementFromPoint(e.clientX,e.clientY):null;($h(t)||$h(n.dom.getContentEditableParent(t)))&&e.preventDefault()})},Zh=function(e){return U(e,function(e,t){return e.concat(function(t){var e=function(e){return W(e,function(e){return(e=Ma(e)).node=t,e})};if(Bo.isElement(t))return e(t.getClientRects());if(Bo.isText(t)){var n=t.ownerDocument.createRange();return n.setStart(t,0),n.setEnd(t,t.data.length),e(n.getClientRects())}}(t))},[])};(Hh=Vh||(Vh={}))[Hh.Up=-1]="Up",Hh[Hh.Down=1]="Down";var ev=function(o,i,a,e,u,t){var n,s,c=0,l=[],r=function(e){var t,n,r;for(r=Zh([e]),-1===o&&(r=r.reverse()),t=0;tt;var t}},ov=function(n){return function(e){return t=n,e.line===t;var t}},iv=Bo.isContentEditableFalse,av=ms,uv=function(e,t){return Math.abs(e.left-t)},sv=function(e,t){return Math.abs(e.right-t)},cv=function(e,t){return e>=t.left&&e<=t.right},lv=function(e,o){return $t.reduce(e,function(e,t){var n,r;return n=Math.min(uv(e,o),sv(e,o)),r=Math.min(uv(t,o),sv(t,o)),cv(o,t)?t:cv(o,e)?e:r===n&&iv(t.node)?t:r=e.top&&n<=e.bottom});return(r=lv(f,t))&&(r=lv((a=e,c=function(t,e){var n;return n=z(Zh([e]),function(e){return!t(e,u)}),s=s.concat(n),0===n.length},(s=[]).push(u=r),fv(Vh.Up,a,d(c,Ua),u.node),fv(Vh.Down,a,d(c,Va),u.node),s),t))&&rs(r.node)?(i=t,{node:(o=r).node,before:uv(o,i)=(n=t).left&&r<=n.right&&o>=n.top&&o<=n.bottom);var n,r,o},!1)},gv=function(t){var e=Oi(function(){if(!t.removed&&t.selection.getRng().collapsed){var e=Um(t,t.selection.getRng(),!1);t.selection.setRng(e)}},0);t.on("focus",function(){e.throttle()}),t.on("blur",function(){e.cancel()})},pv={BACKSPACE:8,DELETE:46,DOWN:40,ENTER:13,LEFT:37,RIGHT:39,SPACEBAR:32,TAB:9,UP:38,END:35,HOME:36,modifierPressed:function(e){return e.shiftKey||e.ctrlKey||e.altKey||this.metaKeyPressed(e)},metaKeyPressed:function(e){return me.mac?e.metaKey:e.ctrlKey&&!e.altKey}},hv=Bo.isContentEditableTrue,vv=Bo.isContentEditableFalse,bv=function(e,t){for(var n=e.getBody();t&&t!==n;){if(hv(t)||vv(t))return t;t=t.parentNode}return null},yv=function(g){var p,e,t,a=g.getBody(),o=ts(g.getBody(),function(e){return g.dom.isBlock(e)},function(){return Kp(g)}),h="sel-"+g.dom.uniqueId(),u=function(e){e&&g.selection.setRng(e)},s=function(){return g.selection.getRng()},v=function(e,t,n,r){return void 0===r&&(r=!0),g.fire("ShowCaret",{target:t,direction:e,before:n}).isDefaultPrevented()?null:(r&&g.selection.scrollIntoView(t,-1===e),o.show(n,t))},b=function(e,t){return t=xs(e,a,t),-1===e?vu.fromRangeStart(t):vu.fromRangeEnd(t)},n=function(e){return va(e)||wa(e)||Na(e)},y=function(e){return n(e.startContainer)||n(e.endContainer)},c=function(e,t){var n,r,o,i,a,u,s,c,l,f,d=g.$,m=g.dom;if(!e)return null;if(e.collapsed){if(!y(e))if(!1===t){if(c=b(-1,e),rs(c.getNode(!0)))return v(-1,c.getNode(!0),!1,!1);if(rs(c.getNode()))return v(-1,c.getNode(),!c.isAtEnd(),!1)}else{if(c=b(1,e),rs(c.getNode()))return v(1,c.getNode(),!c.isAtEnd(),!1);if(rs(c.getNode(!0)))return v(1,c.getNode(!0),!1,!1)}return null}return i=e.startContainer,a=e.startOffset,u=e.endOffset,3===i.nodeType&&0===a&&vv(i.parentNode)&&(i=i.parentNode,a=m.nodeIndex(i),i=i.parentNode),1!==i.nodeType?null:(u===a+1&&(n=i.childNodes[a]),vv(n)?(l=f=n.cloneNode(!0),(s=g.fire("ObjectSelected",{target:n,targetClone:l})).isDefaultPrevented()?null:(r=Yi(or.fromDom(g.getBody()),"#"+h).fold(function(){return d([])},function(e){return d([e.dom()])}),l=s.targetClone,0===r.length&&(r=d('
    ').attr("id",h)).appendTo(g.getBody()),e=g.dom.createRng(),l===f&&me.ie?(r.empty().append('

    \xa0

    ').append(l),e.setStartAfter(r[0].firstChild.firstChild),e.setEndAfter(l)):(r.empty().append("\xa0").append(l).append("\xa0"),e.setStart(r[0].firstChild,1),e.setEnd(r[0].lastChild,0)),r.css({top:m.getPos(n,g.getBody()).y}),r[0].focus(),(o=g.selection.getSel()).removeAllRanges(),o.addRange(e),F(ji(or.fromDom(g.getBody()),"*[data-mce-selected]"),function(e){xr(e,"data-mce-selected")}),n.setAttribute("data-mce-selected","1"),p=n,C(),e)):null)},l=function(){p&&(p.removeAttribute("data-mce-selected"),Yi(or.fromDom(g.getBody()),"#"+h).each(Di),p=null),Yi(or.fromDom(g.getBody()),"#"+h).each(Di),p=null},C=function(){o.hide()};return me.ceFalse&&(function(){g.on("mouseup",function(e){var t=s();t.collapsed&&eh(g,e.clientX,e.clientY)&&u(zm(g,t,!1))}),g.on("click",function(e){var t;(t=bv(g,e.target))&&(vv(t)&&(e.preventDefault(),g.focus()),hv(t)&&g.dom.isChildOf(t,g.selection.getNode())&&l())}),g.on("blur NewBlock",function(){l()}),g.on("ResizeWindow FullscreenStateChanged",function(){return o.reposition()});var n,r,i=function(e,t){var n,r,o=g.dom.getParent(e,g.dom.isBlock),i=g.dom.getParent(t,g.dom.isBlock);return!(!o||!g.dom.isChildOf(o,i)||!1!==vv(bv(g,o)))||o&&(n=o,r=i,!(g.dom.getParent(n,g.dom.isBlock)===g.dom.getParent(r,g.dom.isBlock)))&&function(e){var t=Hs(e);if(!e.firstChild)return!1;var n=vu.before(e.firstChild),r=t.next(n);return r&&!mf(r)&&!gf(r)}(o)};r=!1,(n=g).on("touchstart",function(){r=!1}),n.on("touchmove",function(){r=!0}),n.on("touchend",function(e){var t=bv(n,e.target);vv(t)&&(r||(e.preventDefault(),c(Fm(n,t))))}),g.on("mousedown",function(e){var t,n=e.target;if((n===a||"HTML"===n.nodeName||g.dom.isChildOf(n,a))&&!1!==eh(g,e.clientX,e.clientY))if(t=bv(g,n))vv(t)?(e.preventDefault(),c(Fm(g,t))):(l(),hv(t)&&e.shiftKey||mv(e.clientX,e.clientY,g.selection.getRng())||(C(),g.selection.placeCaretAt(e.clientX,e.clientY)));else if(!1===rs(n)){l(),C();var r=dv(a,e.clientX,e.clientY);if(r&&!i(e.target,r.node)){e.preventDefault();var o=v(1,r.node,r.before,!1);g.getBody().focus(),u(o)}}}),g.on("keypress",function(e){pv.modifierPressed(e)||(e.keyCode,vv(g.selection.getNode())&&e.preventDefault())}),g.on("getSelectionRange",function(e){var t=e.range;if(p){if(!p.parentNode)return void(p=null);(t=t.cloneRange()).selectNode(p),e.range=t}}),g.on("setSelectionRange",function(e){var t;(t=c(e.range,e.forward))&&(e.range=t)}),g.on("AfterSetSelectionRange",function(e){var t,n=e.range;y(n)||"mcepastebin"===n.startContainer.parentNode.id||C(),t=n.startContainer.parentNode,g.dom.hasClass(t,"mce-offscreen-selection")||l()}),g.on("copy",function(e){var t,n=e.clipboardData;if(!e.isDefaultPrevented()&&e.clipboardData&&!me.ie){var r=(t=g.dom.get(h))?t.getElementsByTagName("*")[0]:t;r&&(e.preventDefault(),n.clearData(),n.setData("text/html",r.outerHTML),n.setData("text/plain",r.outerText))}}),Qh(g),gv(g)}(),e=g.contentStyles,t=".mce-content-body",e.push(o.getCss()),e.push(t+" .mce-offscreen-selection {position: absolute;left: -9999999999px;max-width: 1000000px;}"+t+" *[contentEditable=false] {cursor: default;}"+t+" *[contentEditable=true] {cursor: text;}")),{showCaret:v,showBlockCaretContainer:function(e){e.hasAttribute("data-mce-caret")&&(Ea(e),u(s()),g.selection.scrollIntoView(e[0]))},hideFakeCaret:C,destroy:function(){o.destroy(),p=null}}},Cv=function(e,t,n){var r,o,i,a,u=1;for(a=e.getShortEndedElements(),(i=/<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g).lastIndex=r=n;o=i.exec(t);){if(r=i.lastIndex,"/"===o[1])u--;else if(!o[1]){if(o[2]in a)continue;u++}if(0===u)break}return r},xv=function(e,t){var n=e.exec(t);if(n){var r=n[1],o=n[2];return"string"==typeof r&&"data-mce-bogus"===r.toLowerCase()?o:null}return null};function wv(z,U){void 0===U&&(U=ri());var e=function(){};!1!==(z=z||{}).fix_self_closing&&(z.fix_self_closing=!0);var V=z.comment?z.comment:e,H=z.cdata?z.cdata:e,j=z.text?z.text:e,q=z.start?z.start:e,$=z.end?z.end:e,W=z.pi?z.pi:e,K=z.doctype?z.doctype:e;return{parse:function(e){var t,n,r,d,o,i,a,m,u,s,g,c,p,l,f,h,v,b,y,C,x,w,N,E,S,k,T,A,R,_=0,D=[],B=0,O=Wo.decode,P=Gt.makeMap("src,href,data,background,formaction,poster,xlink:href"),L=/((java|vb)script|mhtml):/i,I=function(e){var t,n;for(t=D.length;t--&&D[t].name!==e;);if(0<=t){for(n=D.length-1;t<=n;n--)(e=D[n]).valid&&$(e.name);D.length=t}},M=function(e,t,n,r,o){var i,a,u,s,c;if(n=(t=t.toLowerCase())in g?t:O(n||r||o||""),p&&!m&&0==(0===(u=t).indexOf("data-")||0===u.indexOf("aria-"))){if(!(i=b[t])&&y){for(a=y.length;a--&&!(i=y[a]).pattern.test(t););-1===a&&(i=null)}if(!i)return;if(i.validValues&&!(n in i.validValues))return}if(P[t]&&!z.allow_script_urls){var l=n.replace(/[\s\u0000-\u001F]+/g,"");try{l=decodeURIComponent(l)}catch(f){l=unescape(l)}if(L.test(l))return;if(c=l,!(s=z).allow_html_data_urls&&(/^data:image\//i.test(c)?!1===s.allow_svg_data_urls&&/^data:image\/svg\+xml/i.test(c):/^data:/i.test(c)))return}m&&(t in P||0===t.indexOf("on"))||(d.map[t]=n,d.push({name:t,value:n}))};for(S=new RegExp("<(?:(?:!--([\\w\\W]*?)--\x3e)|(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|(?:!DOCTYPE([\\w\\W]*?)>)|(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^\"'>]+(?:(?:\"[^\"]*\")|(?:'[^']*')|[^>]*))*|\\/|\\s+)>))","g"),k=/([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g,s=U.getShortEndedElements(),E=z.self_closing_elements||U.getSelfClosingElements(),g=U.getBoolAttrs(),p=z.validate,u=z.remove_internals,R=z.fix_self_closing,T=U.getSpecialElements(),N=e+">";t=S.exec(N);){if(_e.length){j(O(e.substr(t.index))),_=t.index+t[0].length;continue}":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),c=n in s,R&&E[n]&&0"===n.charAt(0)&&(n=" "+n),z.allow_conditional_comments||"[if"!==n.substr(0,3).toLowerCase()||(n=" "+n),V(n)):(n=t[2])?H(n.replace(//g,"")):(n=t[3])?K(n):(n=t[4])&&W(n,t[5]);_=t.index+t[0].length}for(_]*data-mce-bogus="all"[^>]*>/g,d=e.schema;for(u=e.getTempAttrs(),s=l,c=new RegExp(["\\s?("+u.join("|")+')="[^"]+"'].join("|"),"gi"),l=s.replace(c,""),a=d.getShortEndedElements();i=f.exec(l);)r=f.lastIndex,o=i[0].length,n=a[i[1]]?r:Nv.findEndTag(d,l,r),l=l.substring(0,r-o)+l.substring(n),f.lastIndex=r-o;return da(l)},Sv={trimExternal:Ev,trimInternal:Ev},kv=0,Tv=2,Av=1,Rv=function(g,p){var e=g.length+p.length+2,h=new Array(e),v=new Array(e),c=function(e,t,n,r,o){var i=l(e,t,n,r);if(null===i||i.start===t&&i.diag===t-r||i.end===e&&i.diag===e-n)for(var a=e,u=n;a")?Lv(r):Iv(t)},Uv=function(e,t,n){"fragmented"===t.type?Ov(t.fragments,e.getBody()):e.setContent(t.content,{format:"raw"}),e.selection.moveToBookmark(n?t.beforeBookmark:t.bookmark)},Vv=function(e,t){return!(!e||!t)&&(r=t,Mv(e)===Mv(r)||(n=t,Fv(e)===Fv(n)));var n,r};function Hv(u){var s,r,o=this,c=0,l=[],t=0,f=function(){return 0===t},i=function(e){f()&&(o.typing=e)},d=function(e){u.setDirty(e)},a=function(e){i(!1),o.add({},e)},n=function(){o.typing&&(i(!1),o.add())};return u.on("init",function(){o.add()}),u.on("BeforeExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&(n(),o.beforeChange())}),u.on("ExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&a(e)}),u.on("ObjectResizeStart Cut",function(){o.beforeChange()}),u.on("SaveContent ObjectResized blur",a),u.on("DragEnd",a),u.on("KeyUp",function(e){var t=e.keyCode;e.isDefaultPrevented()||((33<=t&&t<=36||37<=t&&t<=40||45===t||e.ctrlKey)&&(a(),u.nodeChanged()),46!==t&&8!==t||u.nodeChanged(),r&&o.typing&&!1===Vv(zv(u),l[0])&&(!1===u.isDirty()&&(d(!0),u.fire("change",{level:l[0],lastLevel:null})),u.fire("TypingUndo"),r=!1,u.nodeChanged()))}),u.on("KeyDown",function(e){var t=e.keyCode;if(!e.isDefaultPrevented())if(33<=t&&t<=36||37<=t&&t<=40||45===t)o.typing&&a(e);else{var n=e.ctrlKey&&!e.altKey||e.metaKey;!(t<16||20i.custom_undo_redo_levels){for(n=0;n
    ").append(n.childNodes)}))},$v[jv="pre"]||($v[jv]=[]),$v[jv].push(qv);var Xv=function(e,t){Kv($v[e],function(e){e(t)})},Yv=/^(src|href|style)$/,Gv=Gt.each,Jv=mc.isEq,Qv=function(e,t,n){return e.isChildOf(t,n)&&t!==n&&!e.isBlock(n)},Zv=function(e,t,n){var r,o,i;return r=t[n?"startContainer":"endContainer"],o=t[n?"startOffset":"endOffset"],Bo.isElement(r)&&(i=r.childNodes.length-1,!n&&o&&o--,r=r.childNodes[i=r.nodeValue.length&&(r=new oo(r,e.getBody()).next()||r),Bo.isText(r)&&!n&&0===o&&(r=new oo(r,e.getBody()).prev()||r),r},eb=function(e,t,n,r){var o=e.create(n,r);return t.parentNode.insertBefore(o,t),o.appendChild(t),o},tb=function(e,t,n,r,o){var i=or.fromDom(t),a=or.fromDom(e.create(r,o)),u=n?Vr(i):Ur(i);return Ri(a,u),n?(Si(i,a),Ti(a,i)):(ki(i,a),Ai(a,i)),a.dom()},nb=function(e,t,n,r){return!(t=mc.getNonWhiteSpaceSibling(t,n,r))||"BR"===t.nodeName||e.isBlock(t)},rb=function(e,n,r,o,i){var t,a,u,s,c,l,f,d,m,g,p,h,v,b,y=e.dom;if(c=y,!(Jv(l=o,(f=n).inline)||Jv(l,f.block)||(f.selector?Bo.isElement(l)&&c.is(l,f.selector):void 0)||(s=o,n.links&&"A"===s.tagName)))return!1;if("all"!==n.remove)for(Gv(n.styles,function(e,t){e=mc.normalizeStyleValue(y,mc.replaceVars(e,r),t),"number"==typeof t&&(t=e,i=0),(n.remove_similar||!i||Jv(mc.getStyle(y,i,t),e))&&y.setStyle(o,t,""),u=1}),u&&""===y.getAttrib(o,"style")&&(o.removeAttribute("style"),o.removeAttribute("data-mce-style")),Gv(n.attributes,function(e,t){var n;if(e=mc.replaceVars(e,r),"number"==typeof t&&(t=e,i=0),!i||Jv(y.getAttrib(i,t),e)){if("class"===t&&(e=y.getAttrib(o,t))&&(n="",Gv(e.split(/\s+/),function(e){/mce\-\w+/.test(e)&&(n+=(n?" ":"")+e)}),n))return void y.setAttrib(o,t,n);"class"===t&&o.removeAttribute("className"),Yv.test(t)&&o.removeAttribute("data-mce-"+t),o.removeAttribute(t)}}),Gv(n.classes,function(e){e=mc.replaceVars(e,r),i&&!y.hasClass(i,e)||y.removeClass(o,e)}),a=y.getAttribs(o),t=0;t)\s*/g,"$1"),Gt.map(e.split(/(?:>|\s+(?![^\[\]]+\]))/),function(e){var t=Gt.map(e.split(/(?:~\+|~|\+)/),_b),n=t.pop();return t.length&&(n.siblings=t),n}).reverse()):[]},Bb=function(n,e){var t,r,o,i,a,u,s="";if(!1===(u=n.settings.preview_styles))return"";"string"!=typeof u&&(u="font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow");var c=function(e){return e.replace(/%(\w+)/g,"")};if("string"==typeof e){if(!(e=n.formatter.get(e)))return;e=e[0]}return"preview"in e&&!1===(u=e.preview)?"":(t=e.block||e.inline||"span",(i=Db(e.selector)).length?(i[0].name||(i[0].name=t),t=e.selector,r=Rb(i,n)):r=Rb([t],n),o=Ab.select(t,r)[0]||r.firstChild,Tb(e.styles,function(e,t){(e=c(e))&&Ab.setStyle(o,t,e)}),Tb(e.attributes,function(e,t){(e=c(e))&&Ab.setAttrib(o,t,e)}),Tb(e.classes,function(e){e=c(e),Ab.hasClass(o,e)||Ab.addClass(o,e)}),n.fire("PreviewFormats"),Ab.setStyles(r,{position:"absolute",left:-65535}),n.getBody().appendChild(r),a=Ab.getStyle(n.getBody(),"fontSize",!0),a=/px$/.test(a)?parseInt(a,10):0,Tb(u.split(" "),function(e){var t=Ab.getStyle(o,e,!0);if(!("background-color"===e&&/transparent|rgba\s*\([^)]+,\s*0\)/.test(t)&&(t=Ab.getStyle(n.getBody(),e,!0),"#ffffff"===Ab.toHex(t).toLowerCase())||"color"===e&&"#000000"===Ab.toHex(t).toLowerCase())){if("font-size"===e&&/em|%$/.test(t)){if(0===a)return;t=parseFloat(t)/(/%$/.test(t)?100:1)*a+"px"}"border"===e&&t&&(s+="padding:0 2px;"),s+=e+":"+t+";"}}),n.fire("AfterPreviewFormats"),Ab.remove(r),s)},Ob=function(e,t,n,r,o){var i=t.get(n);!hm.match(e,n,r,o)||"toggle"in i[0]&&!i[0].toggle?Nb.applyFormat(e,n,r,o):ib(e,n,r,o)},Pb=function(e){e.addShortcut("meta+b","","Bold"),e.addShortcut("meta+i","","Italic"),e.addShortcut("meta+u","","Underline");for(var t=1;t<=6;t++)e.addShortcut("access+"+t,"",["FormatBlock",!1,"h"+t]);e.addShortcut("access+7","",["FormatBlock",!1,"p"]),e.addShortcut("access+8","",["FormatBlock",!1,"div"]),e.addShortcut("access+9","",["FormatBlock",!1,"address"])};function Lb(e){var t,n,r,o=(t=e,n={},(r=function(e,t){e&&("string"!=typeof e?Gt.each(e,function(e,t){r(t,e)}):(t=t.length?t:[t],Gt.each(t,function(e){"undefined"==typeof e.deep&&(e.deep=!e.selector),"undefined"==typeof e.split&&(e.split=!e.selector||e.inline),"undefined"==typeof e.remove&&e.selector&&!e.inline&&(e.remove="none"),e.selector&&e.inline&&(e.mixed=!0,e.block_expand=!0),"string"==typeof e.classes&&(e.classes=e.classes.split(/\s+/))}),n[e]=t))})(kb.get(t.dom)),r(t.settings.formats),{get:function(e){return e?n[e]:n},register:r,unregister:function(e){return e&&n[e]&&delete n[e],n}}),i=Pi(null);return Pb(e),Rm(e),{get:o.get,register:o.register,unregister:o.unregister,apply:d(Nb.applyFormat,e),remove:d(ib,e),toggle:d(Ob,e,o),match:d(hm.match,e),matchAll:d(hm.matchAll,e),matchNode:d(hm.matchNode,e),canApply:d(hm.canApply,e),formatChanged:d(Sb,e,i),getCssText:d(Bb,e)}}var Ib,Mb=Object.prototype.hasOwnProperty,Fb=(Ib=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t)/g,"\n").replace(/^[\r\n]*|[\r\n]*$/g,"").replace(/^\s*(()?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g,"")};i--;)r=(n=e[i]).firstChild?n.firstChild.value:"","script"===t?((o=n.attr("type"))&&n.attr("type","mce-no/type"===o?null:o.replace(/^mce\-/,"")),"xhtml"===s.element_format&&0")):"xhtml"===s.element_format&&0T(n)?(C=A(y*b),y=A(C/b)):(y=A(C/b),C=A(y*b))),N.setStyles(D(s),{width:y,height:C}),r=0<(r=f.startPos.x+t)?r:0,o=0<(o=f.startPos.y+n)?o:0,N.setStyles(c,{left:r,top:o,display:"block"}),c.innerHTML=y+" × "+C,f[2]<0&&s.clientWidth<=y&&N.setStyle(s,"left",g+(h-y)),f[3]<0&&s.clientHeight<=C&&N.setStyle(s,"top",p+(v-C)),(t=R.scrollWidth-x)+(n=R.scrollHeight-w)!=0&&N.setStyles(c,{left:r-t,top:o-n}),i||(xp(a,u,h,v),i=!0)},P=function(){i=!1;var e=function(e,t){t&&(u.style[e]||!a.schema.isValid(u.nodeName.toLowerCase(),e)?N.setStyle(D(u),e,t):N.setAttrib(D(u),e,t))};e("width",y),e("height",C),N.unbind(S,"mousemove",O),N.unbind(S,"mouseup",P),k!==S&&(N.unbind(k,"mousemove",O),N.unbind(k,"mouseup",P)),N.remove(s),N.remove(c),o(u),wp(a,u,y,C),N.setAttrib(u,"style",N.getAttrib(u,"style")),a.nodeChanged()},o=function(e){var t,r,o,n,i;L(),F(),t=N.getPos(e,R),g=t.x,p=t.y,i=e.getBoundingClientRect(),r=i.width||i.right-i.left,o=i.height||i.bottom-i.top,u!==e&&(u=e,y=C=0),n=a.fire("ObjectSelected",{target:e}),B(e)&&!n.isDefaultPrevented()?E(l,function(n,e){var t;(t=N.get("mceResizeHandle"+e))&&N.remove(t),t=N.add(R,"div",{id:"mceResizeHandle"+e,"data-mce-bogus":"all","class":"mce-resizehandle",unselectable:!0,style:"cursor:"+e+"-resize; margin:0; padding:0"}),11===me.ie&&(t.contentEditable=!1),N.bind(t,"mousedown",function(e){var t;e.stopImmediatePropagation(),e.preventDefault(),d=(t=e).screenX,m=t.screenY,h=D(u).clientWidth,v=D(u).clientHeight,b=v/h,(f=n).startPos={x:r*n[0]+g,y:o*n[1]+p},x=R.scrollWidth,w=R.scrollHeight,s=u.cloneNode(!0),N.addClass(s,"mce-clonedresizable"),N.setAttrib(s,"data-mce-bogus","all"),s.contentEditable=!1,s.unSelectabe=!0,N.setStyles(s,{left:g,top:p,margin:0}),s.removeAttribute("data-mce-selected"),R.appendChild(s),N.bind(S,"mousemove",O),N.bind(S,"mouseup",P),k!==S&&(N.bind(k,"mousemove",O),N.bind(k,"mouseup",P)),c=N.add(R,"div",{"class":"mce-resize-helper","data-mce-bogus":"all"},h+" × "+v)}),n.elm=t,N.setStyles(t,{left:r*n[0]+g-t.offsetWidth/2,top:o*n[1]+p-t.offsetHeight/2})}):L(),u.setAttribute("data-mce-selected","1")},L=function(){var e,t;for(e in F(),u&&u.removeAttribute("data-mce-selected"),l)(t=N.get("mceResizeHandle"+e))&&(N.unbind(t),N.remove(t))},I=function(e){var t,n=function(e,t){if(e)do{if(e===t)return!0}while(e=e.parentNode)};i||a.removed||(E(N.select("img[data-mce-selected],hr[data-mce-selected]"),function(e){e.removeAttribute("data-mce-selected")}),t="mousedown"===e.type?e.target:r.getNode(),n(t=N.$(t).closest("table,img,figure.image,hr")[0],R)&&(z(),n(r.getStart(!0),t)&&n(r.getEnd(!0),t))?o(t):L())},M=function(e){return gy(function(e,t){for(;t&&t!==e;){if(py(t)||gy(t))return t;t=t.parentNode}return null}(a.getBody(),e))},F=function(){for(var e in l){var t=l[e];t.elm&&(N.unbind(t.elm),delete t.elm)}},z=function(){try{a.getDoc().execCommand("enableObjectResizing",!1,!1)}catch(e){}};return a.on("init",function(){z(),me.ie&&11<=me.ie&&(a.on("mousedown click",function(e){var t=e.target,n=t.nodeName;i||!/^(TABLE|IMG|HR)$/.test(n)||M(t)||(2!==e.button&&a.selection.select(t,"TABLE"===n),"mousedown"===e.type&&a.nodeChanged())}),a.dom.bind(R,"mscontrolselect",function(e){var t=function(e){be.setEditorTimeout(a,function(){a.selection.select(e)})};if(M(e.target))return e.preventDefault(),void t(e.target);/^(TABLE|IMG|HR)$/.test(e.target.nodeName)&&(e.preventDefault(),"IMG"===e.target.tagName&&t(e.target))}));var t=be.throttle(function(e){a.composing||I(e)});a.on("nodechange ResizeEditor ResizeWindow drop FullscreenStateChanged",t),a.on("keyup compositionend",function(e){u&&"TABLE"===u.nodeName&&t(e)}),a.on("hide blur",L),a.on("contextmenu",n)}),a.on("remove",F),{isResizable:B,showResizeRect:o,hideResizeRect:L,updateResizeRect:I,destroy:function(){u=s=null}}},vy=function(e){for(var t=0,n=0,r=e;r&&r.nodeType;)t+=r.offsetLeft||0,n+=r.offsetTop||0,r=r.offsetParent;return{x:t,y:n}},by=function(e,t,n){var r,o,i,a,u,s=e.dom,c=s.getRoot(),l=0;if(u={elm:t,alignToTop:n},e.fire("scrollIntoView",u),!u.isDefaultPrevented()&&Bo.isElement(t)){if(!1===n&&(l=t.offsetHeight),"BODY"!==c.nodeName){var f=e.selection.getScrollContainer();if(f)return r=vy(t).y-vy(f).y+l,a=f.clientHeight,void((r<(i=f.scrollTop)||i+ai.left&&a.rightr.top&&o.bottome?t.cells().length:e},0)},Ry=function(e,t){for(var n=e.rows(),r=0;r_
    ',a.startContainer===u&&a.endContainer===u?u.body.innerHTML=t:(a.deleteContents(),0===u.body.childNodes.length?u.body.innerHTML=t:a.createContextualFragment?a.insertNode(a.createContextualFragment(t)):(o=u.createDocumentFragment(),i=u.createElement("div"),o.appendChild(i),i.outerHTML=t,a.insertNode(o))),r=e.dom.get("__caret"),(a=u.createRange()).setStartBefore(r),a.setEndBefore(r),e.selection.setRng(a),e.dom.remove("__caret");try{e.selection.setRng(a)}catch(s){}}else a.item&&(u.execCommand("Delete",!1,null),a=e.getRng()),/^\s+/.test(t)?(a.pasteHTML('_'+t),e.dom.remove("__mce_tmp")):a.pasteHTML(t);n.no_events||e.fire("SetContent",n)}else e.fire("SetContent",n)},Hy=function(e,t,n,r,o){var i=n?t.startContainer:t.endContainer,a=n?t.startOffset:t.endOffset;return A.from(i).map(or.fromDom).map(function(e){return r&&t.collapsed?e:jr(e,o(e,a)).getOr(e)}).bind(function(e){return lr(e)?A.some(e):Mr(e)}).map(function(e){return e.dom()}).getOr(e)},jy=function(e,t,n){return Hy(e,t,!0,n,function(e,t){return Math.min(e.dom().childNodes.length,t)})},qy=function(e,t,n){return Hy(e,t,!1,n,function(e,t){return 0t.clientHeight){e=t;break}t=t.parentNode}return e},scrollIntoView:function(e,t){return by(c,e,t)},placeCaretAt:function(e,t){return i(xy(e,t,c.getDoc()))},getBoundingClientRect:function(){var e=m();return e.collapsed?vu.fromRangeStart(e).getClientRects()[0]:e.getBoundingClientRect()},destroy:function(){s=l=f=null,t.destroy()}};return n=my(p),t=hy(p,c),p.bookmarkManager=n,p.controlSelection=t,p};(dy=fy||(fy={}))[dy.Br=0]="Br",dy[dy.Block=1]="Block",dy[dy.Wrap=2]="Wrap",dy[dy.Eol=3]="Eol";var Gy=function(e,t){return e===pu.Backwards?t.reverse():t},Jy=function(e,t,n,r){for(var o,i,a,u,s,c,l=Hs(n),f=r,d=[];f&&(s=l,c=f,o=t===pu.Forwards?s.next(c):s.prev(c));){if(Bo.isBr(o.getNode(!1)))return t===pu.Forwards?{positions:Gy(t,d).concat([o]),breakType:fy.Br,breakAt:A.some(o)}:{positions:Gy(t,d),breakType:fy.Br,breakAt:A.some(o)};if(o.isVisible()){if(e(f,o)){var m=(i=t,a=f,u=o,Bo.isBr(u.getNode(i===pu.Forwards))?fy.Br:!1===ps(a,u)?fy.Block:fy.Wrap);return{positions:Gy(t,d),breakType:m,breakAt:A.some(o)}}d.push(o),f=o}else f=o}return{positions:Gy(t,d),breakType:fy.Eol,breakAt:A.none()}},Qy=function(n,r,o,e){return r(o,e).breakAt.map(function(e){var t=r(o,e).positions;return n===pu.Backwards?t.concat(e):[e].concat(t)}).getOr([])},Zy=function(e,i){return U(e,function(e,o){return e.fold(function(){return A.some(o)},function(r){return Wa([te(r.getClientRects()),te(o.getClientRects())],function(e,t){var n=Math.abs(i-e.left);return Math.abs(i-t.left)<=n?o:r}).or(e)})},A.none())},eC=function(t,e){return te(e.getClientRects()).bind(function(e){return Zy(t,e.left)})},tC=d(Jy,gu.isAbove,-1),nC=d(Jy,gu.isBelow,1),rC=d(Qy,-1,tC),oC=d(Qy,1,nC),iC=Bo.isContentEditableFalse,aC=Ha,uC=function(e,t,n,r){var o=e===pu.Forwards,i=o?mf:gf;if(!r.collapsed){var a=aC(r);if(iC(a))return Mm(e,t,a,e===pu.Backwards,!0)}var u=pa(r.startContainer),s=Ns(e,t.getBody(),r);if(i(s))return Fm(t,s.getNode(!o));var c=yl.normalizePosition(o,n(s));if(!c)return u?r:null;if(i(c))return Mm(e,t,c.getNode(!o),o,!0);var l=n(c);return l&&i(l)&&ks(c,l)?Mm(e,t,l.getNode(!o),o,!0):u?Um(t,c.toRange(),!0):null},sC=function(e,t,n,r){var o,i,a,u,s,c,l,f,d;if(d=aC(r),o=Ns(e,t.getBody(),r),i=n(t.getBody(),rv(1),o),a=z(i,ov(1)),s=$t.last(o.getClientRects()),(mf(o)||hf(o))&&(d=o.getNode()),(gf(o)||vf(o))&&(d=o.getNode(!0)),!s)return null;if(c=s.left,(u=lv(a,c))&&iC(u.node))return l=Math.abs(c-u.left),f=Math.abs(c-u.right),Mm(e,t,u.node,l'),o=a,1===t?e.$(r).after(o):e.$(r).before(o),e.selection.select(o,!0),e.selection.collapse())}},lC=function(l,f){return function(){var e,t,n,r,o,i,a,u,s,c=(t=f,r=Hs((e=l).getBody()),o=d(Ss,r.next),i=d(Ss,r.prev),a=t?pu.Forwards:pu.Backwards,u=t?o:i,s=e.selection.getRng(),(n=uC(a,e,u,s))?n:(n=cC(e,a,s))||null);return!!c&&(l.selection.setRng(c),!0)}},fC=function(u,s){return function(){var e,t,n,r,o,i,a=(r=(t=s)?1:-1,o=t?nv:tv,i=(e=u).selection.getRng(),(n=sC(r,e,o,i))?n:(n=cC(e,r,i))||null);return!!a&&(u.selection.setRng(a),!0)}},dC=function(r,o){return function(){var t,e=o?vu.fromRangeEnd(r.selection.getRng()):vu.fromRangeStart(r.selection.getRng()),n=o?nC(r.getBody(),e):tC(r.getBody(),e);return(o?ne(n.positions):te(n.positions)).filter((t=o,function(e){return t?gf(e):mf(e)})).fold(q(!1),function(e){return r.selection.setRng(e.toRange()),!0})}},mC=function(e,t,n,r,o){var i,a,u,s,c=ji(or.fromDom(n),"td,th,caption").map(function(e){return e.dom()}),l=z((i=e,J(c,function(e){var t,n,r=(t=Ma(e.getBoundingClientRect()),n=-1,{left:t.left-n,top:t.top-n,right:t.right+2*n,bottom:t.bottom+2*n,width:t.width+n,height:t.height+n});return[{x:r.left,y:i(r),cell:e},{x:r.right,y:i(r),cell:e}]})),function(e){return t(e,o)});return(a=l,u=r,s=o,U(a,function(e,r){return e.fold(function(){return A.some(r)},function(e){var t=Math.sqrt(Math.abs(e.x-u)+Math.abs(e.y-s)),n=Math.sqrt(Math.abs(r.x-u)+Math.abs(r.y-s));return A.some(nt}),hC=function(t,n){return te(n.getClientRects()).bind(function(e){return gC(t,e.left,e.top)}).bind(function(e){return eC((t=e,Zs.lastPositionIn(t).map(function(e){return tC(t,e).positions.concat(e)}).getOr([])),n);var t})},vC=function(t,n){return ne(n.getClientRects()).bind(function(e){return pC(t,e.left,e.top)}).bind(function(e){return eC((t=e,Zs.firstPositionIn(t).map(function(e){return[e].concat(nC(t,e).positions)}).getOr([])),n);var t})},bC=function(e,t){e.selection.setRng(t),yy(e,t)},yC=function(e,t,n){var r,o,i,a,u=e(t,n);return(a=u).breakType===fy.Wrap&&0===a.positions.length||!Bo.isBr(n.getNode())&&(i=u).breakType===fy.Br&&1===i.positions.length?(r=e,o=t,!u.breakAt.map(function(e){return r(o,e).breakAt.isSome()}).getOr(!1)):u.breakAt.isNone()},CC=d(yC,tC),xC=d(yC,nC),wC=function(e,t,n,r){var o,i,a,u,s=e.selection.getRng(),c=t?1:-1;if(ns()&&(o=t,i=s,a=n,u=vu.fromRangeStart(i),Zs.positionIn(!o,a).map(function(e){return e.isEqual(u)}).getOr(!1))){var l=Mm(c,e,n,!t,!0);return bC(e,l),!0}return!1},NC=function(e,t){var n=t.getNode(e);return Bo.isElement(n)&&"TABLE"===n.nodeName?A.some(n):A.none()},EC=function(u,s,c){var e=NC(!!s,c),t=!1===s;e.fold(function(){return bC(u,c.toRange())},function(a){return Zs.positionIn(t,u.getBody()).filter(function(e){return e.isEqual(c)}).fold(function(){return bC(u,c.toRange())},function(e){return n=s,o=a,t=c,void((i=eg(r=u))?r.undoManager.transact(function(){var e=or.fromTag(i);yr(e,tg(r)),Ai(e,or.fromTag("br")),n?ki(or.fromDom(o),e):Si(or.fromDom(o),e);var t=r.dom.createRng();t.setStart(e.dom(),0),t.setEnd(e.dom(),0),bC(r,t)}):bC(r,t.toRange()));var n,r,o,t,i})})},SC=function(e,t,n,r){var o,i,a,u,s,c,l=e.selection.getRng(),f=vu.fromRangeStart(l),d=e.getBody();if(!t&&CC(r,f)){var m=(u=d,hC(s=n,c=f).orThunk(function(){return te(c.getClientRects()).bind(function(e){return Zy(rC(u,vu.before(s)),e.left)})}).getOr(vu.before(s)));return EC(e,t,m),!0}return!(!t||!xC(r,f))&&(o=d,m=vC(i=n,a=f).orThunk(function(){return te(a.getClientRects()).bind(function(e){return Zy(oC(o,vu.after(i)),e.left)})}).getOr(vu.after(i)),EC(e,t,m),!0)},kC=function(t,n){return function(){return A.from(t.dom.getParent(t.selection.getNode(),"td,th")).bind(function(e){return A.from(t.dom.getParent(e,"table")).map(function(e){return wC(t,n,e)})}).getOr(!1)}},TC=function(n,r){return function(){return A.from(n.dom.getParent(n.selection.getNode(),"td,th")).bind(function(t){return A.from(n.dom.getParent(t,"table")).map(function(e){return SC(n,r,e,t)})}).getOr(!1)}},AC=function(e){return M(["figcaption"],sr(e))},RC=function(e){var t=H.document.createRange();return t.setStartBefore(e.dom()),t.setEndBefore(e.dom()),t},_C=function(e,t,n){n?Ai(e,t):Ti(e,t)},DC=function(e,t,n,r){return""===t?(l=e,f=r,d=or.fromTag("br"),_C(l,d,f),RC(d)):(o=e,i=r,a=t,u=n,s=or.fromTag(a),c=or.fromTag("br"),yr(s,u),Ai(s,c),_C(o,s,i),RC(c));var o,i,a,u,s,c,l,f,d},BC=function(e,t,n){return t?(o=e.dom(),nC(o,n).breakAt.isNone()):(r=e.dom(),tC(r,n).breakAt.isNone());var r,o},OC=function(t,n){var e,r,o,i=or.fromDom(t.getBody()),a=vu.fromRangeStart(t.selection.getRng()),u=eg(t),s=tg(t);return(e=a,r=i,o=d(Pr,r),Ki(or.fromDom(e.container()),lo,o).filter(AC)).exists(function(){if(BC(i,n,a)){var e=DC(i,u,s,n);return t.selection.setRng(e),!0}return!1})},PC=function(e,t){return function(){return!!e.selection.isCollapsed()&&OC(e,t)}},LC=function(e,r){return J(W(e,function(e){return Fb({shiftKey:!1,altKey:!1,ctrlKey:!1,metaKey:!1,keyCode:0,action:o},e)}),function(e){return t=e,(n=r).keyCode===t.keyCode&&n.shiftKey===t.shiftKey&&n.altKey===t.altKey&&n.ctrlKey===t.ctrlKey&&n.metaKey===t.metaKey?[e]:[];var t,n})},IC=function(e){for(var t=[],n=1;n'},QC=function(e,t){return e.nodeName===t||e.previousSibling&&e.previousSibling.nodeName===t},ZC=function(e,t){return t&&e.isBlock(t)&&!/^(TD|TH|CAPTION|FORM)$/.test(t.nodeName)&&!/^(fixed|absolute)/i.test(t.style.position)&&"true"!==e.getContentEditable(t)},ex=function(e,t,n){return!1===Bo.isText(t)?n:e?1===n&&t.data.charAt(n-1)===fa?0:n:n===t.data.length-1&&t.data.charAt(n)===fa?t.data.length:n},tx=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},nx=function(e,t){var n=eg(e);n&&n.toLowerCase()===t.tagName.toLowerCase()&&e.dom.setAttribs(t,tg(e))},rx=function(a,e){var t,u,s,i,c,n,r,o,l,f,d,m,g,p,h,v,b,y,C,x=a.dom,w=a.schema,N=w.getNonEmptyElements(),E=a.selection.getRng(),S=function(e){var t,n,r,o=s,i=w.getTextInlineElements();if(e||"TABLE"===f||"HR"===f?(t=x.create(e||m),nx(a,t)):t=c.cloneNode(!1),r=t,!1===og(a))x.setAttrib(t,"style",null),x.setAttrib(t,"class",null);else do{if(i[o.nodeName]){if(Uu(o)||cc(o))continue;n=o.cloneNode(!1),x.setAttrib(n,"id",""),t.hasChildNodes()?n.appendChild(t.firstChild):r=n,t.appendChild(n)}}while((o=o.parentNode)&&o!==u);return JC(r),t},k=function(e){var t,n,r,o;if(o=ex(e,s,i),Bo.isText(s)&&(e?0s.childNodes.length-1,s=s.childNodes[Math.min(i,s.childNodes.length-1)]||s,i=g&&Bo.isText(s)?s.nodeValue.length:0),(u=tx(x,s))&&((m&&!n||!m&&n)&&(s=function(e,t,n,r,o){var i,a,u,s,c,l,f,d=t||"P",m=e.dom,g=tx(m,r);if(!(a=m.getParent(r,m.isBlock))||!ZC(m,a)){if(l=(a=a||g)===e.getBody()||(f=a)&&/^(TD|TH|CAPTION)$/.test(f.nodeName)?a.nodeName.toLowerCase():a.parentNode.nodeName.toLowerCase(),!a.hasChildNodes())return i=m.create(d),nx(e,i),a.appendChild(i),n.setStart(i,0),n.setEnd(i,0),i;for(s=r;s.parentNode!==a;)s=s.parentNode;for(;s&&!m.isBlock(s);)s=(u=s).previousSibling;if(u&&e.schema.isValidChild(l,d.toLowerCase())){for(i=m.create(d),nx(e,i),u.parentNode.insertBefore(i,u),s=u;s&&!m.isBlock(s);)c=s.nextSibling,i.appendChild(s),s=c;n.setStart(r,o),n.setEnd(r,o)}}return r}(a,m,E,s,i)),c=x.getParent(s,x.isBlock),l=c?x.getParent(c.parentNode,x.isBlock):null,f=c?c.nodeName.toUpperCase():"","LI"!==(d=l?l.nodeName.toUpperCase():"")||e.ctrlKey||(l=(c=l).parentNode,f=d),/^(LI|DT|DD)$/.test(f)&&x.isEmpty(c)?GC(a,S,l,c,m):m&&c===a.getBody()||(m=m||"P",pa(c)?(r=Ea(c),x.isEmpty(c)&&JC(c),VC(a,r)):k()?T():k(!0)?(r=c.parentNode.insertBefore(S(),c),VC(a,QC(c,"HR")?r:c)):((t=(y=E,C=y.cloneRange(),C.setStart(y.startContainer,ex(!0,y.startContainer,y.startOffset)),C.setEnd(y.endContainer,ex(!1,y.endContainer,y.endOffset)),C).cloneRange()).setEndAfter(c),o=t.extractContents(),b=o,F(Hi(or.fromDom(b),fr),function(e){var t=e.dom();t.nodeValue=da(t.nodeValue)}),function(e){for(;Bo.isText(e)&&(e.nodeValue=e.nodeValue.replace(/^[\r\n]+/,"")),e=e.firstChild;);}(o),r=o.firstChild,x.insertAfter(o,c),function(e,t,n){var r,o=n,i=[];if(o){for(;o=o.firstChild;){if(e.isBlock(o))return;Bo.isElement(o)&&!t[o.nodeName.toLowerCase()]&&i.push(o)}for(r=i.length;r--;)!(o=i[r]).hasChildNodes()||o.firstChild===o.lastChild&&""===o.firstChild.nodeValue?e.remove(o):(a=e,(u=o)&&"A"===u.nodeName&&a.isEmpty(u)&&e.remove(o));var a,u}}(x,N,r),p=x,(h=c).normalize(),(v=h.lastChild)&&!/^(left|right)$/gi.test(p.getStyle(v,"float",!0))||p.add(h,"br"),x.isEmpty(c)&&JC(c),r.normalize(),x.isEmpty(r)?(x.remove(r),T()):VC(a,r)),x.setAttrib(r,"id",""),a.fire("NewBlock",{newBlock:r})))},ox=function(e,t){return jC(e).filter(function(e){return 0",Ym(f)!==f.documentBaseUrl&&(g+=''),g+='',d=Gm(f),m=Jm(f),Qm(f)&&(g+=''),g+='
    '),ow.add(t.iframeContainer,l),p},aw=function(e,t){var n=iw(e,t);t.editorContainer&&(ow.get(t.editorContainer).style.display=e.orgDisplay,e.hidden=ow.isHidden(t.editorContainer)),e.getElement().style.display="none",ow.setAttrib(e.id,"aria-hidden","true"),n||rw(e)},uw=hi.DOM,sw=function(t,n,e){var r=mh.get(e),o=mh.urls[e]||t.documentBaseUrl.replace(/\/$/,"");if(e=Gt.trim(e),r&&-1===Gt.inArray(n,e)){if(Gt.each(mh.dependencies(e),function(e){sw(t,n,e)}),t.plugins[e])return;try{var i=new r(t,o,t.$);(t.plugins[e]=i).init&&(i.init(t,o),n.push(e))}catch(jN){dh.pluginInitError(t,e,jN)}}},cw=function(e){return e.replace(/^\-/,"")},lw=function(e){return{editorContainer:e,iframeContainer:e}},fw=function(e){var t,n,r=e.getElement();return e.inline?lw(null):(t=r,n=uw.create("div"),uw.insertAfter(n,t),lw(n))},dw=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.getElement();return e.orgDisplay=m.style.display,R(d.theme)?(l=(o=e).settings,f=o.getElement(),i=l.width||uw.getStyle(f,"width")||"100%",a=l.height||uw.getStyle(f,"height")||f.offsetHeight,u=l.min_height||100,(s=/^[0-9\.]+(|px)$/i).test(""+i)&&(i=Math.max(parseInt(i,10),100)),s.test(""+a)&&(a=Math.max(parseInt(a,10),u)),c=o.theme.renderUI({targetNode:f,width:i,height:a,deltaWidth:l.delta_width,deltaHeight:l.delta_height}),l.content_editable||(a=(c.iframeHeight||a)+("number"==typeof a?c.deltaHeight||0:""))=n.length)for(r=0,o=e.length;r=n.length||e[r]!==n[r]){i=r+1;break}if(e.length=e.length||e[r]!==n[r]){i=r+1;break}if(1===i)return t;for(r=0,o=e.length-(i-1);r]*>( | |\\s|\xa0|
    |)<\\/"+a+">[\r\n]*|
    [\r\n]*)$"),r=i.replace(u,"")}return"text"===t.format||Co(or.fromDom(n))?t.content=r:t.content=Gt.trim(r),t.no_events||e.fire("GetContent",t),t.content},Sw=function(e,t){t(e),e.firstChild&&Sw(e.firstChild,t),e.next&&Sw(e.next,t)},kw=function(e,t,n){var r=function(e,n,t){var r={},o={},i=[];for(var a in t.firstChild&&Sw(t.firstChild,function(t){F(e,function(e){e.name===t.name&&(r[e.name]?r[e.name].nodes.push(t):r[e.name]={filter:e,nodes:[t]})}),F(n,function(e){"string"==typeof t.attr(e.name)&&(o[e.name]?o[e.name].nodes.push(t):o[e.name]={filter:e,nodes:[t]})})}),r)r.hasOwnProperty(a)&&i.push(r[a]);for(var a in o)o.hasOwnProperty(a)&&i.push(o[a]);return i}(e,t,n);F(r,function(t){F(t.filter.callbacks,function(e){e(t.nodes,t.filter.name,{})})})},Tw=function(e){return e instanceof Kb},Aw=function(e,t){var r;e.dom.setHTML(e.getBody(),t),Kp(r=e)&&Zs.firstPositionIn(r.getBody()).each(function(e){var t=e.getNode(),n=Bo.isTable(t)?Zs.firstPositionIn(t).getOr(e):e;r.selection.setRng(n.toRange())})},Rw=function(u,s,c){return void 0===c&&(c={}),c.format=c.format?c.format:"html",c.set=!0,c.content=Tw(s)?"":s,Tw(s)||c.no_events||(u.fire("BeforeSetContent",c),s=c.content),A.from(u.getBody()).fold(q(s),function(e){return Tw(s)?function(e,t,n,r){kw(e.parser.getNodeFilters(),e.parser.getAttributeFilters(),n);var o=Jc({validate:e.validate},e.schema).serialize(n);return r.content=Co(or.fromDom(t))?o:Gt.trim(o),Aw(e,r.content),r.no_events||e.fire("SetContent",r),n}(u,e,s,c):(t=u,n=e,o=c,0===(r=s).length||/^\s+$/.test(r)?(a='
    ',"TABLE"===n.nodeName?r=""+a+"":/^(UL|OL)$/.test(n.nodeName)&&(r="
  • "+a+"
  • "),(i=eg(t))&&t.schema.isValidChild(n.nodeName.toLowerCase(),i.toLowerCase())?(r=a,r=t.dom.createHTML(i,t.settings.forced_root_block_attrs,r)):r||(r='
    '),Aw(t,r),t.fire("SetContent",o)):("raw"!==o.format&&(r=Jc({validate:t.validate},t.schema).serialize(t.parser.parse(r,{isRootContent:!0,insert:!0}))),o.content=Co(or.fromDom(n))?r:Gt.trim(r),Aw(t,o.content),o.no_events||t.fire("SetContent",o)),o.content);var t,n,r,o,i,a})},_w=hi.DOM,Dw=function(e){return A.from(e).each(function(e){return e.destroy()})},Bw=function(e){if(!e.removed){var t=e._selectionOverrides,n=e.editorUpload,r=e.getBody(),o=e.getElement();r&&e.save({is_removing:!0}),e.removed=!0,e.unbindAllNativeEvents(),e.hasHiddenInput&&o&&_w.remove(o.nextSibling),bp(e),e.editorManager.remove(e),!e.inline&&r&&(i=e,_w.setStyle(i.id,"display",i.orgDisplay)),yp(e),_w.remove(e.getContainer()),Dw(t),Dw(n),e.destroy()}var i},Ow=function(e,t){var n,r,o,i=e.selection,a=e.dom;e.destroyed||(t||e.removed?(t||(e.editorManager.off("beforeunload",e._beforeUnload),e.theme&&e.theme.destroy&&e.theme.destroy(),Dw(i),Dw(a)),(r=(n=e).formElement)&&(r._mceOldSubmit&&(r.submit=r._mceOldSubmit,r._mceOldSubmit=null),_w.unbind(r,"submit reset",n.formEventDelegate)),(o=e).contentAreaContainer=o.formElement=o.container=o.editorContainer=null,o.bodyElement=o.contentDocument=o.contentWindow=null,o.iframeElement=o.targetElm=null,o.selection&&(o.selection=o.selection.win=o.selection.dom=o.selection.dom.doc=null),e.destroyed=!0):e.remove())},Pw=hi.DOM,Lw=Gt.extend,Iw=Gt.each,Mw=Gt.resolve,Fw=me.ie,zw=function(e,t,n){var r,o,i,a,u,s,c,l=this,f=l.documentBaseUrl=n.documentBaseURL,d=n.baseURI;r=l,o=e,i=f,a=n.defaultSettings,u=t,c={id:o,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:i,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,render_ui:!0,indentation:"40px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",entity_encoding:"named",url_converter:(s=r).convertURL,url_converter_scope:s,ie7_compat:!0},t=ml(al,c,a,u),l.settings=t,Ei.language=t.language||"en",Ei.languageLoad=t.language_load,Ei.baseURL=n.baseURL,l.id=e,l.setDirty(!1),l.plugins={},l.documentBaseURI=new Nw(t.document_base_url,{base_uri:d}),l.baseURI=d,l.contentCSS=[],l.contentStyles=[],l.shortcuts=new Mp(l),l.loadedCSS={},l.editorCommands=new lp(l),l.suffix=n.suffix,l.editorManager=n,l.inline=t.inline,l.buttons={},l.menuItems={},t.cache_suffix&&(me.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),!1===t.override_viewport&&(me.overrideViewPort=!1),n.fire("SetupEditor",{editor:l}),l.execCallback("setup",l),l.$=hn.overrideDefaults(function(){return{context:l.inline?l.getBody():l.getDoc(),element:l.getBody()}})};Lw(zw.prototype={render:function(){vw(this)},focus:function(e){Wp(this,e)},hasFocus:function(){return Kp(this)},execCallback:function(e){for(var t=[],n=1;n=n.x&&o.x+o.w<=n.w+n.x&&o.y>=n.y&&o.y+o.h<=n.h+n.y)return r[i];return null},intersect:function(e,t){var n,r,o,i;return n=hN(e.x,t.x),r=hN(e.y,t.y),o=pN(e.x+e.w,t.x+t.w),i=pN(e.y+e.h,t.y+t.h),o-n<0||i-r<0?null:yN(n,r,o-n,i-r)},clamp:function(e,t,n){var r,o,i,a,u,s,c,l,f,d;return u=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h,f=t.x+t.w,d=t.y+t.h,r=hN(0,t.x-u),o=hN(0,t.y-s),i=hN(0,c-f),a=hN(0,l-d),u+=r,s+=o,n&&(c+=r,l+=o,u-=i,s-=a),yN(u,s,(c-=i)-u,(l-=a)-s)},create:yN,fromClientRect:function(e){return yN(e.left,e.top,e.width,e.height)}},xN={},wN={add:function(e,t){xN[e.toLowerCase()]=t},has:function(e){return!!xN[e.toLowerCase()]},get:function(e){var t=e.toLowerCase(),n=xN.hasOwnProperty(t)?xN[t]:null;if(null===n)throw new Error("Could not find module for type: "+e);return n},create:function(e,t){var n;if("string"==typeof e?(t=t||{}).type=e:e=(t=e).type,e=e.toLowerCase(),!(n=xN[e]))throw new Error("Could not find control by type: "+e);return(n=new n(t)).type=e,n}},NN=Gt.each,EN=Gt.extend,SN=function(){};SN.extend=dN=function(n){var e,t,r,o=this.prototype,i=function(){var e,t,n;if(!mN&&(this.init&&this.init.apply(this,arguments),t=this.Mixins))for(e=t.length;e--;)(n=t[e]).init&&n.init.apply(this,arguments)},a=function(){return this},u=function(n,r){return function(){var e,t=this._super;return this._super=o[n],e=r.apply(this,arguments),this._super=t,e}};for(t in mN=!0,e=new this,mN=!1,n.Mixins&&(NN(n.Mixins,function(e){for(var t in e)"init"!==t&&(n[t]=e[t])}),o.Mixins&&(n.Mixins=o.Mixins.concat(n.Mixins))),n.Methods&&NN(n.Methods.split(","),function(e){n[e]=a}),n.Properties&&NN(n.Properties.split(","),function(e){var t="_"+e;n[e]=function(e){return e!==undefined?(this[t]=e,this):this[t]}}),n.Statics&&NN(n.Statics,function(e,t){i[t]=e}),n.Defaults&&o.Defaults&&(n.Defaults=EN({},o.Defaults,n.Defaults)),n)"function"==typeof(r=n[t])&&o[t]?e[t]=u(t,r):e[t]=r;return i.prototype=e,(i.constructor=i).extend=dN,i};var kN=Math.min,TN=Math.max,AN=Math.round,RN=function(e,n){var r,o,t,i;if(n=n||'"',null===e)return"null";if("string"==(t=typeof e))return o="\bb\tt\nn\ff\rr\"\"''\\\\",n+e.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g,function(e,t){return'"'===n&&"'"===e?e:(r=o.indexOf(t))+1?"\\"+o.charAt(r+1):(e=t.charCodeAt().toString(16),"\\u"+"0000".substring(e.length)+e)})+n;if("object"===t){if(e.hasOwnProperty&&"[object Array]"===Object.prototype.toString.call(e)){for(r=0,o="[";r + @stop @section('body-class', 'flexbox') From 6428f324839eaea1c897dc1b8e1fc9af5e5fecf3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 21 Apr 2019 14:11:49 +0100 Subject: [PATCH 146/160] Prevented invalid form inputs having incorrect padding --- resources/assets/sass/_forms.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss index f40c92a19..48f4a902e 100644 --- a/resources/assets/sass/_forms.scss +++ b/resources/assets/sass/_forms.scss @@ -251,7 +251,7 @@ input[type=date] { } .form-group { - .text-pos, .text-neg { + div.text-pos, div.text-neg, p.text-post, p.text-neg { padding: $-xs 0; } } From aeb1fc4d49b0641bd585a8b62bb553979e101bb2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 21 Apr 2019 15:52:29 +0100 Subject: [PATCH 147/160] Started rewriting back-end image managment --- app/Auth/UserRepo.php | 2 +- app/Http/Controllers/ImageController.php | 111 +++++++++++------- app/Uploads/ImageRepo.php | 33 ++++-- ...55_set_user_profile_images_uploaded_to.php | 36 ++++++ resources/assets/js/vues/image-manager.js | 4 +- resources/views/users/edit.blade.php | 2 +- routes/web.php | 19 ++- tests/Uploads/ImageTest.php | 97 +++++++++++++-- 8 files changed, 236 insertions(+), 68 deletions(-) create mode 100644 database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php diff --git a/app/Auth/UserRepo.php b/app/Auth/UserRepo.php index 1a73d0072..94ebb27ab 100644 --- a/app/Auth/UserRepo.php +++ b/app/Auth/UserRepo.php @@ -198,7 +198,7 @@ class UserRepo $user->delete(); // Delete user profile images - $profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get(); + $profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get(); foreach ($profileImages as $image) { Images::destroy($image); } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index 4d6f759b3..ae2d74305 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -45,13 +45,21 @@ class ImageController extends Controller /** * Get all images for a specific type, Paginated + * @param Request $request * @param string $type * @param int $page * @return \Illuminate\Http\JsonResponse */ - public function getAllByType($type, $page = 0) + public function getAllByType(Request $request, $type, $page = 0) { - $imgData = $this->imageRepo->getPaginatedByType($type, $page); + $uploadedToFilter = $request->get('uploaded_to', null); + + // For user profile request, check access to user images + if ($type === 'user') { + $this->checkPermissionOrCurrentUser('users-manage', $uploadedToFilter ?? 0); + } + + $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, $uploadedToFilter); return response()->json($imgData); } @@ -73,17 +81,6 @@ class ImageController extends Controller return response()->json($imgData); } - /** - * Get all images for a user. - * @param int $page - * @return \Illuminate\Http\JsonResponse - */ - public function getAllForUserType($page = 0) - { - $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id); - return response()->json($imgData); - } - /** * Get gallery images with a specific filter such as book or page * @param $filter @@ -94,7 +91,7 @@ class ImageController extends Controller public function getGalleryFiltered(Request $request, $filter, $page = 0) { $this->validate($request, [ - 'page_id' => 'required|integer' + 'uploaded_to' => 'required|integer' ]); $validFilters = collect(['page', 'book']); @@ -102,12 +99,57 @@ class ImageController extends Controller return response('Invalid filter', 500); } - $pageId = $request->get('page_id'); + $pageId = $request->get('uploaded_to'); $imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24); return response()->json($imgData); } + public function uploadGalleryImage(Request $request) + { + // TODO + } + + public function uploadUserImage(Request $request) + { + // TODO + } + + public function uploadSystemImage(Request $request) + { + // TODO + } + + public function uploadCoverImage(Request $request) + { + // TODO + } + + /** + * Upload a draw.io image into the system. + * @param Request $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + */ + public function uploadDrawioImage(Request $request) + { + $this->validate($request, [ + 'image' => 'required|string', + 'uploaded_to' => 'required|integer' + ]); + $uploadedTo = $request->get('uploaded_to', 0); + $page = $this-> + $this->checkPermission('image-create-all'); + $imageBase64Data = $request->get('image'); + + try { + $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + /** * Handles image uploads for use on pages. * @param string $type @@ -130,6 +172,12 @@ class ImageController extends Controller try { $uploadedTo = $request->get('uploaded_to', 0); + + // For user profile request, check access to user images + if ($type === 'user') { + $this->checkPermissionOrCurrentUser('users-manage', $uploadedTo ?? 0); + } + $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); @@ -137,31 +185,6 @@ class ImageController extends Controller return response()->json($image); } - - /** - * Upload a drawing to the system. - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function uploadDrawing(Request $request) - { - $this->validate($request, [ - 'image' => 'required|string', - 'uploaded_to' => 'required|integer' - ]); - $this->checkPermission('image-create-all'); - $imageBase64Data = $request->get('image'); - - try { - $uploadedTo = $request->get('uploaded_to', 0); - $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - /** * Get the content of an image based64 encoded. * @param $id @@ -199,19 +222,21 @@ class ImageController extends Controller /** * Update image details - * @param integer $imageId + * @param integer $id * @param Request $request * @return \Illuminate\Http\JsonResponse * @throws ImageUploadException * @throws \Exception */ - public function update($imageId, Request $request) + public function update($id, Request $request) { $this->validate($request, [ 'name' => 'required|min:2|string' ]); - $image = $this->imageRepo->getById($imageId); + + $image = $this->imageRepo->getById($id); $this->checkOwnablePermission('image-update', $image); + $image = $this->imageRepo->updateImageDetails($image, $request->all()); return response()->json($image); } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 0ef8cad48..c13d995bd 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -2,6 +2,7 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Page; +use BookStack\Http\Requests\Request; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageRepo @@ -44,12 +45,12 @@ class ImageRepo * @param $query * @param int $page * @param int $pageSize + * @param bool $filterOnPage * @return array */ private function returnPaginated($query, $page = 0, $pageSize = 24) { - $images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to'); - $images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); + $images = $query->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); $hasMore = count($images) > $pageSize; $returnImages = $images->take(24); @@ -68,15 +69,20 @@ class ImageRepo * @param string $type * @param int $page * @param int $pageSize - * @param bool|int $userFilter + * @param int $uploadedTo * @return array */ - public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false) + public function getPaginatedByType(string $type, int $page = 0, int $pageSize = 24, int $uploadedTo = null) { - $images = $this->image->where('type', '=', strtolower($type)); + $images = $this->image->newQuery()->where('type', '=', strtolower($type)); - if ($userFilter !== false) { - $images = $images->where('created_by', '=', $userFilter); + if ($uploadedTo !== null) { + $images = $images->where('uploaded_to', '=', $uploadedTo); + } + + // Filter by page access if gallery + if ($type === 'gallery') { + $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); } return $this->returnPaginated($images, $page, $pageSize); @@ -90,9 +96,17 @@ class ImageRepo * @param string $searchTerm * @return array */ - public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24) + public function searchPaginatedByType(Request $request, $type, $searchTerm, $page = 0, $pageSize = 24) { - $images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%'); + // TODO - Filter by uploaded_to + $images = $this->image->newQuery() + ->where('type', '=', strtolower($type)) + ->where('name', 'LIKE', '%' . $searchTerm . '%'); + + if ($type === 'gallery') { + $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); + } + return $this->returnPaginated($images, $page, $pageSize); } @@ -118,6 +132,7 @@ class ImageRepo $images = $images->whereIn('uploaded_to', $validPageIds); } + $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); return $this->returnPaginated($images, $pageNum, $pageSize); } diff --git a/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php b/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php new file mode 100644 index 000000000..f1d00b3e5 --- /dev/null +++ b/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php @@ -0,0 +1,36 @@ +where('type', '=', 'user') + ->update([ + 'uploaded_to' => DB::raw('`created_by`') + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('images') + ->where('type', '=', 'user') + ->update([ + 'uploaded_to' => 0 + ]); + } +} diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js index 6bfc2662d..843aae378 100644 --- a/resources/assets/js/vues/image-manager.js +++ b/resources/assets/js/vues/image-manager.js @@ -59,7 +59,7 @@ const methods = { fetchData() { let url = baseUrl + page; let query = {}; - if (this.uploadedTo !== false) query.page_id = this.uploadedTo; + if (this.uploadedTo !== false) query.uploaded_to = this.uploadedTo; if (this.searching) query.term = this.searchTerm; this.$http.get(url, {params: query}).then(response => { @@ -133,7 +133,7 @@ const methods = { }, saveImageDetails() { - let url = window.baseUrl(`/images/update/${this.selectedImage.id}`); + let url = window.baseUrl(`/images/${this.selectedImage.id}`); this.$http.put(url, this.selectedImage).then(response => { this.$events.emit('success', trans('components.image_update_success')); }).catch(error => { diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index b9ad052c7..7c8175d9a 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -87,5 +87,5 @@ @endif
    - @include('components.image-manager', ['imageType' => 'user']) + @include('components.image-manager', ['imageType' => 'user', 'uploaded_to' => $user->id]) @stop diff --git a/routes/web.php b/routes/web.php index 695f61654..933179aa7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -105,19 +105,28 @@ Route::group(['middleware' => 'auth'], function () { // Image routes Route::group(['prefix' => 'images'], function() { // Get for user images - Route::get('/user/all', 'ImageController@getAllForUserType'); - Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); +// Route::get('/user/all', 'ImageController@getAllForUserType'); +// Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); // Standard get, update and deletion for all types Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); Route::get('/base64/{id}', 'ImageController@getBase64Image'); - Route::put('/update/{imageId}', 'ImageController@update'); - Route::post('/drawing/upload', 'ImageController@uploadDrawing'); Route::get('/usage/{id}', 'ImageController@usage'); - Route::post('/{type}/upload', 'ImageController@uploadByType'); Route::get('/{type}/all', 'ImageController@getAllByType'); Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); Route::get('/{type}/search/{page}', 'ImageController@searchByType'); Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); + + // TODO - Remove use of abstract "Type" variable (Above) + // TODO - Clearly define each endpoint so logic for each is clear + // TODO - Move into per-type controllers + // TODO - Test and fully think about permissions and each stage + Route::post('/drawio', 'ImageController@uploadDrawioImage'); + Route::post('/gallery', 'ImageController@uploadGalleryImage'); + Route::post('/user', 'ImageController@uploadUserImage'); + Route::post('/system', 'ImageController@uploadSystemImage'); + Route::post('/cover', 'ImageController@uploadCoverImage'); + + Route::put('/{id}', 'ImageController@update'); Route::delete('/{id}', 'ImageController@destroy'); }); diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 8373a809c..c2e21f95c 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -217,18 +217,97 @@ class ImageTest extends TestCase $this->assertTrue($testImageData === $uploadedImageData, "Uploaded image file data does not match our test image as expected"); } + protected function getTestProfileImage() + { + $imageName = 'profile.png'; + $relPath = $this->getTestImagePath('user', $imageName); + $this->deleteImage($relPath); + + return $this->getTestImage($imageName); + } + + public function test_user_image_upload() + { + $editor = $this->getEditor(); + $admin = $this->getAdmin(); + $this->actingAs($admin); + + $file = $this->getTestProfileImage(); + $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); + + $this->assertDatabaseHas('images', [ + 'type' => 'user', + 'uploaded_to' => $editor->id, + 'created_by' => $admin->id, + ]); + } + + public function test_standard_user_with_manage_users_permission_can_view_other_profile_images() + { + $editor = $this->getEditor(); + $this->giveUserPermissions($editor, ['users-manage']); + + $admin = $this->getAdmin(); + + $this->actingAs($admin); + $file = $this->getTestProfileImage(); + $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); + + $expectedJson = [ + 'name' => 'profile.png', + 'uploaded_to' => $admin->id, + 'type' => 'user' + ]; + + $this->actingAs($editor); + $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id); + $adminImagesGet->assertStatus(200)->assertJsonFragment($expectedJson); + + $allImagesGet = $this->get("/images/user/all/0"); + $allImagesGet->assertStatus(200)->assertJsonFragment($expectedJson); + } + + public function test_standard_user_cant_view_other_profile_images() + { + $editor = $this->getEditor(); + $admin = $this->getAdmin(); + + $this->actingAs($admin); + $file = $this->getTestProfileImage(); + $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); + + $this->actingAs($editor); + $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id); + $adminImagesGet->assertStatus(302); + + $allImagesGet = $this->get("/images/user/all/0"); + $allImagesGet->assertStatus(302); + } + + public function test_standard_user_cant_upload_other_profile_images() + { + $editor = $this->getEditor(); + $admin = $this->getAdmin(); + + $this->actingAs($editor); + $file = $this->getTestProfileImage(); + $upload = $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); + $upload->assertStatus(302); + + $this->assertDatabaseMissing('images', [ + 'type' => 'user', + 'uploaded_to' => $admin->id, + ]); + } + public function test_user_images_deleted_on_user_deletion() { $editor = $this->getEditor(); $this->actingAs($editor); - $imageName = 'profile.png'; - $relPath = $this->getTestImagePath('gallery', $imageName); - $this->deleteImage($relPath); - - $file = $this->getTestImage($imageName); - $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []); - $this->call('POST', '/images/user/upload', [], [], ['file' => $file], []); + $file = $this->getTestProfileImage(); + $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); + $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); $this->assertTrue($profileImages->count() === 2, "Found profile images does not match upload count"); @@ -239,6 +318,10 @@ class ImageTest extends TestCase 'type' => 'user', 'created_by' => $editor->id ]); + $this->assertDatabaseMissing('images', [ + 'type' => 'user', + 'uploaded_to' => $editor->id + ]); } public function test_deleted_unused_images() From a87ae1601061322e7e7b2dc11658f56467761787 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 27 Apr 2019 14:18:00 +0100 Subject: [PATCH 148/160] Started extraction of image controller to separate controllers --- app/Auth/Permissions/PermissionService.php | 11 +- app/Http/Controllers/ImageController.php | 29 ----- .../Images/CoverImageController.php | 95 ++++++++++++++ .../Images/DrawioImageController.php | 67 ++++++++++ .../Images/GalleryImageController.php | 65 ++++++++++ .../Images/SystemImageController.php | 64 +++++++++ .../Images/UserImageController.php | 70 ++++++++++ app/Http/Controllers/UserController.php | 16 +-- app/Uploads/ImageRepo.php | 122 +++++++++++------- ...55_set_user_profile_images_uploaded_to.php | 15 +++ .../assets/js/components/wysiwyg-editor.js | 35 +++-- resources/assets/js/services/drawio.js | 11 +- resources/assets/js/vues/image-manager.js | 96 +++++++------- resources/views/books/edit.blade.php | 2 +- .../views/components/image-manager.blade.php | 12 +- resources/views/shelves/edit.blade.php | 2 +- routes/web.php | 32 +++-- 17 files changed, 571 insertions(+), 173 deletions(-) create mode 100644 app/Http/Controllers/Images/CoverImageController.php create mode 100644 app/Http/Controllers/Images/DrawioImageController.php create mode 100644 app/Http/Controllers/Images/GalleryImageController.php create mode 100644 app/Http/Controllers/Images/SystemImageController.php create mode 100644 app/Http/Controllers/Images/UserImageController.php diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index 7e710edaf..a5ab4ea9a 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -732,18 +732,21 @@ class PermissionService } /** - * Filters pages that are a direct relation to another item. + * Add conditions to a query to filter the selection to related entities + * where permissions are granted. + * @param $entityType * @param $query * @param $tableName * @param $entityIdColumn * @return mixed */ - public function filterRelatedPages($query, $tableName, $entityIdColumn) + public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn) { $this->currentAction = 'view'; $tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn]; - $pageMorphClass = $this->entityProvider->page->getMorphClass(); + $pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass(); + $q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) { $query->where(function ($query) use (&$tableDetails, $pageMorphClass) { $query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) { @@ -761,7 +764,9 @@ class PermissionService }); })->orWhere($tableDetails['entityIdColumn'], '=', 0); }); + $this->clean(); + return $q; } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php index ae2d74305..df7758176 100644 --- a/app/Http/Controllers/ImageController.php +++ b/app/Http/Controllers/ImageController.php @@ -81,35 +81,6 @@ class ImageController extends Controller return response()->json($imgData); } - /** - * Get gallery images with a specific filter such as book or page - * @param $filter - * @param int $page - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function getGalleryFiltered(Request $request, $filter, $page = 0) - { - $this->validate($request, [ - 'uploaded_to' => 'required|integer' - ]); - - $validFilters = collect(['page', 'book']); - if (!$validFilters->contains($filter)) { - return response('Invalid filter', 500); - } - - $pageId = $request->get('uploaded_to'); - $imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24); - - return response()->json($imgData); - } - - public function uploadGalleryImage(Request $request) - { - // TODO - } - public function uploadUserImage(Request $request) { // TODO diff --git a/app/Http/Controllers/Images/CoverImageController.php b/app/Http/Controllers/Images/CoverImageController.php new file mode 100644 index 000000000..807fddc38 --- /dev/null +++ b/app/Http/Controllers/Images/CoverImageController.php @@ -0,0 +1,95 @@ +imageRepo = $imageRepo; + $this->entityRepo = $entityRepo; + + parent::__construct(); + } + + /** + * Get a list of cover images, in a list. + * Can be paged and filtered by entity. + * @param Request $request + * @param string $entity + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request, $entity) + { + if (!$this->isValidEntityTypeForCover($entity)) { + return $this->jsonError(trans('errors.image_upload_type_error')); + } + + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + + $type = 'cover_' . $entity; + $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, null, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new cover image in the system. + * @param Request $request + * @param string $entity + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request, $entity) + { + $this->checkPermission('image-create-all'); + $this->validate($request, [ + 'file' => $this->imageRepo->getImageValidationRules(), + 'uploaded_to' => 'required|integer' + ]); + + if (!$this->isValidEntityTypeForCover($entity)) { + return $this->jsonError(trans('errors.image_upload_type_error')); + } + + $uploadedTo = $request->get('uploaded_to', 0); + $entityInstance = $this->entityRepo->getById($entity, $uploadedTo); + $this->checkOwnablePermission($entity . '-update', $entityInstance); + + try { + $type = 'cover_' . $entity; + $imageUpload = $request->file('file'); + $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + + /** + * Check if the given entity type is valid entity to have cover images. + * @param string $entityType + * @return bool + */ + protected function isValidEntityTypeForCover(string $entityType) + { + return ($entityType === 'book' || $entityType === 'bookshelf'); + } + +} diff --git a/app/Http/Controllers/Images/DrawioImageController.php b/app/Http/Controllers/Images/DrawioImageController.php new file mode 100644 index 000000000..eb0e32827 --- /dev/null +++ b/app/Http/Controllers/Images/DrawioImageController.php @@ -0,0 +1,67 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of gallery images, in a list. + * Can be paged and filtered by entity. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + $uploadedToFilter = $request->get('uploaded_to', null); + $parentTypeFilter = $request->get('filter_type', null); + + $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new gallery image in the system. + * @param Request $request + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request) + { + $this->validate($request, [ + 'image' => 'required|string', + 'uploaded_to' => 'required|integer' + ]); + + $this->checkPermission('image-create-all'); + $imageBase64Data = $request->get('image'); + + try { + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + +} diff --git a/app/Http/Controllers/Images/GalleryImageController.php b/app/Http/Controllers/Images/GalleryImageController.php new file mode 100644 index 000000000..35087463b --- /dev/null +++ b/app/Http/Controllers/Images/GalleryImageController.php @@ -0,0 +1,65 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of gallery images, in a list. + * Can be paged and filtered by entity. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + $uploadedToFilter = $request->get('uploaded_to', null); + $parentTypeFilter = $request->get('filter_type', null); + + $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new gallery image in the system. + * @param Request $request + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request) + { + $this->checkPermission('image-create-all'); + $this->validate($request, [ + 'file' => $this->imageRepo->getImageValidationRules() + ]); + + try { + $imageUpload = $request->file('file'); + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + +} diff --git a/app/Http/Controllers/Images/SystemImageController.php b/app/Http/Controllers/Images/SystemImageController.php new file mode 100644 index 000000000..1c4de2f70 --- /dev/null +++ b/app/Http/Controllers/Images/SystemImageController.php @@ -0,0 +1,64 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of system images, in a list. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $this->checkPermission('settings-manage'); + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + + $imgData = $this->imageRepo->getPaginatedByType('system', $page, 24, null, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new system image. + * @param Request $request + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request) + { + $this->checkPermission('image-create-all'); + $this->checkPermission('settings-manage'); + + $this->validate($request, [ + 'file' => $this->imageRepo->getImageValidationRules() + ]); + + try { + $imageUpload = $request->file('file'); + $image = $this->imageRepo->saveNew($imageUpload, 'system', 0); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + +} diff --git a/app/Http/Controllers/Images/UserImageController.php b/app/Http/Controllers/Images/UserImageController.php new file mode 100644 index 000000000..492d867e5 --- /dev/null +++ b/app/Http/Controllers/Images/UserImageController.php @@ -0,0 +1,70 @@ +imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Get a list of user profile images, in a list. + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function list(Request $request) + { + $page = $request->get('page', 1); + $searchTerm = $request->get('search', null); + $userId = $request->get('uploaded_to', null); + + $this->checkPermissionOrCurrentUser('users-manage', $userId); + + $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $userId, $searchTerm); + return response()->json($imgData); + } + + /** + * Store a new user profile image in the system. + * @param Request $request + * @return Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function create(Request $request) + { + $this->checkPermission('image-create-all'); + + $this->validate($request, [ + 'uploaded_to' => 'required|integer', + 'file' => $this->imageRepo->getImageValidationRules() + ]); + + $userId = $request->get('uploaded_to', null); + $this->checkPermissionOrCurrentUser('users-manage', $userId); + + try { + $imageUpload = $request->file('file'); + $uploadedTo = $request->get('uploaded_to', 0); + $image = $this->imageRepo->saveNew($imageUpload, 'user', $uploadedTo); + } catch (ImageUploadException $e) { + return response($e->getMessage(), 500); + } + + return response()->json($image); + } + +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1bb5d46cd..a93e8d9c9 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -107,9 +107,7 @@ class UserController extends Controller */ public function edit($id, SocialAuthService $socialAuthService) { - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->user->findOrFail($id); @@ -131,9 +129,7 @@ class UserController extends Controller public function update(Request $request, $id) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $this->validate($request, [ 'name' => 'min:2', @@ -184,9 +180,7 @@ class UserController extends Controller */ public function delete($id) { - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->userRepo->getById($id); $this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name])); @@ -202,9 +196,7 @@ class UserController extends Controller public function destroy($id) { $this->preventAccessForDemoUsers(); - $this->checkPermissionOr('users-manage', function () use ($id) { - return $this->currentUser->id == $id; - }); + $this->checkPermissionOrCurrentUser('users-manage', $id); $user = $this->userRepo->getById($id); diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index c13d995bd..235889eee 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -3,6 +3,7 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Page; use BookStack\Http\Requests\Request; +use Illuminate\Database\Eloquent\Builder; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageRepo @@ -20,7 +21,12 @@ class ImageRepo * @param \BookStack\Auth\Permissions\PermissionService $permissionService * @param \BookStack\Entities\Page $page */ - public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page) + public function __construct( + Image $image, + ImageService $imageService, + PermissionService $permissionService, + Page $page + ) { $this->image = $image; $this->imageService = $imageService; @@ -48,92 +54,104 @@ class ImageRepo * @param bool $filterOnPage * @return array */ - private function returnPaginated($query, $page = 0, $pageSize = 24) + private function returnPaginated($query, $page = 1, $pageSize = 24) { - $images = $query->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get(); + $images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get(); $hasMore = count($images) > $pageSize; - $returnImages = $images->take(24); + $returnImages = $images->take($pageSize); $returnImages->each(function ($image) { $this->loadThumbs($image); }); return [ 'images' => $returnImages, - 'hasMore' => $hasMore + 'has_more' => $hasMore ]; } /** - * Gets a load images paginated, filtered by image type. + * Fetch a list of images in a paginated format, filtered by image type. + * Can be filtered by uploaded to and also by name. * @param string $type * @param int $page * @param int $pageSize * @param int $uploadedTo + * @param string|null $search + * @param callable|null $whereClause * @return array */ - public function getPaginatedByType(string $type, int $page = 0, int $pageSize = 24, int $uploadedTo = null) + public function getPaginatedByType( + string $type, + int $page = 0, + int $pageSize = 24, + int $uploadedTo = null, + string $search = null, + callable $whereClause = null + ) { - $images = $this->image->newQuery()->where('type', '=', strtolower($type)); + $imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type)); if ($uploadedTo !== null) { - $images = $images->where('uploaded_to', '=', $uploadedTo); + $imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo); + } + + if ($search !== null) { + $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%'); } // Filter by page access if gallery if ($type === 'gallery') { - $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); + $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); } - return $this->returnPaginated($images, $page, $pageSize); + // Filter by entity if cover + if (strpos($type, 'cover_') === 0) { + $entityType = explode('_', $type)[1]; + $imageQuery = $this->restrictionService->filterRelatedEntity($entityType, $imageQuery, 'images', 'uploaded_to'); + } + + if ($whereClause !== null) { + $imageQuery = $imageQuery->where($whereClause); + } + + return $this->returnPaginated($imageQuery, $page, $pageSize); } /** - * Search for images by query, of a particular type. + * Get paginated gallery images within a specific page or book. * @param string $type + * @param string $filterType * @param int $page * @param int $pageSize - * @param string $searchTerm + * @param int|null $uploadedTo + * @param string|null $search * @return array */ - public function searchPaginatedByType(Request $request, $type, $searchTerm, $page = 0, $pageSize = 24) + public function getEntityFiltered( + string $type, + string $filterType = null, + int $page = 0, + int $pageSize = 24, + int $uploadedTo = null, + string $search = null + ) { - // TODO - Filter by uploaded_to - $images = $this->image->newQuery() - ->where('type', '=', strtolower($type)) - ->where('name', 'LIKE', '%' . $searchTerm . '%'); + $contextPage = $this->page->findOrFail($uploadedTo); + $parentFilter = null; - if ($type === 'gallery') { - $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); + if ($filterType === 'book' || $filterType === 'page') { + $parentFilter = function(Builder $query) use ($filterType, $contextPage) { + if ($filterType === 'page') { + $query->where('uploaded_to', '=', $contextPage->id); + } elseif ($filterType === 'book') { + $validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray(); + $query->whereIn('uploaded_to', $validPageIds); + } + }; } - return $this->returnPaginated($images, $page, $pageSize); - } - - /** - * Get gallery images with a particular filter criteria such as - * being within the current book or page. - * @param $filter - * @param $pageId - * @param int $pageNum - * @param int $pageSize - * @return array - */ - public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24) - { - $images = $this->image->where('type', '=', 'gallery'); - - $page = $this->page->findOrFail($pageId); - - if ($filter === 'page') { - $images = $images->where('uploaded_to', '=', $page->id); - } elseif ($filter === 'book') { - $validPageIds = $page->book->pages->pluck('id')->toArray(); - $images = $images->whereIn('uploaded_to', $validPageIds); - } - - $images = $this->restrictionService->filterRelatedPages($images, 'images', 'uploaded_to'); - return $this->returnPaginated($images, $pageNum, $pageSize); + return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter); } /** @@ -253,7 +271,17 @@ class ImageRepo */ public function isValidType($type) { + // TODO - To delete? $validTypes = ['gallery', 'cover', 'system', 'user']; return in_array($type, $validTypes); } + + /** + * Get the validation rules for image files. + * @return string + */ + public function getImageValidationRules() + { + return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff'; + } } diff --git a/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php b/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php index f1d00b3e5..a61bd1830 100644 --- a/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php +++ b/database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php @@ -18,6 +18,17 @@ class SetUserProfileImagesUploadedTo extends Migration ->update([ 'uploaded_to' => DB::raw('`created_by`') ]); + + DB::table('images') + ->where('type', '=', 'cover') + ->update(['type' => 'cover_book']); + + $firstBook = DB::table('books')->first(['id']); + if ($firstBook) { + DB::table('images') + ->where('type', '=', 'cover_book') + ->update(['uploaded_to' => $firstBook->id]); + } } /** @@ -32,5 +43,9 @@ class SetUserProfileImagesUploadedTo extends Migration ->update([ 'uploaded_to' => 0 ]); + + DB::table('images') + ->where('type', '=', 'cover_book') + ->update(['type' => 'cover', 'uploaded_to' => 0]); } } diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index ce5cfbf4e..3f60ff031 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -257,39 +257,38 @@ function drawIoPlugin() { DrawIO.show(drawingInit, updateContent); } - function updateContent(pngData) { - let id = "image-" + Math.random().toString(16).slice(2); - let loadingImage = window.baseUrl('/loading.gif'); - let data = { - image: pngData, - uploaded_to: Number(document.getElementById('page-editor').getAttribute('page-id')) - }; + async function updateContent(pngData) { + const id = "image-" + Math.random().toString(16).slice(2); + const loadingImage = window.baseUrl('/loading.gif'); + const pageId = Number(document.getElementById('page-editor').getAttribute('page-id')); // Handle updating an existing image if (currentNode) { DrawIO.close(); let imgElem = currentNode.querySelector('img'); - window.$http.post(window.baseUrl(`/images/drawing/upload`), data).then(resp => { - pageEditor.dom.setAttrib(imgElem, 'src', resp.data.url); - pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', resp.data.id); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(imgElem, 'src', img.url); + pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id); + } catch (err) { window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } return; } - setTimeout(() => { + setTimeout(async () => { pageEditor.insertContent(`
    `); DrawIO.close(); - window.$http.post(window.baseUrl('/images/drawing/upload'), data).then(resp => { - pageEditor.dom.setAttrib(id, 'src', resp.data.url); - pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', resp.data.id); - }).catch(err => { + try { + const img = await DrawIO.upload(pngData, pageId); + pageEditor.dom.setAttrib(id, 'src', img.url); + pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id); + } catch (err) { pageEditor.dom.remove(id); window.$events.emit('error', trans('errors.image_upload_error')); console.log(err); - }); + } }, 5); } diff --git a/resources/assets/js/services/drawio.js b/resources/assets/js/services/drawio.js index b4fcfd59f..6acf091f0 100644 --- a/resources/assets/js/services/drawio.js +++ b/resources/assets/js/services/drawio.js @@ -66,4 +66,13 @@ function drawPostMessage(data) { iFrame.contentWindow.postMessage(JSON.stringify(data), '*'); } -export default {show, close}; \ No newline at end of file +async function upload(imageData, pageUploadedToId) { + let data = { + image: imageData, + uploaded_to: pageUploadedToId, + }; + const resp = await window.$http.post(window.baseUrl(`/images/drawio`), data); + return resp.data; +} + +export default {show, close, upload}; \ No newline at end of file diff --git a/resources/assets/js/vues/image-manager.js b/resources/assets/js/vues/image-manager.js index 843aae378..dd1d9d17a 100644 --- a/resources/assets/js/vues/image-manager.js +++ b/resources/assets/js/vues/image-manager.js @@ -1,7 +1,7 @@ import * as Dates from "../services/dates"; import dropzone from "./components/dropzone"; -let page = 0; +let page = 1; let previousClickTime = 0; let previousClickImage = 0; let dataLoaded = false; @@ -20,7 +20,7 @@ const data = { selectedImage: false, dependantPages: false, showing: false, - view: 'all', + filter: null, hasMore: false, searching: false, searchTerm: '', @@ -56,32 +56,37 @@ const methods = { this.$el.children[0].components.overlay.hide(); }, - fetchData() { - let url = baseUrl + page; - let query = {}; - if (this.uploadedTo !== false) query.uploaded_to = this.uploadedTo; - if (this.searching) query.term = this.searchTerm; + async fetchData() { + let query = { + page, + search: this.searching ? this.searchTerm : null, + uploaded_to: this.uploadedTo || null, + filter_type: this.filter, + }; - this.$http.get(url, {params: query}).then(response => { - this.images = this.images.concat(response.data.images); - this.hasMore = response.data.hasMore; - page++; - }); + const {data} = await this.$http.get(baseUrl, {params: query}); + this.images = this.images.concat(data.images); + this.hasMore = data.has_more; + page++; }, - setView(viewName) { - this.view = viewName; + setFilterType(filterType) { + this.filter = filterType; this.resetState(); this.fetchData(); }, resetState() { this.cancelSearch(); + this.resetListView(); + this.deleteConfirm = false; + baseUrl = window.baseUrl(`/images/${this.imageType}`); + }, + + resetListView() { this.images = []; this.hasMore = false; - this.deleteConfirm = false; - page = 0; - baseUrl = window.baseUrl(`/images/${this.imageType}/${this.view}/`); + page = 1; }, searchImages() { @@ -94,10 +99,7 @@ const methods = { } this.searching = true; - this.images = []; - this.hasMore = false; - page = 0; - baseUrl = window.baseUrl(`/images/${this.imageType}/search/`); + this.resetListView(); this.fetchData(); }, @@ -110,10 +112,10 @@ const methods = { }, imageSelect(image) { - let dblClickTime = 300; - let currentTime = Date.now(); - let timeDiff = currentTime - previousClickTime; - let isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; + const dblClickTime = 300; + const currentTime = Date.now(); + const timeDiff = currentTime - previousClickTime; + const isDblClick = timeDiff < dblClickTime && image.id === previousClickImage; if (isDblClick) { this.callbackAndHide(image); @@ -132,11 +134,11 @@ const methods = { this.hide(); }, - saveImageDetails() { + async saveImageDetails() { let url = window.baseUrl(`/images/${this.selectedImage.id}`); - this.$http.put(url, this.selectedImage).then(response => { - this.$events.emit('success', trans('components.image_update_success')); - }).catch(error => { + try { + await this.$http.put(url, this.selectedImage) + } catch (error) { if (error.response.status === 422) { let errors = error.response.data; let message = ''; @@ -145,27 +147,29 @@ const methods = { }); this.$events.emit('error', message); } - }); + } }, - deleteImage() { + async deleteImage() { if (!this.deleteConfirm) { - let url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); - this.$http.get(url).then(resp => { - this.dependantPages = resp.data; - }).catch(console.error).then(() => { - this.deleteConfirm = true; - }); + const url = window.baseUrl(`/images/usage/${this.selectedImage.id}`); + try { + const {data} = await this.$http.get(url); + this.dependantPages = data; + } catch (error) { + console.error(error); + } + this.deleteConfirm = true; return; } - let url = window.baseUrl(`/images/${this.selectedImage.id}`); - this.$http.delete(url).then(resp => { - this.images.splice(this.images.indexOf(this.selectedImage), 1); - this.selectedImage = false; - this.$events.emit('success', trans('components.image_delete_success')); - this.deleteConfirm = false; - }); + + const url = window.baseUrl(`/images/${this.selectedImage.id}`); + await this.$http.delete(url); + this.images.splice(this.images.indexOf(this.selectedImage), 1); + this.selectedImage = false; + this.$events.emit('success', trans('components.image_delete_success')); + this.deleteConfirm = false; }, getDate(stringDate) { @@ -180,7 +184,7 @@ const methods = { const computed = { uploadUrl() { - return window.baseUrl(`/images/${this.imageType}/upload`); + return window.baseUrl(`/images/${this.imageType}`); } }; @@ -188,7 +192,7 @@ 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/') + baseUrl = window.baseUrl('/images/' + this.imageType) } export default { diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index f048b543b..a02029a10 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -23,5 +23,5 @@
    - @include('components.image-manager', ['imageType' => 'cover']) + @include('components.image-manager', ['imageType' => 'cover', 'uploaded_to']) @stop \ No newline at end of file diff --git a/resources/views/components/image-manager.blade.php b/resources/views/components/image-manager.blade.php index df577b545..7c9084ad1 100644 --- a/resources/views/components/image-manager.blade.php +++ b/resources/views/components/image-manager.blade.php @@ -10,12 +10,12 @@
    - - @include('components.image-manager', ['imageType' => 'cover']) + @include('components.image-manager', ['imageType' => 'cover_bookshelf', 'uploaded_to' => $shelf->id]) @stop \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 933179aa7..cecd67456 100644 --- a/routes/web.php +++ b/routes/web.php @@ -103,27 +103,41 @@ Route::group(['middleware' => 'auth'], function () { Route::get('/user/{userId}', 'UserController@showProfilePage'); // Image routes - Route::group(['prefix' => 'images'], function() { + Route::group(['prefix' => 'images'], function () { + // Get for user images // Route::get('/user/all', 'ImageController@getAllForUserType'); // Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); + // Standard get, update and deletion for all types Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); Route::get('/base64/{id}', 'ImageController@getBase64Image'); Route::get('/usage/{id}', 'ImageController@usage'); - Route::get('/{type}/all', 'ImageController@getAllByType'); - Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); - Route::get('/{type}/search/{page}', 'ImageController@searchByType'); - Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); +// Route::get('/{type}/all', 'ImageController@getAllByType'); +// Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); +// Route::get('/{type}/search/{page}', 'ImageController@searchByType'); +// Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); + + // Gallery + Route::get('/gallery', 'Images\GalleryImageController@list'); + Route::post('/gallery', 'Images\GalleryImageController@create'); + // Drawio + Route::get('/drawio', 'Images\DrawioImageController@list'); + Route::post('/drawio', 'Images\DrawioImageController@create'); + // User + Route::get('/user', 'Images\UserImageController@list'); + Route::post('/user', 'Images\UserImageController@create'); + // System + Route::get('/system', 'Images\SystemImageController@list'); + Route::post('/system', 'Images\SystemImageController@create'); + // Cover + Route::get('/cover_{entity}', 'Images\CoverImageController@list'); + Route::post('/cover_{entity}', 'Images\CoverImageController@create'); // TODO - Remove use of abstract "Type" variable (Above) // TODO - Clearly define each endpoint so logic for each is clear // TODO - Move into per-type controllers // TODO - Test and fully think about permissions and each stage - Route::post('/drawio', 'ImageController@uploadDrawioImage'); - Route::post('/gallery', 'ImageController@uploadGalleryImage'); - Route::post('/user', 'ImageController@uploadUserImage'); - Route::post('/system', 'ImageController@uploadSystemImage'); Route::post('/cover', 'ImageController@uploadCoverImage'); Route::put('/{id}', 'ImageController@update'); From cb832a2c10984b91bb7dbf4df7874bbb2e7dcbef Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 27 Apr 2019 14:55:23 +0100 Subject: [PATCH 149/160] Started diversion to not using image manager for cover/system/user --- app/Http/Controllers/BookshelfController.php | 112 +++++++++++------- .../Images/CoverImageController.php | 4 + .../Images/SystemImageController.php | 3 + .../Images/UserImageController.php | 3 + ...55_set_user_profile_images_uploaded_to.php | 51 -------- .../views/components/image-picker.blade.php | 2 + resources/views/shelves/edit.blade.php | 1 - routes/web.php | 28 +---- 8 files changed, 90 insertions(+), 114 deletions(-) delete mode 100644 database/migrations/2019_04_21_131855_set_user_profile_images_uploaded_to.php diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index b86bc2e38..e63cfd1d5 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -5,6 +5,7 @@ use BookStack\Auth\UserRepo; use BookStack\Entities\Bookshelf; use BookStack\Entities\EntityContextManager; use BookStack\Entities\Repos\EntityRepo; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; use Views; @@ -15,18 +16,21 @@ class BookshelfController extends Controller protected $entityRepo; protected $userRepo; protected $entityContextManager; + protected $imageRepo; /** * BookController constructor. * @param EntityRepo $entityRepo * @param UserRepo $userRepo * @param EntityContextManager $entityContextManager + * @param ImageRepo $imageRepo */ - public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager) + public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo) { $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; $this->entityContextManager = $entityContextManager; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -91,13 +95,14 @@ class BookshelfController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255', 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); - $bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); - $this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', '')); - Activity::add($bookshelf, 'bookshelf_create'); + $shelf = $this->entityRepo->createFromInput('bookshelf', $request->all()); + $this->shelfUpdateActions($shelf, $request); - return redirect($bookshelf->getUrl()); + Activity::add($shelf, 'bookshelf_create'); + return redirect($shelf->getUrl()); } @@ -109,19 +114,19 @@ class BookshelfController extends Controller */ public function show(string $slug) { - /** @var Bookshelf $bookshelf */ - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('book-view', $bookshelf); + /** @var Bookshelf $shelf */ + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('book-view', $shelf); - $books = $this->entityRepo->getBookshelfChildren($bookshelf); - Views::add($bookshelf); - $this->entityContextManager->setShelfContext($bookshelf->id); + $books = $this->entityRepo->getBookshelfChildren($shelf); + Views::add($shelf); + $this->entityContextManager->setShelfContext($shelf->id); - $this->setPageTitle($bookshelf->getShortName()); + $this->setPageTitle($shelf->getShortName()); return view('shelves.show', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'books' => $books, - 'activity' => Activity::entityActivity($bookshelf, 20, 1) + 'activity' => Activity::entityActivity($shelf, 20, 1) ]); } @@ -133,19 +138,19 @@ class BookshelfController extends Controller */ public function edit(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-update', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-update', $shelf); - $shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf); + $shelfBooks = $this->entityRepo->getBookshelfChildren($shelf); $shelfBookIds = $shelfBooks->pluck('id'); $books = $this->entityRepo->getAll('book', false, 'update'); $books = $books->filter(function ($book) use ($shelfBookIds) { return !$shelfBookIds->contains($book->id); }); - $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()])); + $this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()])); return view('shelves.edit', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'books' => $books, 'shelfBooks' => $shelfBooks, ]); @@ -154,10 +159,11 @@ class BookshelfController extends Controller /** * Update the specified bookshelf in storage. - * @param Request $request + * @param Request $request * @param string $slug * @return Response * @throws \BookStack\Exceptions\NotFoundException + * @throws \BookStack\Exceptions\ImageUploadException */ public function update(Request $request, string $slug) { @@ -166,10 +172,12 @@ class BookshelfController extends Controller $this->validate($request, [ 'name' => 'required|string|max:255', 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); $shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all()); - $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + $this->shelfUpdateActions($shelf, $request); + Activity::add($shelf, 'bookshelf_update'); return redirect($shelf->getUrl()); @@ -184,11 +192,11 @@ class BookshelfController extends Controller */ public function showDelete(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-delete', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $shelf); - $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()])); - return view('shelves.delete', ['shelf' => $bookshelf]); + $this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()])); + return view('shelves.delete', ['shelf' => $shelf]); } /** @@ -200,10 +208,15 @@ class BookshelfController extends Controller */ public function destroy(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */ - $this->checkOwnablePermission('bookshelf-delete', $bookshelf); - Activity::addMessage('bookshelf_delete', 0, $bookshelf->name); - $this->entityRepo->destroyBookshelf($bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */ + $this->checkOwnablePermission('bookshelf-delete', $shelf); + Activity::addMessage('bookshelf_delete', 0, $shelf->name); + + if ($shelf->cover) { + $this->imageRepo->destroyImage($shelf->cover); + } + $this->entityRepo->destroyBookshelf($shelf); + return redirect('/shelves'); } @@ -215,12 +228,12 @@ class BookshelfController extends Controller */ public function showPermissions(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); $roles = $this->userRepo->getRestrictableRoles(); return view('shelves.permissions', [ - 'shelf' => $bookshelf, + 'shelf' => $shelf, 'roles' => $roles ]); } @@ -235,12 +248,12 @@ class BookshelfController extends Controller */ public function permissions(string $slug, Request $request) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); - $this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf); + $this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf); session()->flash('success', trans('entities.shelves_permissions_updated')); - return redirect($bookshelf->getUrl()); + return redirect($shelf->getUrl()); } /** @@ -251,11 +264,30 @@ class BookshelfController extends Controller */ public function copyPermissions(string $slug) { - $bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); - $this->checkOwnablePermission('restrictions-manage', $bookshelf); + $shelf = $this->entityRepo->getBySlug('bookshelf', $slug); + $this->checkOwnablePermission('restrictions-manage', $shelf); - $updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf); + $updateCount = $this->entityRepo->copyBookshelfPermissions($shelf); session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount])); - return redirect($bookshelf->getUrl()); + return redirect($shelf->getUrl()); + } + + /** + * Common actions to run on bookshelf update. + * @param Bookshelf $shelf + * @param Request $request + * @throws \BookStack\Exceptions\ImageUploadException + */ + protected function shelfUpdateActions(Bookshelf $shelf, Request $request) + { + // Update the books that the shelf references + $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); + + // Update the cover image if in request + if ($request->has('image') && userCan('image-create-all')) { + $image = $this->imageRepo->saveNew($request->file('image'), 'cover', $shelf->id); + $shelf->image_id = $image->id; + $shelf->save(); + } } } diff --git a/app/Http/Controllers/Images/CoverImageController.php b/app/Http/Controllers/Images/CoverImageController.php index 807fddc38..fdec56be9 100644 --- a/app/Http/Controllers/Images/CoverImageController.php +++ b/app/Http/Controllers/Images/CoverImageController.php @@ -2,6 +2,10 @@ namespace BookStack\Http\Controllers\Images; +// TODO - Replace this with entity-level handling +// Since won't be part of image manager handling +// Added some to bookshelf controller already + use BookStack\Entities\EntityProvider; use BookStack\Entities\Repos\EntityRepo; use BookStack\Exceptions\ImageUploadException; diff --git a/app/Http/Controllers/Images/SystemImageController.php b/app/Http/Controllers/Images/SystemImageController.php index 1c4de2f70..ad3de9625 100644 --- a/app/Http/Controllers/Images/SystemImageController.php +++ b/app/Http/Controllers/Images/SystemImageController.php @@ -1,5 +1,8 @@ where('type', '=', 'user') - ->update([ - 'uploaded_to' => DB::raw('`created_by`') - ]); - - DB::table('images') - ->where('type', '=', 'cover') - ->update(['type' => 'cover_book']); - - $firstBook = DB::table('books')->first(['id']); - if ($firstBook) { - DB::table('images') - ->where('type', '=', 'cover_book') - ->update(['uploaded_to' => $firstBook->id]); - } - } - - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - DB::table('images') - ->where('type', '=', 'user') - ->update([ - 'uploaded_to' => 0 - ]); - - DB::table('images') - ->where('type', '=', 'cover_book') - ->update(['type' => 'cover', 'uploaded_to' => 0]); - } -} diff --git a/resources/views/components/image-picker.blade.php b/resources/views/components/image-picker.blade.php index e7c74c786..10df50293 100644 --- a/resources/views/components/image-picker.blade.php +++ b/resources/views/components/image-picker.blade.php @@ -17,4 +17,6 @@
    +{{-- TODO - Revamp to be custom file upload button, instead of being linked to image manager--}} +{{-- TODO - Remove image manager use where this is used and clean image manager for drawing/gallery use.--}}
    \ No newline at end of file diff --git a/resources/views/shelves/edit.blade.php b/resources/views/shelves/edit.blade.php index c51330ce0..fa8a3d63b 100644 --- a/resources/views/shelves/edit.blade.php +++ b/resources/views/shelves/edit.blade.php @@ -23,5 +23,4 @@
    - @include('components.image-manager', ['imageType' => 'cover_bookshelf', 'uploaded_to' => $shelf->id]) @stop \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index cecd67456..975ab5b17 100644 --- a/routes/web.php +++ b/routes/web.php @@ -105,41 +105,25 @@ Route::group(['middleware' => 'auth'], function () { // Image routes Route::group(['prefix' => 'images'], function () { - // Get for user images -// Route::get('/user/all', 'ImageController@getAllForUserType'); -// Route::get('/user/all/{page}', 'ImageController@getAllForUserType'); - + // TODO - Check auth on these + // TODO - Maybe check types for only gallery or drawing // Standard get, update and deletion for all types Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); Route::get('/base64/{id}', 'ImageController@getBase64Image'); Route::get('/usage/{id}', 'ImageController@usage'); -// Route::get('/{type}/all', 'ImageController@getAllByType'); -// Route::get('/{type}/all/{page}', 'ImageController@getAllByType'); -// Route::get('/{type}/search/{page}', 'ImageController@searchByType'); -// Route::get('/gallery/{filter}/{page}', 'ImageController@getGalleryFiltered'); // Gallery Route::get('/gallery', 'Images\GalleryImageController@list'); Route::post('/gallery', 'Images\GalleryImageController@create'); + // Drawio Route::get('/drawio', 'Images\DrawioImageController@list'); Route::post('/drawio', 'Images\DrawioImageController@create'); - // User - Route::get('/user', 'Images\UserImageController@list'); - Route::post('/user', 'Images\UserImageController@create'); - // System - Route::get('/system', 'Images\SystemImageController@list'); - Route::post('/system', 'Images\SystemImageController@create'); - // Cover - Route::get('/cover_{entity}', 'Images\CoverImageController@list'); - Route::post('/cover_{entity}', 'Images\CoverImageController@create'); - // TODO - Remove use of abstract "Type" variable (Above) - // TODO - Clearly define each endpoint so logic for each is clear - // TODO - Move into per-type controllers - // TODO - Test and fully think about permissions and each stage - Route::post('/cover', 'ImageController@uploadCoverImage'); + // TODO - Check auth on these + // TODO - Maybe check types for only gallery or drawing + // Or add to gallery/drawio controllers Route::put('/{id}', 'ImageController@update'); Route::delete('/{id}', 'ImageController@destroy'); }); From 79f6dc00a366fbb318ac70db7ec1128615482655 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 May 2019 15:48:15 +0100 Subject: [PATCH 150/160] Change image-selector to not use manager - Now changes the images directly for user, system & cover. - Extra permission checks added to edit & delete actions. --- app/Auth/User.php | 2 +- app/Http/Controllers/BookController.php | 54 +++- app/Http/Controllers/BookshelfController.php | 14 +- app/Http/Controllers/ImageController.php | 242 ------------------ .../Images/CoverImageController.php | 99 ------- .../Images/DrawioImageController.php | 22 ++ .../Controllers/Images/ImageController.php | 115 +++++++++ .../Images/SystemImageController.php | 67 ----- .../Images/UserImageController.php | 73 ------ app/Http/Controllers/SettingController.php | 33 ++- app/Http/Controllers/UserController.php | 32 ++- app/Uploads/Image.php | 11 + app/Uploads/ImageRepo.php | 58 ++--- app/Uploads/ImageService.php | 54 +++- .../assets/js/components/image-picker.js | 56 ++-- .../assets/js/components/markdown-editor.js | 4 +- .../assets/js/components/wysiwyg-editor.js | 4 +- resources/assets/js/services/drawio.js | 12 +- resources/assets/sass/_layout.scss | 4 + resources/lang/en/validation.php | 1 + resources/views/books/create.blade.php | 1 - resources/views/books/edit.blade.php | 4 +- resources/views/books/form.blade.php | 8 +- .../views/components/image-picker.blade.php | 22 +- resources/views/settings/index.blade.php | 10 +- resources/views/shelves/create.blade.php | 2 - resources/views/shelves/edit.blade.php | 2 +- resources/views/shelves/form.blade.php | 8 +- resources/views/users/edit.blade.php | 5 +- routes/web.php | 21 +- 30 files changed, 415 insertions(+), 625 deletions(-) delete mode 100644 app/Http/Controllers/ImageController.php delete mode 100644 app/Http/Controllers/Images/CoverImageController.php create mode 100644 app/Http/Controllers/Images/ImageController.php delete mode 100644 app/Http/Controllers/Images/SystemImageController.php delete mode 100644 app/Http/Controllers/Images/UserImageController.php diff --git a/app/Auth/User.php b/app/Auth/User.php index 05e77e13d..12f022b06 100644 --- a/app/Auth/User.php +++ b/app/Auth/User.php @@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon * The attributes that are mass assignable. * @var array */ - protected $fillable = ['name', 'email', 'image_id']; + protected $fillable = ['name', 'email']; /** * The attributes excluded from the model's JSON form. diff --git a/app/Http/Controllers/BookController.php b/app/Http/Controllers/BookController.php index 24e0d784d..7c8ad5c2f 100644 --- a/app/Http/Controllers/BookController.php +++ b/app/Http/Controllers/BookController.php @@ -6,6 +6,7 @@ use BookStack\Entities\Book; use BookStack\Entities\EntityContextManager; use BookStack\Entities\Repos\EntityRepo; use BookStack\Entities\ExportService; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; use Views; @@ -17,6 +18,7 @@ class BookController extends Controller protected $userRepo; protected $exportService; protected $entityContextManager; + protected $imageRepo; /** * BookController constructor. @@ -24,17 +26,20 @@ class BookController extends Controller * @param UserRepo $userRepo * @param ExportService $exportService * @param EntityContextManager $entityContextManager + * @param ImageRepo $imageRepo */ public function __construct( EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService, - EntityContextManager $entityContextManager + EntityContextManager $entityContextManager, + ImageRepo $imageRepo ) { $this->entityRepo = $entityRepo; $this->userRepo = $userRepo; $this->exportService = $exportService; $this->entityContextManager = $entityContextManager; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -101,13 +106,15 @@ class BookController extends Controller * @param string $shelfSlug * @return Response * @throws \BookStack\Exceptions\NotFoundException + * @throws \BookStack\Exceptions\ImageUploadException */ public function store(Request $request, string $shelfSlug = null) { $this->checkPermission('book-create-all'); $this->validate($request, [ 'name' => 'required|string|max:255', - 'description' => 'string|max:1000' + 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); $bookshelf = null; @@ -117,6 +124,7 @@ class BookController extends Controller } $book = $this->entityRepo->createFromInput('book', $request->all()); + $this->bookUpdateActions($book, $request); Activity::add($book, 'book_create', $book->id); if ($bookshelf) { @@ -170,20 +178,27 @@ class BookController extends Controller /** * Update the specified book in storage. - * @param Request $request + * @param Request $request * @param $slug * @return Response + * @throws \BookStack\Exceptions\ImageUploadException + * @throws \BookStack\Exceptions\NotFoundException */ - public function update(Request $request, $slug) + public function update(Request $request, string $slug) { $book = $this->entityRepo->getBySlug('book', $slug); $this->checkOwnablePermission('book-update', $book); $this->validate($request, [ 'name' => 'required|string|max:255', - 'description' => 'string|max:1000' + 'description' => 'string|max:1000', + 'image' => $this->imageRepo->getImageValidationRules(), ]); + $book = $this->entityRepo->updateFromInput('book', $book, $request->all()); + $this->bookUpdateActions($book, $request); + Activity::add($book, 'book_update', $book->id); + return redirect($book->getUrl()); } @@ -311,7 +326,12 @@ class BookController extends Controller $book = $this->entityRepo->getBySlug('book', $bookSlug); $this->checkOwnablePermission('book-delete', $book); Activity::addMessage('book_delete', 0, $book->name); + + if ($book->cover) { + $this->imageRepo->destroyImage($book->cover); + } $this->entityRepo->destroyBook($book); + return redirect('/books'); } @@ -383,4 +403,28 @@ class BookController extends Controller $textContent = $this->exportService->bookToPlainText($book); return $this->downloadResponse($textContent, $bookSlug . '.txt'); } + + /** + * Common actions to run on book update. + * Handles updating the cover image. + * @param Book $book + * @param Request $request + * @throws \BookStack\Exceptions\ImageUploadException + */ + protected function bookUpdateActions(Book $book, Request $request) + { + // Update the cover image if in request + if ($request->has('image')) { + $newImage = $request->file('image'); + $image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true); + $book->image_id = $image->id; + $book->save(); + } + + if ($request->has('image_reset')) { + $this->imageRepo->destroyImage($book->cover); + $book->image_id = 0; + $book->save(); + } + } } diff --git a/app/Http/Controllers/BookshelfController.php b/app/Http/Controllers/BookshelfController.php index e63cfd1d5..dba2503ef 100644 --- a/app/Http/Controllers/BookshelfController.php +++ b/app/Http/Controllers/BookshelfController.php @@ -86,8 +86,9 @@ class BookshelfController extends Controller /** * Store a newly created bookshelf in storage. - * @param Request $request + * @param Request $request * @return Response + * @throws \BookStack\Exceptions\ImageUploadException */ public function store(Request $request) { @@ -284,10 +285,17 @@ class BookshelfController extends Controller $this->entityRepo->updateShelfBooks($shelf, $request->get('books', '')); // Update the cover image if in request - if ($request->has('image') && userCan('image-create-all')) { - $image = $this->imageRepo->saveNew($request->file('image'), 'cover', $shelf->id); + if ($request->has('image')) { + $newImage = $request->file('image'); + $image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true); $shelf->image_id = $image->id; $shelf->save(); } + + if ($request->has('image_reset')) { + $this->imageRepo->destroyImage($shelf->cover); + $shelf->image_id = 0; + $shelf->save(); + } } } diff --git a/app/Http/Controllers/ImageController.php b/app/Http/Controllers/ImageController.php deleted file mode 100644 index df7758176..000000000 --- a/app/Http/Controllers/ImageController.php +++ /dev/null @@ -1,242 +0,0 @@ -image = $image; - $this->file = $file; - $this->imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Provide an image file from storage. - * @param string $path - * @return mixed - */ - public function showImage(string $path) - { - $path = storage_path('uploads/images/' . $path); - if (!file_exists($path)) { - abort(404); - } - - return response()->file($path); - } - - /** - * Get all images for a specific type, Paginated - * @param Request $request - * @param string $type - * @param int $page - * @return \Illuminate\Http\JsonResponse - */ - public function getAllByType(Request $request, $type, $page = 0) - { - $uploadedToFilter = $request->get('uploaded_to', null); - - // For user profile request, check access to user images - if ($type === 'user') { - $this->checkPermissionOrCurrentUser('users-manage', $uploadedToFilter ?? 0); - } - - $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, $uploadedToFilter); - return response()->json($imgData); - } - - /** - * Search through images within a particular type. - * @param $type - * @param int $page - * @param Request $request - * @return mixed - */ - public function searchByType(Request $request, $type, $page = 0) - { - $this->validate($request, [ - 'term' => 'required|string' - ]); - - $searchTerm = $request->get('term'); - $imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24); - return response()->json($imgData); - } - - public function uploadUserImage(Request $request) - { - // TODO - } - - public function uploadSystemImage(Request $request) - { - // TODO - } - - public function uploadCoverImage(Request $request) - { - // TODO - } - - /** - * Upload a draw.io image into the system. - * @param Request $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response - */ - public function uploadDrawioImage(Request $request) - { - $this->validate($request, [ - 'image' => 'required|string', - 'uploaded_to' => 'required|integer' - ]); - $uploadedTo = $request->get('uploaded_to', 0); - $page = $this-> - $this->checkPermission('image-create-all'); - $imageBase64Data = $request->get('image'); - - try { - $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - - /** - * Handles image uploads for use on pages. - * @param string $type - * @param Request $request - * @return \Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function uploadByType($type, Request $request) - { - $this->checkPermission('image-create-all'); - $this->validate($request, [ - 'file' => 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff' - ]); - - if (!$this->imageRepo->isValidType($type)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $imageUpload = $request->file('file'); - - try { - $uploadedTo = $request->get('uploaded_to', 0); - - // For user profile request, check access to user images - if ($type === 'user') { - $this->checkPermissionOrCurrentUser('users-manage', $uploadedTo ?? 0); - } - - $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - /** - * Get the content of an image based64 encoded. - * @param $id - * @return \Illuminate\Http\JsonResponse|mixed - */ - public function getBase64Image($id) - { - $image = $this->imageRepo->getById($id); - $imageData = $this->imageRepo->getImageData($image); - if ($imageData === null) { - return $this->jsonError("Image data could not be found"); - } - return response()->json([ - 'content' => base64_encode($imageData) - ]); - } - - /** - * Generate a sized thumbnail for an image. - * @param $id - * @param $width - * @param $height - * @param $crop - * @return \Illuminate\Http\JsonResponse - * @throws ImageUploadException - * @throws \Exception - */ - public function getThumbnail($id, $width, $height, $crop) - { - $this->checkPermission('image-create-all'); - $image = $this->imageRepo->getById($id); - $thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false'); - return response()->json(['url' => $thumbnailUrl]); - } - - /** - * Update image details - * @param integer $id - * @param Request $request - * @return \Illuminate\Http\JsonResponse - * @throws ImageUploadException - * @throws \Exception - */ - public function update($id, Request $request) - { - $this->validate($request, [ - 'name' => 'required|min:2|string' - ]); - - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('image-update', $image); - - $image = $this->imageRepo->updateImageDetails($image, $request->all()); - return response()->json($image); - } - - /** - * Show the usage of an image on pages. - * @param \BookStack\Entities\Repos\EntityRepo $entityRepo - * @param $id - * @return \Illuminate\Http\JsonResponse - */ - public function usage(EntityRepo $entityRepo, $id) - { - $image = $this->imageRepo->getById($id); - $pageSearch = $entityRepo->searchForImage($image->url); - return response()->json($pageSearch); - } - - /** - * Deletes an image and all thumbnail/image files - * @param int $id - * @return \Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function destroy($id) - { - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission('image-delete', $image); - - $this->imageRepo->destroyImage($image); - return response()->json(trans('components.images_deleted')); - } -} diff --git a/app/Http/Controllers/Images/CoverImageController.php b/app/Http/Controllers/Images/CoverImageController.php deleted file mode 100644 index fdec56be9..000000000 --- a/app/Http/Controllers/Images/CoverImageController.php +++ /dev/null @@ -1,99 +0,0 @@ -imageRepo = $imageRepo; - $this->entityRepo = $entityRepo; - - parent::__construct(); - } - - /** - * Get a list of cover images, in a list. - * Can be paged and filtered by entity. - * @param Request $request - * @param string $entity - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request, $entity) - { - if (!$this->isValidEntityTypeForCover($entity)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - - $type = 'cover_' . $entity; - $imgData = $this->imageRepo->getPaginatedByType($type, $page, 24, null, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new cover image in the system. - * @param Request $request - * @param string $entity - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request, $entity) - { - $this->checkPermission('image-create-all'); - $this->validate($request, [ - 'file' => $this->imageRepo->getImageValidationRules(), - 'uploaded_to' => 'required|integer' - ]); - - if (!$this->isValidEntityTypeForCover($entity)) { - return $this->jsonError(trans('errors.image_upload_type_error')); - } - - $uploadedTo = $request->get('uploaded_to', 0); - $entityInstance = $this->entityRepo->getById($entity, $uploadedTo); - $this->checkOwnablePermission($entity . '-update', $entityInstance); - - try { - $type = 'cover_' . $entity; - $imageUpload = $request->file('file'); - $image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - - /** - * Check if the given entity type is valid entity to have cover images. - * @param string $entityType - * @return bool - */ - protected function isValidEntityTypeForCover(string $entityType) - { - return ($entityType === 'book' || $entityType === 'bookshelf'); - } - -} diff --git a/app/Http/Controllers/Images/DrawioImageController.php b/app/Http/Controllers/Images/DrawioImageController.php index eb0e32827..2deb64d96 100644 --- a/app/Http/Controllers/Images/DrawioImageController.php +++ b/app/Http/Controllers/Images/DrawioImageController.php @@ -64,4 +64,26 @@ class DrawioImageController extends Controller return response()->json($image); } + /** + * Get the content of an image based64 encoded. + * @param $id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getAsBase64($id) + { + $image = $this->imageRepo->getById($id); + $page = $image->getPage(); + if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) { + return $this->jsonError("Image data could not be found"); + } + + $imageData = $this->imageRepo->getImageData($image); + if ($imageData === null) { + return $this->jsonError("Image data could not be found"); + } + return response()->json([ + 'content' => base64_encode($imageData) + ]); + } + } diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php new file mode 100644 index 000000000..a76fb5e53 --- /dev/null +++ b/app/Http/Controllers/Images/ImageController.php @@ -0,0 +1,115 @@ +image = $image; + $this->file = $file; + $this->imageRepo = $imageRepo; + parent::__construct(); + } + + /** + * Provide an image file from storage. + * @param string $path + * @return mixed + */ + public function showImage(string $path) + { + $path = storage_path('uploads/images/' . $path); + if (!file_exists($path)) { + abort(404); + } + + return response()->file($path); + } + + + /** + * Update image details + * @param integer $id + * @param Request $request + * @return \Illuminate\Http\JsonResponse + * @throws ImageUploadException + * @throws \Exception + */ + public function update($id, Request $request) + { + $this->validate($request, [ + 'name' => 'required|min:2|string' + ]); + + $image = $this->imageRepo->getById($id); + $this->checkImagePermission($image); + $this->checkOwnablePermission('image-update', $image); + + $image = $this->imageRepo->updateImageDetails($image, $request->all()); + return response()->json($image); + } + + /** + * Show the usage of an image on pages. + * @param \BookStack\Entities\Repos\EntityRepo $entityRepo + * @param $id + * @return \Illuminate\Http\JsonResponse + */ + public function usage(EntityRepo $entityRepo, $id) + { + $image = $this->imageRepo->getById($id); + $this->checkImagePermission($image); + $pageSearch = $entityRepo->searchForImage($image->url); + return response()->json($pageSearch); + } + + /** + * Deletes an image and all thumbnail/image files + * @param int $id + * @return \Illuminate\Http\JsonResponse + * @throws \Exception + */ + public function destroy($id) + { + $image = $this->imageRepo->getById($id); + $this->checkOwnablePermission('image-delete', $image); + $this->checkImagePermission($image); + + $this->imageRepo->destroyImage($image); + return response()->json(trans('components.images_deleted')); + } + + /** + * Check related page permission and ensure type is drawio or gallery. + * @param Image $image + */ + protected function checkImagePermission(Image $image) + { + if ($image->type !== 'drawio' || $image->type !== 'gallery') { + $this->showPermissionError(); + } + + $relatedPage = $image->getPage(); + if ($relatedPage) { + $this->checkOwnablePermission('page-view', $relatedPage); + } + } +} diff --git a/app/Http/Controllers/Images/SystemImageController.php b/app/Http/Controllers/Images/SystemImageController.php deleted file mode 100644 index ad3de9625..000000000 --- a/app/Http/Controllers/Images/SystemImageController.php +++ /dev/null @@ -1,67 +0,0 @@ -imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Get a list of system images, in a list. - * @param Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request) - { - $this->checkPermission('settings-manage'); - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - - $imgData = $this->imageRepo->getPaginatedByType('system', $page, 24, null, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new system image. - * @param Request $request - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request) - { - $this->checkPermission('image-create-all'); - $this->checkPermission('settings-manage'); - - $this->validate($request, [ - 'file' => $this->imageRepo->getImageValidationRules() - ]); - - try { - $imageUpload = $request->file('file'); - $image = $this->imageRepo->saveNew($imageUpload, 'system', 0); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - -} diff --git a/app/Http/Controllers/Images/UserImageController.php b/app/Http/Controllers/Images/UserImageController.php deleted file mode 100644 index 312ccf47e..000000000 --- a/app/Http/Controllers/Images/UserImageController.php +++ /dev/null @@ -1,73 +0,0 @@ -imageRepo = $imageRepo; - parent::__construct(); - } - - /** - * Get a list of user profile images, in a list. - * @param Request $request - * @return \Illuminate\Http\JsonResponse - */ - public function list(Request $request) - { - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - $userId = $request->get('uploaded_to', null); - - $this->checkPermissionOrCurrentUser('users-manage', $userId); - - $imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $userId, $searchTerm); - return response()->json($imgData); - } - - /** - * Store a new user profile image in the system. - * @param Request $request - * @return Illuminate\Http\JsonResponse - * @throws \Exception - */ - public function create(Request $request) - { - $this->checkPermission('image-create-all'); - - $this->validate($request, [ - 'uploaded_to' => 'required|integer', - 'file' => $this->imageRepo->getImageValidationRules() - ]); - - $userId = $request->get('uploaded_to', null); - $this->checkPermissionOrCurrentUser('users-manage', $userId); - - try { - $imageUpload = $request->file('file'); - $uploadedTo = $request->get('uploaded_to', 0); - $image = $this->imageRepo->saveNew($imageUpload, 'user', $uploadedTo); - } catch (ImageUploadException $e) { - return response($e->getMessage(), 500); - } - - return response()->json($image); - } - -} diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 159e19a2b..650833c7f 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -1,6 +1,7 @@ imageRepo = $imageRepo; + parent::__construct(); + } + + /** * Display a listing of the settings. * @return Response @@ -35,6 +49,9 @@ class SettingController extends Controller { $this->preventAccessForDemoUsers(); $this->checkPermission('settings-manage'); + $this->validate($request, [ + 'app_logo' => $this->imageRepo->getImageValidationRules(), + ]); // Cycles through posted settings and update them foreach ($request->all() as $name => $value) { @@ -42,7 +59,21 @@ class SettingController extends Controller continue; } $key = str_replace('setting-', '', trim($name)); - Setting::put($key, $value); + setting()->put($key, $value); + } + + // Update logo image if set + if ($request->has('app_logo')) { + $logoFile = $request->file('app_logo'); + $this->imageRepo->destroyByType('system'); + $image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86); + setting()->put('app-logo', $image->url); + } + + // Clear logo image if requested + if ($request->get('app_logo_reset', null)) { + $this->imageRepo->destroyByType('system'); + setting()->remove('app-logo'); } session()->flash('success', trans('settings.settings_save_success')); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index a93e8d9c9..c7a7c5646 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService; use BookStack\Auth\User; use BookStack\Auth\UserRepo; use BookStack\Exceptions\UserUpdateException; +use BookStack\Uploads\ImageRepo; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -12,16 +13,19 @@ class UserController extends Controller protected $user; protected $userRepo; + protected $imageRepo; /** * UserController constructor. - * @param User $user + * @param User $user * @param UserRepo $userRepo + * @param ImageRepo $imageRepo */ - public function __construct(User $user, UserRepo $userRepo) + public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo) { $this->user = $user; $this->userRepo = $userRepo; + $this->imageRepo = $imageRepo; parent::__construct(); } @@ -94,6 +98,7 @@ class UserController extends Controller $this->userRepo->setUserRoles($user, $roles); } + // TODO - Check this uses new profile assignment $this->userRepo->downloadAndAssignUserAvatar($user); return redirect('/settings/users'); @@ -121,10 +126,11 @@ class UserController extends Controller /** * Update the specified user in storage. - * @param Request $request - * @param int $id + * @param Request $request + * @param int $id * @return Response * @throws UserUpdateException + * @throws \BookStack\Exceptions\ImageUploadException */ public function update(Request $request, $id) { @@ -136,7 +142,8 @@ class UserController extends Controller 'email' => 'min:2|email|unique:users,email,' . $id, 'password' => 'min:5|required_with:password_confirm', 'password-confirm' => 'same:password|required_with:password', - 'setting' => 'array' + 'setting' => 'array', + 'profile_image' => $this->imageRepo->getImageValidationRules(), ]); $user = $this->userRepo->getById($id); @@ -166,10 +173,23 @@ class UserController extends Controller } } + // Save profile image if in request + if ($request->has('profile_image')) { + $imageUpload = $request->file('profile_image'); + $this->imageRepo->destroyImage($user->avatar); + $image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id); + $user->image_id = $image->id; + } + + // Delete the profile image if set to + if ($request->has('profile_image_reset')) { + $this->imageRepo->destroyImage($user->avatar); + } + $user->save(); session()->flash('success', trans('settings.users_edit_success')); - $redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id; + $redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id); return redirect($redirectUrl); } diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index df6d9fb0d..6fa5db2a5 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -1,5 +1,6 @@ belongsTo(Page::class, 'uploaded_to')->first(); + } } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index 235889eee..dbf652ddf 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -2,7 +2,6 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Entities\Page; -use BookStack\Http\Requests\Request; use Illuminate\Database\Eloquent\Builder; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -38,7 +37,7 @@ class ImageRepo /** * Get an image with the given id. * @param $id - * @return mixed + * @return Image */ public function getById($id) { @@ -100,16 +99,8 @@ class ImageRepo $imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%'); } - // Filter by page access if gallery - if ($type === 'gallery') { - $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); - } - - // Filter by entity if cover - if (strpos($type, 'cover_') === 0) { - $entityType = explode('_', $type)[1]; - $imageQuery = $this->restrictionService->filterRelatedEntity($entityType, $imageQuery, 'images', 'uploaded_to'); - } + // Filter by page access + $imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to'); if ($whereClause !== null) { $imageQuery = $imageQuery->where($whereClause); @@ -157,15 +148,17 @@ class ImageRepo /** * Save a new image into storage and return the new image. * @param UploadedFile $uploadFile - * @param string $type + * @param string $type * @param int $uploadedTo + * @param int|null $resizeWidth + * @param int|null $resizeHeight + * @param bool $keepRatio * @return Image * @throws \BookStack\Exceptions\ImageUploadException - * @throws \Exception */ - public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0) + public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true) { - $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo); + $image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio); $this->loadThumbs($image); return $image; } @@ -208,12 +201,27 @@ class ImageRepo * @return bool * @throws \Exception */ - public function destroyImage(Image $image) + public function destroyImage(Image $image = null) { - $this->imageService->destroy($image); + if ($image) { + $this->imageService->destroy($image); + } return true; } + /** + * Destroy all images of a certain type. + * @param string $imageType + * @throws \Exception + */ + public function destroyByType(string $imageType) + { + $images = $this->image->where('type', '=', $imageType)->get(); + foreach ($images as $image) { + $this->destroyImage($image); + } + } + /** * Load thumbnails onto an image object. @@ -241,7 +249,7 @@ class ImageRepo * @throws \BookStack\Exceptions\ImageUploadException * @throws \Exception */ - public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) + protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) { try { return $this->imageService->getThumbnail($image, $width, $height, $keepRatio); @@ -264,18 +272,6 @@ class ImageRepo } } - /** - * Check if the provided image type is valid. - * @param $type - * @return bool - */ - public function isValidType($type) - { - // TODO - To delete? - $validTypes = ['gallery', 'cover', 'system', 'user']; - return in_array($type, $validTypes); - } - /** * Get the validation rules for image files. * @return string diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 1dd8b713d..63c3b3172 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Filesystem\Factory as FileSystem; use Intervention\Image\Exception\NotSupportedException; use Intervention\Image\ImageManager; +use phpDocumentor\Reflection\Types\Integer; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageService extends UploadService @@ -57,15 +58,30 @@ class ImageService extends UploadService /** * Saves a new image from an upload. * @param UploadedFile $uploadedFile - * @param string $type + * @param string $type * @param int $uploadedTo + * @param int|null $resizeWidth + * @param int|null $resizeHeight + * @param bool $keepRatio * @return mixed * @throws ImageUploadException */ - public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0) + public function saveNewFromUpload( + UploadedFile $uploadedFile, + string $type, + int $uploadedTo = 0, + int $resizeWidth = null, + int $resizeHeight = null, + bool $keepRatio = true + ) { $imageName = $uploadedFile->getClientOriginalName(); $imageData = file_get_contents($uploadedFile->getRealPath()); + + if ($resizeWidth !== null || $resizeHeight !== null) { + $imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio); + } + return $this->saveNew($imageName, $imageData, $type, $uploadedTo); } @@ -122,7 +138,7 @@ class ImageService extends UploadService $secureUploads = setting('app-secure-images'); $imageName = str_replace(' ', '-', $imageName); - $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/'; + $imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/'; while ($storage->exists($imagePath . $imageName)) { $imageName = str_random(3) . $imageName; @@ -201,8 +217,28 @@ class ImageService extends UploadService return $this->getPublicUrl($thumbFilePath); } + $thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio); + + $storage->put($thumbFilePath, $thumbData); + $storage->setVisibility($thumbFilePath, 'public'); + $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); + + return $this->getPublicUrl($thumbFilePath); + } + + /** + * Resize image data. + * @param string $imageData + * @param int $width + * @param int $height + * @param bool $keepRatio + * @return string + * @throws ImageUploadException + */ + protected function resizeImage(string $imageData, $width = 220, $height = null, $keepRatio = true) + { try { - $thumb = $this->imageTool->make($storage->get($imagePath)); + $thumb = $this->imageTool->make($imageData); } catch (Exception $e) { if ($e instanceof \ErrorException || $e instanceof NotSupportedException) { throw new ImageUploadException(trans('errors.cannot_create_thumbs')); @@ -211,20 +247,14 @@ class ImageService extends UploadService } if ($keepRatio) { - $thumb->resize($width, null, function ($constraint) { + $thumb->resize($width, $height, function ($constraint) { $constraint->aspectRatio(); $constraint->upsize(); }); } else { $thumb->fit($width, $height); } - - $thumbData = (string)$thumb->encode(); - $storage->put($thumbFilePath, $thumbData); - $storage->setVisibility($thumbFilePath, 'public'); - $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); - - return $this->getPublicUrl($thumbFilePath); + return (string)$thumb->encode(); } /** diff --git a/resources/assets/js/components/image-picker.js b/resources/assets/js/components/image-picker.js index 7cbed4509..7455fa622 100644 --- a/resources/assets/js/components/image-picker.js +++ b/resources/assets/js/components/image-picker.js @@ -4,54 +4,50 @@ class ImagePicker { constructor(elem) { this.elem = elem; this.imageElem = elem.querySelector('img'); - this.input = elem.querySelector('input'); + this.imageInput = elem.querySelector('input[type=file]'); + this.resetInput = elem.querySelector('input[data-reset-input]'); + this.removeInput = elem.querySelector('input[data-remove-input]'); - this.isUsingIds = elem.getAttribute('data-current-id') !== ''; - this.isResizing = elem.getAttribute('data-resize-height') && elem.getAttribute('data-resize-width'); - this.isResizeCropping = elem.getAttribute('data-resize-crop') !== ''; + this.defaultImage = elem.getAttribute('data-default-image'); - let selectButton = elem.querySelector('button[data-action="show-image-manager"]'); - selectButton.addEventListener('click', this.selectImage.bind(this)); - - let resetButton = elem.querySelector('button[data-action="reset-image"]'); + const resetButton = elem.querySelector('button[data-action="reset-image"]'); resetButton.addEventListener('click', this.reset.bind(this)); - let removeButton = elem.querySelector('button[data-action="remove-image"]'); + const removeButton = elem.querySelector('button[data-action="remove-image"]'); if (removeButton) { removeButton.addEventListener('click', this.removeImage.bind(this)); } + + this.imageInput.addEventListener('change', this.fileInputChange.bind(this)); } - selectImage() { - window.ImageManager.show(image => { - if (!this.isResizing) { - this.setImage(image); - return; - } + fileInputChange() { + this.resetInput.setAttribute('disabled', 'disabled'); + if (this.removeInput) { + this.removeInput.setAttribute('disabled', 'disabled'); + } - let requestString = '/images/thumb/' + image.id + '/' + this.elem.getAttribute('data-resize-width') + '/' + this.elem.getAttribute('data-resize-height') + '/' + (this.isResizeCropping ? 'true' : 'false'); - - window.$http.get(window.baseUrl(requestString)).then(resp => { - image.url = resp.data.url; - this.setImage(image); - }); - }); + for (let file of this.imageInput.files) { + this.imageElem.src = window.URL.createObjectURL(file); + } + this.imageElem.classList.remove('none'); } reset() { - this.setImage({id: 0, url: this.elem.getAttribute('data-default-image')}); - } - - setImage(image) { - this.imageElem.src = image.url; - this.input.value = this.isUsingIds ? image.id : image.url; + this.imageInput.value = ''; + this.imageElem.src = this.defaultImage; + this.resetInput.removeAttribute('disabled'); + if (this.removeInput) { + this.removeInput.setAttribute('disabled', 'disabled'); + } this.imageElem.classList.remove('none'); } removeImage() { - this.imageElem.src = this.elem.getAttribute('data-default-image'); + this.imageInput.value = ''; this.imageElem.classList.add('none'); - this.input.value = 'none'; + this.removeInput.removeAttribute('disabled'); + this.resetInput.setAttribute('disabled', 'disabled'); } } diff --git a/resources/assets/js/components/markdown-editor.js b/resources/assets/js/components/markdown-editor.js index 55cf67813..b099a7ca9 100644 --- a/resources/assets/js/components/markdown-editor.js +++ b/resources/assets/js/components/markdown-editor.js @@ -394,9 +394,7 @@ class MarkdownEditor { const drawingId = imgContainer.getAttribute('drawio-diagram'); DrawIO.show(() => { - return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - return `data:image/png;base64,${resp.data.content}`; - }); + return DrawIO.load(drawingId); }, (pngData) => { let data = { diff --git a/resources/assets/js/components/wysiwyg-editor.js b/resources/assets/js/components/wysiwyg-editor.js index 3f60ff031..b894c3fa6 100644 --- a/resources/assets/js/components/wysiwyg-editor.js +++ b/resources/assets/js/components/wysiwyg-editor.js @@ -299,9 +299,7 @@ function drawIoPlugin() { } let drawingId = currentNode.getAttribute('drawio-diagram'); - return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => { - return `data:image/png;base64,${resp.data.content}`; - }); + return DrawIO.load(drawingId); } window.tinymce.PluginManager.add('drawio', function(editor, url) { diff --git a/resources/assets/js/services/drawio.js b/resources/assets/js/services/drawio.js index 6acf091f0..a570737d1 100644 --- a/resources/assets/js/services/drawio.js +++ b/resources/assets/js/services/drawio.js @@ -75,4 +75,14 @@ async function upload(imageData, pageUploadedToId) { return resp.data; } -export default {show, close, upload}; \ No newline at end of file +/** + * Load an existing image, by fetching it as Base64 from the system. + * @param drawingId + * @returns {Promise} + */ +async function load(drawingId) { + const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`)); + return `data:image/png;base64,${resp.data.content}`; +} + +export default {show, close, upload, load}; \ No newline at end of file diff --git a/resources/assets/sass/_layout.scss b/resources/assets/sass/_layout.scss index 137048935..9bb4e1c70 100644 --- a/resources/assets/sass/_layout.scss +++ b/resources/assets/sass/_layout.scss @@ -140,6 +140,10 @@ body.flexbox { display: inline-block; } +.hidden { + display: none; +} + .float { float: left; &.right { diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 9baeb9f30..210980ac2 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -71,6 +71,7 @@ return [ 'timezone' => 'The :attribute must be a valid zone.', 'unique' => 'The :attribute has already been taken.', 'url' => 'The :attribute format is invalid.', + 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', // Custom validation lines 'custom' => [ diff --git a/resources/views/books/create.blade.php b/resources/views/books/create.blade.php index 882ce556a..40b781441 100644 --- a/resources/views/books/create.blade.php +++ b/resources/views/books/create.blade.php @@ -33,5 +33,4 @@
    - @include('components.image-manager', ['imageType' => 'cover']) @stop \ No newline at end of file diff --git a/resources/views/books/edit.blade.php b/resources/views/books/edit.blade.php index a02029a10..2e51ed6e9 100644 --- a/resources/views/books/edit.blade.php +++ b/resources/views/books/edit.blade.php @@ -16,12 +16,10 @@

    {{ trans('entities.books_edit') }}

    - + @include('books.form', ['model' => $book])
    - - @include('components.image-manager', ['imageType' => 'cover', 'uploaded_to']) @stop \ No newline at end of file diff --git a/resources/views/books/form.blade.php b/resources/views/books/form.blade.php index ebbc279fd..4edec240a 100644 --- a/resources/views/books/form.blade.php +++ b/resources/views/books/form.blade.php @@ -18,13 +18,9 @@

    {{ trans('common.cover_image_description') }}

    @include('components.image-picker', [ - 'resizeHeight' => '512', - 'resizeWidth' => '512', - 'showRemove' => false, 'defaultImage' => baseUrl('/book_default_cover.png'), - 'currentImage' => isset($model) ? $model->getBookCover() : baseUrl('/book_default_cover.png') , - 'currentId' => isset($model) && $model->image_id ? $model->image_id : 0, - 'name' => 'image_id', + 'currentImage' => (isset($model) && $model->cover) ? $model->getBookCover() : baseUrl('/book_default_cover.png') , + 'name' => 'image', 'imageClass' => 'cover' ])
    diff --git a/resources/views/components/image-picker.blade.php b/resources/views/components/image-picker.blade.php index 10df50293..73885aeb4 100644 --- a/resources/views/components/image-picker.blade.php +++ b/resources/views/components/image-picker.blade.php @@ -1,22 +1,32 @@ -
    +
    {{ trans('components.image_preview') }}
    - + + + + + @if(isset($removeName)) + + @endif +
    - @if ($showRemove) + @if(isset($removeName)) | @endif
    - -{{-- TODO - Revamp to be custom file upload button, instead of being linked to image manager--}} -{{-- TODO - Remove image manager use where this is used and clean image manager for drawing/gallery use.--}} + @if($errors->has($name)) +
    {{ $errors->first($name) }}
    + @endif +
    \ No newline at end of file diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php index 582bb078e..2dabe9dec 100644 --- a/resources/views/settings/index.blade.php +++ b/resources/views/settings/index.blade.php @@ -79,7 +79,7 @@

    {{ trans('settings.app_customization') }}

    -
    + {!! csrf_field() !!}
    @@ -119,14 +119,12 @@
    @include('components.image-picker', [ - 'resizeHeight' => '43', - 'resizeWidth' => '200', - 'showRemove' => true, + 'removeName' => 'setting-app-logo', + 'removeValue' => 'none', 'defaultImage' => baseUrl('/logo.png'), 'currentImage' => setting('app-logo'), - 'name' => 'setting-app-logo', + 'name' => 'app_logo', 'imageClass' => 'logo-image', - 'currentId' => false ])
    diff --git a/resources/views/shelves/create.blade.php b/resources/views/shelves/create.blade.php index 72bf904fe..706e15d07 100644 --- a/resources/views/shelves/create.blade.php +++ b/resources/views/shelves/create.blade.php @@ -26,6 +26,4 @@
    - @include('components.image-manager', ['imageType' => 'cover']) - @stop \ No newline at end of file diff --git a/resources/views/shelves/edit.blade.php b/resources/views/shelves/edit.blade.php index fa8a3d63b..8c2cd4f45 100644 --- a/resources/views/shelves/edit.blade.php +++ b/resources/views/shelves/edit.blade.php @@ -16,7 +16,7 @@

    {{ trans('entities.shelves_edit') }}

    - + @include('shelves.form', ['model' => $shelf]) diff --git a/resources/views/shelves/form.blade.php b/resources/views/shelves/form.blade.php index 8a270a894..4191f421d 100644 --- a/resources/views/shelves/form.blade.php +++ b/resources/views/shelves/form.blade.php @@ -53,13 +53,9 @@

    {{ trans('common.cover_image_description') }}

    @include('components.image-picker', [ - 'resizeHeight' => '512', - 'resizeWidth' => '512', - 'showRemove' => false, 'defaultImage' => baseUrl('/book_default_cover.png'), - 'currentImage' => isset($shelf) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , - 'currentId' => isset($shelf) && $shelf->image_id ? $shelf->image_id : 0, - 'name' => 'image_id', + 'currentImage' => (isset($shelf) && $shelf->cover) ? $shelf->getBookCover() : baseUrl('/book_default_cover.png') , + 'name' => 'image', 'imageClass' => 'cover' ])
    diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 7c8175d9a..377500193 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -9,7 +9,7 @@

    {{ $user->id === $currentUser->id ? trans('settings.users_edit_profile') : trans('settings.users_edit') }}

    -
    id}") }}" method="post"> + id}") }}" method="post" enctype="multipart/form-data"> {!! csrf_field() !!} @@ -29,7 +29,7 @@ 'defaultImage' => baseUrl('/user_avatar.png'), 'currentImage' => $user->getAvatar(80), 'currentId' => $user->image_id, - 'name' => 'image_id', + 'name' => 'profile_image', 'imageClass' => 'avatar large' ])
    @@ -87,5 +87,4 @@ @endif
    - @include('components.image-manager', ['imageType' => 'user', 'uploaded_to' => $user->id]) @stop diff --git a/routes/web.php b/routes/web.php index 975ab5b17..25d7ab692 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,7 +6,8 @@ Route::get('/robots.txt', 'HomeController@getRobots'); // Authenticated routes... Route::group(['middleware' => 'auth'], function () { - Route::get('/uploads/images/{path}', 'ImageController@showImage') + // Secure images routing + Route::get('/uploads/images/{path}', 'Images\ImageController@showImage') ->where('path', '.*$'); Route::group(['prefix' => 'pages'], function() { @@ -105,27 +106,19 @@ Route::group(['middleware' => 'auth'], function () { // Image routes Route::group(['prefix' => 'images'], function () { - // TODO - Check auth on these - // TODO - Maybe check types for only gallery or drawing - // Standard get, update and deletion for all types - Route::get('/thumb/{id}/{width}/{height}/{crop}', 'ImageController@getThumbnail'); - Route::get('/base64/{id}', 'ImageController@getBase64Image'); - Route::get('/usage/{id}', 'ImageController@usage'); - // Gallery Route::get('/gallery', 'Images\GalleryImageController@list'); Route::post('/gallery', 'Images\GalleryImageController@create'); // Drawio Route::get('/drawio', 'Images\DrawioImageController@list'); + Route::get('/drawio/base64/{id}', 'Images\DrawioImageController@getAsBase64'); Route::post('/drawio', 'Images\DrawioImageController@create'); - - // TODO - Check auth on these - // TODO - Maybe check types for only gallery or drawing - // Or add to gallery/drawio controllers - Route::put('/{id}', 'ImageController@update'); - Route::delete('/{id}', 'ImageController@destroy'); + // Shared gallery & draw.io endpoint + Route::get('/usage/{id}', 'Images\ImageController@usage'); + Route::put('/{id}', 'Images\ImageController@update'); + Route::delete('/{id}', 'Images\ImageController@destroy'); }); // Attachments routes From 8c190324ac5f8debdb5ee643a38a36667d977bfd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 4 May 2019 18:11:00 +0100 Subject: [PATCH 151/160] Updated existing image tests to reflect changes - Also added some new tests --- app/Entities/Repos/EntityRepo.php | 2 +- .../Controllers/Images/ImageController.php | 2 +- app/Http/Controllers/UserController.php | 1 - app/Uploads/ImageRepo.php | 4 +- app/Uploads/ImageService.php | 3 +- tests/Uploads/ImageTest.php | 195 ++++++++++-------- tests/Uploads/UsesImages.php | 31 ++- 7 files changed, 146 insertions(+), 92 deletions(-) diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 88ca1bca6..1cff46da1 100644 --- a/app/Entities/Repos/EntityRepo.php +++ b/app/Entities/Repos/EntityRepo.php @@ -762,7 +762,7 @@ class EntityRepo */ public function searchForImage($imageString) { - $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(); + $pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']); foreach ($pages as $page) { $page->url = $page->getUrl(); $page->html = ''; diff --git a/app/Http/Controllers/Images/ImageController.php b/app/Http/Controllers/Images/ImageController.php index a76fb5e53..024003f87 100644 --- a/app/Http/Controllers/Images/ImageController.php +++ b/app/Http/Controllers/Images/ImageController.php @@ -103,7 +103,7 @@ class ImageController extends Controller */ protected function checkImagePermission(Image $image) { - if ($image->type !== 'drawio' || $image->type !== 'gallery') { + if ($image->type !== 'drawio' && $image->type !== 'gallery') { $this->showPermissionError(); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index c7a7c5646..1367f631b 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -98,7 +98,6 @@ class UserController extends Controller $this->userRepo->setUserRoles($user, $roles); } - // TODO - Check this uses new profile assignment $this->userRepo->downloadAndAssignUserAvatar($user); return redirect('/settings/users'); diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index dbf652ddf..16d1bb3d1 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -232,8 +232,8 @@ class ImageRepo protected function loadThumbs(Image $image) { $image->thumbs = [ - 'gallery' => $this->getThumbnail($image, 150, 150), - 'display' => $this->getThumbnail($image, 840, 0, true) + 'gallery' => $this->getThumbnail($image, 150, 150, false), + 'display' => $this->getThumbnail($image, 840, null, true) ]; } diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index 63c3b3172..eaf787c9c 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -235,7 +235,7 @@ class ImageService extends UploadService * @return string * @throws ImageUploadException */ - protected function resizeImage(string $imageData, $width = 220, $height = null, $keepRatio = true) + protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true) { try { $thumb = $this->imageTool->make($imageData); @@ -336,6 +336,7 @@ class ImageService extends UploadService $image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName); $image->created_by = $user->id; $image->updated_by = $user->id; + $image->uploaded_to = $user->id; $image->save(); return $image; diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index c2e21f95c..01bf23d5b 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -17,14 +17,10 @@ class ImageTest extends TestCase $admin = $this->getAdmin(); $this->actingAs($admin); - $imageName = 'first-image.png'; - $relPath = $this->getTestImagePath('gallery', $imageName); - $this->deleteImage($relPath); + $imgDetails = $this->uploadGalleryImage($page); + $relPath = $imgDetails['path']; - $upload = $this->uploadImage($imageName, $page->id); - $upload->assertStatus(200); - - $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image not found at path: '. public_path($relPath)); + $this->assertTrue(file_exists(public_path($relPath)), 'Uploaded image found at path: '. public_path($relPath)); $this->deleteImage($relPath); @@ -35,10 +31,93 @@ class ImageTest extends TestCase 'path' => $relPath, 'created_by' => $admin->id, 'updated_by' => $admin->id, - 'name' => $imageName + 'name' => $imgDetails['name'], ]); } + public function test_image_edit() + { + $editor = $this->getEditor(); + $this->actingAs($editor); + + $imgDetails = $this->uploadGalleryImage(); + $image = Image::query()->first(); + + $newName = str_random(); + $update = $this->put('/images/' . $image->id, ['name' => $newName]); + $update->assertSuccessful(); + $update->assertJson([ + 'id' => $image->id, + 'name' => $newName, + 'type' => 'gallery', + ]); + + $this->deleteImage($imgDetails['path']); + + $this->assertDatabaseHas('images', [ + 'type' => 'gallery', + 'name' => $newName + ]); + } + + public function test_gallery_get_list_format() + { + $this->asEditor(); + + $imgDetails = $this->uploadGalleryImage(); + $image = Image::query()->first(); + + $emptyJson = ['images' => [], 'has_more' => false]; + $resultJson = [ + 'images' => [ + [ + 'id' => $image->id, + 'name' => $imgDetails['name'], + ] + ], + 'has_more' => false, + ]; + + $pageId = $imgDetails['page']->id; + $firstPageRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}"); + $firstPageRequest->assertSuccessful()->assertJson($resultJson); + + $secondPageRequest = $this->get("/images/gallery?page=2&uploaded_to={$pageId}"); + $secondPageRequest->assertSuccessful()->assertExactJson($emptyJson); + + $namePartial = substr($imgDetails['name'], 0, 3); + $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}"); + $searchHitRequest->assertSuccessful()->assertJson($resultJson); + + $namePartial = str_random(16); + $searchHitRequest = $this->get("/images/gallery?page=1&uploaded_to={$pageId}&search={$namePartial}"); + $searchHitRequest->assertSuccessful()->assertExactJson($emptyJson); + } + + public function test_image_usage() + { + $page = Page::first(); + $editor = $this->getEditor(); + $this->actingAs($editor); + + $imgDetails = $this->uploadGalleryImage($page); + + $image = Image::query()->first(); + $page->html = ''; + $page->save(); + + $usage = $this->get('/images/usage/' . $image->id); + $usage->assertSuccessful(); + $usage->assertJson([ + [ + 'id' => $page->id, + 'name' => $page->name + ] + ]); + + $this->deleteImage($imgDetails['path']); + } + public function test_php_files_cannot_be_uploaded() { $page = Page::first(); @@ -50,7 +129,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); @@ -72,7 +151,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped'); @@ -89,7 +168,7 @@ class ImageTest extends TestCase $this->deleteImage($relPath); $file = $this->getTestImage($fileName); - $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []); + $upload = $this->withHeader('Content-Type', 'image/png')->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $file], []); $upload->assertStatus(302); $this->assertFalse(file_exists(public_path($relPath)), 'Uploaded double extension file was uploaded but should have been stopped'); @@ -101,9 +180,9 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); - $upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); + $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $upload->assertStatus(200); $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); @@ -119,9 +198,9 @@ class ImageTest extends TestCase $this->asEditor(); $galleryFile = $this->getTestImage('my-secure-test-upload.png'); $page = Page::first(); - $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png'); + $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m') . '/my-secure-test-upload.png'); - $upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); + $upload = $this->call('POST', '/images/gallery', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); $imageUrl = json_decode($upload->getContent(), true)['url']; $page->html .= ""; $page->save(); @@ -139,13 +218,12 @@ class ImageTest extends TestCase public function test_system_images_remain_public() { config()->set('filesystems.default', 'local_secure'); - $this->asEditor(); + $this->asAdmin(); $galleryFile = $this->getTestImage('my-system-test-upload.png'); - $page = Page::first(); - $expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png'); + $expectedPath = public_path('uploads/images/system/' . Date('Y-m') . '/my-system-test-upload.png'); - $upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []); - $upload->assertStatus(200); + $upload = $this->call('POST', '/settings', [], [], ['app_logo' => $galleryFile], []); + $upload->assertRedirect('/settings'); $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: '. $expectedPath); @@ -183,8 +261,10 @@ class ImageTest extends TestCase $this->uploadImage($imageName, $page->id); $image = Image::first(); + $image->type = 'drawio'; + $image->save(); - $imageGet = $this->getJson("/images/base64/{$image->id}"); + $imageGet = $this->getJson("/images/drawio/base64/{$image->id}"); $imageGet->assertJson([ 'content' => 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' ]); @@ -196,7 +276,7 @@ class ImageTest extends TestCase $editor = $this->getEditor(); $this->actingAs($editor); - $upload = $this->postJson('images/drawing/upload', [ + $upload = $this->postJson('images/drawio', [ 'uploaded_to' => $page->id, 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=' ]); @@ -233,7 +313,7 @@ class ImageTest extends TestCase $this->actingAs($admin); $file = $this->getTestProfileImage(); - $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); + $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); $this->assertDatabaseHas('images', [ 'type' => 'user', @@ -242,78 +322,23 @@ class ImageTest extends TestCase ]); } - public function test_standard_user_with_manage_users_permission_can_view_other_profile_images() - { - $editor = $this->getEditor(); - $this->giveUserPermissions($editor, ['users-manage']); - - $admin = $this->getAdmin(); - - $this->actingAs($admin); - $file = $this->getTestProfileImage(); - $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); - - $expectedJson = [ - 'name' => 'profile.png', - 'uploaded_to' => $admin->id, - 'type' => 'user' - ]; - - $this->actingAs($editor); - $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id); - $adminImagesGet->assertStatus(200)->assertJsonFragment($expectedJson); - - $allImagesGet = $this->get("/images/user/all/0"); - $allImagesGet->assertStatus(200)->assertJsonFragment($expectedJson); - } - - public function test_standard_user_cant_view_other_profile_images() - { - $editor = $this->getEditor(); - $admin = $this->getAdmin(); - - $this->actingAs($admin); - $file = $this->getTestProfileImage(); - $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); - - $this->actingAs($editor); - $adminImagesGet = $this->get("/images/user/all/0?uploaded_to=" . $admin->id); - $adminImagesGet->assertStatus(302); - - $allImagesGet = $this->get("/images/user/all/0"); - $allImagesGet->assertStatus(302); - } - - public function test_standard_user_cant_upload_other_profile_images() - { - $editor = $this->getEditor(); - $admin = $this->getAdmin(); - - $this->actingAs($editor); - $file = $this->getTestProfileImage(); - $upload = $this->call('POST', '/images/user/upload', ['uploaded_to' => $admin->id], [], ['file' => $file], []); - $upload->assertStatus(302); - - $this->assertDatabaseMissing('images', [ - 'type' => 'user', - 'uploaded_to' => $admin->id, - ]); - } - public function test_user_images_deleted_on_user_deletion() { $editor = $this->getEditor(); $this->actingAs($editor); $file = $this->getTestProfileImage(); - $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); - $this->call('POST', '/images/user/upload', ['uploaded_to' => $editor->id], [], ['file' => $file], []); + $this->call('PUT', '/settings/users/' . $editor->id, [], [], ['profile_image' => $file], []); $profileImages = Image::where('type', '=', 'user')->where('created_by', '=', $editor->id)->get(); - $this->assertTrue($profileImages->count() === 2, "Found profile images does not match upload count"); + $this->assertTrue($profileImages->count() === 1, "Found profile images does not match upload count"); + + $imagePath = public_path($profileImages->first()->path); + $this->assertTrue(file_exists($imagePath)); $userDelete = $this->asAdmin()->delete("/settings/users/{$editor->id}"); $userDelete->assertStatus(302); + $this->assertDatabaseMissing('images', [ 'type' => 'user', 'created_by' => $editor->id @@ -322,6 +347,8 @@ class ImageTest extends TestCase 'type' => 'user', 'uploaded_to' => $editor->id ]); + + $this->assertFalse(file_exists($imagePath)); } public function test_deleted_unused_images() diff --git a/tests/Uploads/UsesImages.php b/tests/Uploads/UsesImages.php index 93bf278e2..aa5ffe4c7 100644 --- a/tests/Uploads/UsesImages.php +++ b/tests/Uploads/UsesImages.php @@ -1,6 +1,7 @@ getTestImage($name); return $this->withHeader('Content-Type', $contentType) - ->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + ->call('POST', '/images/gallery', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []); + } + + /** + * Upload a new gallery image. + * Returns the image name. + * Can provide a page to relate the image to. + * @param Page|null $page + * @return array + */ + protected function uploadGalleryImage(Page $page = null) + { + if ($page === null) { + $page = Page::query()->first(); + } + + $imageName = 'first-image.png'; + $relPath = $this->getTestImagePath('gallery', $imageName); + $this->deleteImage($relPath); + + $upload = $this->uploadImage($imageName, $page->id); + $upload->assertStatus(200); + return [ + 'name' => $imageName, + 'path' => $relPath, + 'page' => $page + ]; } /** From ad542f0407b0f8112b43a48b6a0b25a161ad329f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 5 May 2019 13:53:37 +0100 Subject: [PATCH 152/160] Prevented potential inline JS event usage - Removes 'on*' attributes from elements. - Also updated script logic to remove scripts instead of escaping. - All JS injection removal now uses DomDocument + xpath parsing. --- app/Entities/Repos/EntityRepo.php | 95 ++++++++++++++++++++----------- tests/Entity/PageContentTest.php | 39 +++++++++++-- 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/app/Entities/Repos/EntityRepo.php b/app/Entities/Repos/EntityRepo.php index 1cff46da1..a0934530f 100644 --- a/app/Entities/Repos/EntityRepo.php +++ b/app/Entities/Repos/EntityRepo.php @@ -1,5 +1,6 @@ .*?<\/script>/ms'; - $matches = []; - preg_match_all($scriptSearchRegex, $html, $matches); - - foreach ($matches[0] as $match) { - $html = str_replace($match, htmlentities($match), $html); + if ($html == '') { + return $html; } + + libxml_use_internal_errors(true); + $doc = new DOMDocument(); + $doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8')); + $xPath = new DOMXPath($doc); + + // Remove standard script tags + $scriptElems = $xPath->query('//body//*//script'); + foreach ($scriptElems as $scriptElem) { + $scriptElem->parentNode->removeChild($scriptElem); + } + + // Remove 'on*' attributes + $onAttributes = $xPath->query('//body//*/@*[starts-with(name(), \'on\')]'); + foreach ($onAttributes as $attr) { + /** @var \DOMAttr $attr*/ + $attrName = $attr->nodeName; + $attr->parentNode->removeAttribute($attrName); + } + + $html = ''; + $topElems = $doc->documentElement->childNodes->item(0)->childNodes; + foreach ($topElems as $child) { + $html .= $doc->saveHTML($child); + } + return $html; } @@ -773,8 +800,8 @@ class EntityRepo /** * Destroy a bookshelf instance - * @param \BookStack\Entities\Bookshelf $shelf - * @throws \Throwable + * @param Bookshelf $shelf + * @throws Throwable */ public function destroyBookshelf(Bookshelf $shelf) { @@ -784,9 +811,9 @@ class EntityRepo /** * Destroy the provided book and all its child entities. - * @param \BookStack\Entities\Book $book + * @param Book $book * @throws NotifyException - * @throws \Throwable + * @throws Throwable */ public function destroyBook(Book $book) { @@ -802,8 +829,8 @@ class EntityRepo /** * Destroy a chapter and its relations. - * @param \BookStack\Entities\Chapter $chapter - * @throws \Throwable + * @param Chapter $chapter + * @throws Throwable */ public function destroyChapter(Chapter $chapter) { @@ -821,7 +848,7 @@ class EntityRepo * Destroy a given page along with its dependencies. * @param Page $page * @throws NotifyException - * @throws \Throwable + * @throws Throwable */ public function destroyPage(Page $page) { @@ -844,12 +871,12 @@ class EntityRepo /** * Destroy or handle the common relations connected to an entity. - * @param \BookStack\Entities\Entity $entity - * @throws \Throwable + * @param Entity $entity + * @throws Throwable */ protected function destroyEntityCommonRelations(Entity $entity) { - \Activity::removeEntity($entity); + Activity::removeEntity($entity); $entity->views()->delete(); $entity->permissions()->delete(); $entity->tags()->delete(); @@ -861,9 +888,9 @@ class EntityRepo /** * Copy the permissions of a bookshelf to all child books. * Returns the number of books that had permissions updated. - * @param \BookStack\Entities\Bookshelf $bookshelf + * @param Bookshelf $bookshelf * @return int - * @throws \Throwable + * @throws Throwable */ public function copyBookshelfPermissions(Bookshelf $bookshelf) { diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index 88169c50d..6201cf5d7 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -71,17 +71,30 @@ class PageContentTest extends TestCase $pageResp->assertSee($content); } - public function test_page_content_scripts_escaped_by_default() + public function test_page_content_scripts_removed_by_default() { $this->asEditor(); $page = Page::first(); - $script = ''; + $script = 'abc123abc123'; $page->html = "escape {$script}"; $page->save(); $pageView = $this->get($page->getUrl()); $pageView->assertDontSee($script); - $pageView->assertSee(htmlentities($script)); + $pageView->assertSee('abc123abc123'); + } + + public function test_page_inline_on_attributes_removed_by_default() + { + $this->asEditor(); + $page = Page::first(); + $script = '

    Hello

    '; + $page->html = "escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertDontSee($script); + $pageView->assertSee('

    Hello

    '); } public function test_page_content_scripts_show_when_configured() @@ -89,13 +102,29 @@ class PageContentTest extends TestCase $this->asEditor(); $page = Page::first(); config()->push('app.allow_content_scripts', 'true'); - $script = ''; + + $script = 'abc123abc123'; $page->html = "no escape {$script}"; $page->save(); $pageView = $this->get($page->getUrl()); $pageView->assertSee($script); - $pageView->assertDontSee(htmlentities($script)); + $pageView->assertDontSee('abc123abc123'); + } + + public function test_page_inline_on_attributes_show_if_configured() + { + $this->asEditor(); + $page = Page::first(); + config()->push('app.allow_content_scripts', 'true'); + + $script = '

    Hello

    '; + $page->html = "escape {$script}"; + $page->save(); + + $pageView = $this->get($page->getUrl()); + $pageView->assertSee($script); + $pageView->assertDontSee('

    Hello

    '); } public function test_duplicate_ids_does_not_break_page_render() From adc866cb3db7ba05b58d7c3169ebc96b5dd89ba1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 5 May 2019 14:43:26 +0100 Subject: [PATCH 153/160] Added ability for dropdown menu to be bottom of dom body - Used when a dropdown is within a scrollable section such as editor toolbar on mobile. - Also made mobile page save button more obvious by increasing size and inverting color. --- resources/assets/js/components/dropdown.js | 36 +++++++++++++++++-- .../assets/js/components/list-sort-control.js | 9 +++-- resources/assets/sass/_lists.scss | 2 +- resources/assets/sass/_pages.scss | 19 +++++----- resources/views/books/show.blade.php | 2 +- resources/views/chapters/show.blade.php | 2 +- resources/views/comments/comment.blade.php | 2 +- resources/views/common/header.blade.php | 2 +- resources/views/pages/form.blade.php | 11 +++--- resources/views/pages/revisions.blade.php | 4 +-- resources/views/pages/show.blade.php | 2 +- resources/views/partials/sort.blade.php | 8 ++--- 12 files changed, 66 insertions(+), 33 deletions(-) diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js index 400ddb576..ce797bbeb 100644 --- a/resources/assets/js/components/dropdown.js +++ b/resources/assets/js/components/dropdown.js @@ -6,24 +6,54 @@ class DropDown { constructor(elem) { this.container = elem; - this.menu = elem.querySelector('ul, [dropdown-menu]'); + this.menu = elem.querySelector('.dropdown-menu, [dropdown-menu]'); + this.moveMenu = elem.hasAttribute('dropdown-move-menu'); this.toggle = elem.querySelector('[dropdown-toggle]'); + this.body = document.body; this.setupListeners(); } - show() { + show(event) { + this.hide(); + this.menu.style.display = 'block'; this.menu.classList.add('anim', 'menuIn'); - this.container.addEventListener('mouseleave', this.hide.bind(this)); + + if (this.moveMenu) { + // Move to body to prevent being trapped within scrollable sections + this.rect = this.menu.getBoundingClientRect(); + this.body.appendChild(this.menu); + this.menu.style.position = 'fixed'; + this.menu.style.left = `${this.rect.left}px`; + this.menu.style.top = `${this.rect.top}px`; + this.menu.style.width = `${this.rect.width}px`; + } + + // Set listener to hide on mouse leave or window click + this.menu.addEventListener('mouseleave', this.hide.bind(this)); + window.addEventListener('click', event => { + if (!this.menu.contains(event.target)) { + this.hide(); + } + }); // Focus on first input if existing let input = this.menu.querySelector('input'); if (input !== null) input.focus(); + + event.stopPropagation(); } hide() { this.menu.style.display = 'none'; this.menu.classList.remove('anim', 'menuIn'); + if (this.moveMenu) { + this.menu.style.position = ''; + this.menu.style.left = ''; + this.menu.style.top = ''; + this.menu.style.width = ''; + this.container.appendChild(this.menu); + } } setupListeners() { diff --git a/resources/assets/js/components/list-sort-control.js b/resources/assets/js/components/list-sort-control.js index d463ed0b7..23fc64ae6 100644 --- a/resources/assets/js/components/list-sort-control.js +++ b/resources/assets/js/components/list-sort-control.js @@ -6,20 +6,23 @@ class ListSortControl { constructor(elem) { this.elem = elem; + this.menu = elem.querySelector('ul'); this.sortInput = elem.querySelector('[name="sort"]'); this.orderInput = elem.querySelector('[name="order"]'); this.form = elem.querySelector('form'); - this.elem.addEventListener('click', event => { + this.menu.addEventListener('click', event => { if (event.target.closest('[data-sort-value]') !== null) { this.sortOptionClick(event); } + }); + + this.elem.addEventListener('click', event => { if (event.target.closest('[data-sort-dir]') !== null) { this.sortDirectionClick(event); } - }) - + }); } sortOptionClick(event) { diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss index dc4dc8816..565c3f0f8 100644 --- a/resources/assets/sass/_lists.scss +++ b/resources/assets/sass/_lists.scss @@ -510,7 +510,7 @@ ul.pagination { position: relative; } -.dropdown-container ul { +.dropdown-menu { display: none; position: absolute; z-index: 999; diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss index 969682c3b..e3b1deb5b 100755 --- a/resources/assets/sass/_pages.scss +++ b/resources/assets/sass/_pages.scss @@ -20,7 +20,7 @@ } } -@include smaller-than($l) { +@include smaller-than($m) { .page-edit-toolbar { overflow-x: scroll; overflow-y: visible; @@ -35,18 +35,21 @@ } } -@include smaller-than($l) { +@include smaller-than($m) { .page-edit-toolbar #save-button { position: fixed; z-index: 30; - background-color: #FFF; border-radius: 50%; - width: 42px; - height: 42px; - font-size: 16px; + width: 56px; + height: 56px; + font-size: 24px; right: $-m; - bottom: $-xs; - box-shadow: $bs-med; + bottom: $-s; + box-shadow: $bs-hover; + background-color: currentColor; + svg { + fill: #FFF; + } span { display: none; } diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index 3141f5ab2..b709b29dc 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -126,7 +126,7 @@ @icon('export') {{ trans('entities.export') }}
    -
    -
      +