Added the Media tab, minor update in .htaccess to allow temporary public directory to be accessible; implemented the basic UI components and navigation; implemented grid, list and tiles view modes; implemented drag-select interface; implemented Media Library cache refreshing; implemented thumbnail generating for local and remote media files; fixed memory leak in third-party Flot Resize library; minor update in the AJAX framework - AJAX request cancelling is not considered as an error anymore; added back-end UI components for creating panels.

This commit is contained in:
alekseybobkov 2015-03-15 12:52:03 -07:00
parent 1c273f28ba
commit 18e058ad59
49 changed files with 18816 additions and 3823 deletions

View File

@ -21,6 +21,7 @@
RewriteRule ^vendor/.* index.php [L,NC]
RewriteRule ^storage/cms/.* index.php [L,NC]
RewriteRule ^storage/logs/.* index.php [L,NC]
RewriteCond %{REQUEST_URI} !storage/temp/public
RewriteRule ^storage/temp/.* index.php [L,NC]
RewriteRule ^storage/framework/.* index.php [L,NC]

View File

@ -29,7 +29,8 @@
"october/system": "~1.0",
"october/backend": "~1.0",
"october/cms": "~1.0",
"october/rain": "~1.0"
"october/rain": "~1.0",
"league/flysystem-aws-s3-v2": "~1.0"
},
"autoload-dev": {
"classmap": [

File diff suppressed because it is too large Load Diff

View File

@ -1833,10 +1833,10 @@ if(typeof item.series.data[item.dataIndex][1]==='number'){content=this.adjustVal
if(typeof item.series.xaxis.tickFormatter!=='undefined'){content=content.replace(xPattern,item.series.xaxis.tickFormatter(item.series.data[item.dataIndex][0],item.series.xaxis));}
if(typeof item.series.yaxis.tickFormatter!=='undefined'){content=content.replace(yPattern,item.series.yaxis.tickFormatter(item.series.data[item.dataIndex][1],item.series.yaxis));}
return content;};FlotTooltip.prototype.isTimeMode=function(axisName,item){return(typeof item.series[axisName].options.mode!=='undefined'&&item.series[axisName].options.mode==='time');};FlotTooltip.prototype.isXDateFormat=function(item){return(typeof this.tooltipOptions.xDateFormat!=='undefined'&&this.tooltipOptions.xDateFormat!==null);};FlotTooltip.prototype.isYDateFormat=function(item){return(typeof this.tooltipOptions.yDateFormat!=='undefined'&&this.tooltipOptions.yDateFormat!==null);};FlotTooltip.prototype.timestampToDate=function(tmst,dateFormat){var theDate=new Date(tmst);return $.plot.formatDate(theDate,dateFormat);};FlotTooltip.prototype.adjustValPrecision=function(pattern,content,value){var precision;if(content.match(pattern)!==null){if(RegExp.$1!==''){precision=RegExp.$1;value=value.toFixed(precision);content=content.replace(pattern,value);}}
return content;};var init=function(plot){new FlotTooltip(plot);};$.plot.plugins.push({init:init,options:defaultOptions,name:'tooltip',version:'0.6.1'});})(jQuery);(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)
return content;};var init=function(plot){new FlotTooltip(plot);};$.plot.plugins.push({init:init,options:defaultOptions,name:'tooltip',version:'0.6.1'});})(jQuery);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)
return;plot.resize();plot.setupGrid();plot.draw();}
function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize);}
function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize);}
function bindEvents(plot,eventHolder){$(window).bind('resize',onResize)}
function shutdown(plot,eventHolder){$(window).unbind('resize',onResize)}
plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown);}
$.plot.plugins.push({init:init,options:options,name:'resize',version:'1.0'});})(jQuery);(function($){var options={xaxis:{timezone:null,timeformat:null,twelveHourClock:false,monthNames:null}};function floorInBase(n,base){return base*Math.floor(n/base);}
function formatDate(d,fmt,monthNames,dayNames){if(typeof d.strftime=="function"){return d.strftime(fmt);}

View File

@ -28,6 +28,11 @@
}
}
.btn-default.on {
background-color: #95a5a6;
color: #f9f9f9;
}
.btn-group {
.btn{
border-right: 1px solid rgba(0,0,0,0.09);
@ -62,6 +67,12 @@
}
}
.btn, .btn-group {
&.offset-right {
margin-right: 10px;
}
}
.btn-icon {
display: inline-block;
height: 36px;
@ -76,12 +87,32 @@
}
&:hover:before {
color: @color-link;
}
&.danger:hover:before {
color: @color-btn-danger;
}
&.pull-right:before {
margin-right: 0;
}
&.margin-left {
margin-left: 5px;
}
&.small {
font-size: 17px;
height: 17px;
line-height: 15px;
}
&.larger {
font-size: 21px;
height: 21px;
line-height: 17px;
}
}
.btn-text {

View File

@ -224,6 +224,17 @@ label {
}
}
.field-section {
border-bottom: 1px solid @color-form-field-border;
padding-top: 3px;
padding-bottom: 7px;
> p:first-child,
> h4:first-child {
margin: 0;
}
}
.field-textarea {
resize: vertical;
&.size-tiny { min-height: @size-tiny; }
@ -233,6 +244,12 @@ label {
&.size-giant { min-height: @size-giant; }
}
.field-checkboxlist {
.checkbox:last-of-type {
margin-bottom: 0;
}
}
.field-checkboxlist-scrollable {
background: white;
border: 1px solid @color-list-border;

View File

@ -169,11 +169,11 @@ table.table.data {
border-left: 3px solid @color-list-stripe-active;
}
}
tr:not(.no-data):hover td {
tr:not(.no-data):hover td, tr:not(.no-data).selected td, {
background: @color-list-hover-bg !important;
color: white;
a, span {
a, span, i[class^="icon-"] {
color: white;
}
}
@ -242,6 +242,22 @@ table.table.data {
padding-left: 0;
padding-right: 0;
}
&.icons {
td i[class^="icon-"] {
display: inline-block;
margin-right: 7px;
font-size: 15px;
color: #95a5a6;
position: relative;
top: 1px;
}
}
&.clickable {
cursor: pointer;
.user-select(none);
}
}
tfoot {

View File

@ -0,0 +1,25 @@
table.name-value-list {
border-collapse: collapse;
font-size: 13px;
th, td {
padding: 4px 0 4px 0;
}
tr:first-child {
th, td {
padding-top: 0;
}
}
th {
font-weight: 600;
color: #95a5a6;
padding-right: 15px;
text-transform: uppercase;
}
td {
color: #2b3e50;
}
}

View File

@ -0,0 +1,73 @@
div.panel {
@panel-border-color: #ecf0f1;
padding: 20px;
&.no-padding {
padding: 0;
}
&.no-padding-bottom {
padding-bottom: 0;
}
&.padding-top {
padding-top: 20px;
}
&.padding-less {
padding: 15px;
}
&.transparent {
background: transparent;
}
&.border-left {
border-left: 1px solid @panel-border-color;
}
&.border-right {
border-right: 1px solid @panel-border-color;
}
&.border-bottom {
border-bottom: 1px solid @panel-border-color;
}
&.triangle-down {
position: relative;
&:after {
.triangle(down, 15px, 8px, white);
position: absolute;
left: 15px;
bottom: -8px;
z-index: 101;
}
&:before {
.triangle(down, 17px, 9px, #edeeef);
position: absolute;
left: 14px;
bottom: -9px;
z-index: 100;
}
}
/*
* Panel sections
*/
h3.section, > label {
text-transform: uppercase;
color: #95a5a6;
font-size: 13px;
font-weight: 600;
margin: 0 0 15px 0;
}
> label {
margin-bottom: 5px;
}
}

View File

@ -0,0 +1,34 @@
.nav.selector-group {
font-size: 13px;
letter-spacing: 0.01em;
li {
a {
padding: 7px 20px 7px 23px;
color: #95a5a6;
}
&.active {
border-left: 3px solid #e6802b;
padding-left: 0;
a {
padding-left: 20px;
color: #2b3e50;
}
}
i[class^="icon-"] {
font-size: 17px;
margin-right: 6px;
position: relative;
top: 1px;
}
}
}
div.panel {
.nav.selector-group {
margin: 0 -20px 0 -20px;
}
}

View File

@ -8,6 +8,10 @@
position: relative;
.horizontal-scroll-indicators(@color-scroll-indicator);
&.standalone-paddings {
padding: 20px;
}
&:before {
left: -10px;
}
@ -22,6 +26,10 @@
white-space: nowrap;
margin-right: 20px;
&.last {
margin-right: 0;
}
.horizontal-scroll-indicators(@color-scroll-indicator);
&:before { left: -10px; }

View File

@ -0,0 +1,61 @@
ul.tree-path {
list-style: none;
padding: 0;
margin-bottom: 0;
li {
display: inline-block;
margin-right: 1px;
font-size: 13px;
&:after {
.icon(@angle-right);
display: inline-block;
font-size: 13px;
margin-left: 5px;
position: relative;
top: 1px;
color: #95a5a6;
}
&:last-child {
a {
cursor: default;
}
&:after {
display: none;
}
}
&.go-up {
font-size: 12px;
margin-right: 7px;
a {
color: #95a5a6;
&:hover {
color: @color-link;
}
}
&:after {
display: none;
}
}
&.root a {
font-weight: 600;
color: #405261;
}
a {
color: #95a5a6;
&:hover {
text-decoration: none;
}
}
}
}

View File

@ -17,6 +17,11 @@ body.loading, body.loading * {
cursor: wait !important;
}
body.no-select {
.user-select(none);
cursor: default!important;
}
//
// Layout canvas
//
@ -114,6 +119,10 @@ body {
}
}
.whiteboard {
background: white;
}
//
// Layout styles
//

View File

@ -46,6 +46,10 @@
@import "controls/treeview.less";
@import "controls/callout.less";
@import "controls/sidenav-tree.less";
@import "controls/panels.less";
@import "controls/selector-group.less";
@import "controls/tree-path.less";
@import "controls/namevaluelist.less";
// Vendor
@import "../vendor/sweet-alert/sweet-alert.less";

View File

@ -20,7 +20,14 @@ can just fix the size of their placeholders.
* http://benalman.com/about/license/
*/
(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);
/*
* The plugin depends on jQuery.Resize plugin https://github.com/cowboy/jquery-resize
* which causes the memory leaking. The plugin dependency was replaced with the native
* window.resize event as we don't need the jQuery.Resize functionality anyways.
* -ab March 1, 2015
*/
// (function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this);
(function ($) {
var options = { }; // no options
@ -40,11 +47,14 @@ can just fix the size of their placeholders.
}
function bindEvents(plot, eventHolder) {
plot.getPlaceholder().resize(onResize);
//plot.getPlaceholder().resize(onResize);
$(window).bind('resize', onResize)
}
function shutdown(plot, eventHolder) {
plot.getPlaceholder().unbind("resize", onResize);
//plot.getPlaceholder().unbind("resize", onResize);
$(window).unbind('resize', onResize)
}
plot.hooks.bindEvents.push(bindEvents);

View File

@ -114,7 +114,7 @@ class FilterScope
protected function evalConfig($config)
{
if (isset($config['options'])) {
$this->options($config['options']);
$this->options = $config['options'];
}
if (isset($config['context'])) {
$this->context = $config['context'];

View File

@ -170,6 +170,10 @@ class Filter extends WidgetBase
*/
protected function getAvailableOptions($scope, $searchQuery = null)
{
if (count($scope->options)) {
return $scope->options;
}
$available = [];
$nameColumn = $this->getScopeNameColumn($scope);
$options = $this->getOptionsFromModel($scope, $searchQuery);

View File

@ -3,9 +3,9 @@
use App;
use Str;
use Lang;
use Form as FormHelper;
use Input;
use Event;
use Form as FormHelper;
use Backend\Classes\FormTabs;
use Backend\Classes\FormField;
use Backend\Classes\WidgetBase;
@ -23,7 +23,6 @@ use October\Rain\Database\Model;
*/
class Form extends WidgetBase
{
/**
* {@inheritDoc}
*/

View File

@ -1,6 +1,6 @@
<?php if (!$field->hidden): ?>
<?php if (in_array($field->type, ['checkbox', 'switch'])): ?>
<?php if (in_array($field->type, ['checkbox', 'switch', 'section'])): ?>
<?= $this->makePartial('field_'.$field->type, ['field' => $field]) ?>

View File

@ -5,85 +5,86 @@
<!-- Checkbox List -->
<?php if (count($fieldOptions)): ?>
<?php if ($this->previewMode): ?>
<!-- Read-only -->
<div class="field-checkboxlist">
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
if (!in_array($value, $checkedValues)) continue;
if (is_string($option)) $option = [$option];
?>
<div class="checkbox custom-checkbox">
<input
type="checkbox"
id="<?= $checkboxId ?>"
name="<?= $field->getName() ?>[]"
value="<?= $value ?>"
disabled="disabled"
checked="checked">
<?php if ($this->previewMode): ?>
<label for="<?= $checkboxId ?>">
<?= e(trans($option[0])) ?>
</label>
<?php if (isset($option[1])): ?>
<p class="help-block"><?= e(trans($option[1])) ?></p>
<?php endif ?>
</div>
<?php endforeach ?>
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
if (!in_array($value, $checkedValues)) continue;
if (is_string($option)) $option = [$option];
?>
<div class="checkbox custom-checkbox">
<input
type="checkbox"
id="<?= $checkboxId ?>"
name="<?= $field->getName() ?>[]"
value="<?= $value ?>"
disabled="disabled"
checked="checked">
<?php else: ?>
<!-- Editable -->
<?php if (count($fieldOptions) > 10): ?>
<!-- Quick selection -->
<small>
<?= e(trans('backend::lang.form.select')) ?>:
<a href="javascript:;" onclick="jQuery('#<?= $field->getId('scrollable') ?> input[type=checkbox]').prop('checked', true)"><?= e(trans('backend::lang.form.select_all')) ?></a>,
<a href="javascript:;" onclick="jQuery('#<?= $field->getId('scrollable') ?> input[type=checkbox]').prop('checked', false)"><?= e(trans('backend::lang.form.select_none')) ?></a>
</small>
<!-- Scrollable Checkbox list -->
<div class="field-checkboxlist-scrollable" id="<?= $field->getId('scrollable') ?>">
<div class="control-scrollbar" data-control="scrollbar">
<?php endif ?>
<input
type="hidden"
name="<?= $field->getName() ?>"
value="0" />
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
if (is_string($option)) $option = [$option];
?>
<div class="checkbox custom-checkbox">
<input
type="checkbox"
id="<?= $checkboxId ?>"
name="<?= $field->getName() ?>[]"
value="<?= $value ?>"
<?= in_array($value, $checkedValues) ? 'checked="checked"' : '' ?>>
<label for="<?= $checkboxId ?>">
<?= e(trans($option[0])) ?>
</label>
<?php if (isset($option[1])): ?>
<p class="help-block"><?= e(trans($option[1])) ?></p>
<?php endif ?>
</div>
<?php endforeach ?>
<?php if (count($fieldOptions) > 10): ?>
<label for="<?= $checkboxId ?>">
<?= e(trans($option[0])) ?>
</label>
<?php if (isset($option[1])): ?>
<p class="help-block"><?= e(trans($option[1])) ?></p>
<?php endif ?>
</div>
</div>
<?php endforeach ?>
<?php else: ?>
<?php if (count($fieldOptions) > 10): ?>
<!-- Quick selection -->
<small>
<?= e(trans('backend::lang.form.select')) ?>:
<a href="javascript:;" onclick="jQuery('#<?= $field->getId('scrollable') ?> input[type=checkbox]').prop('checked', true)"><?= e(trans('backend::lang.form.select_all')) ?></a>,
<a href="javascript:;" onclick="jQuery('#<?= $field->getId('scrollable') ?> input[type=checkbox]').prop('checked', false)"><?= e(trans('backend::lang.form.select_none')) ?></a>
</small>
<!-- Scrollable Checkbox list -->
<div class="field-checkboxlist-scrollable" id="<?= $field->getId('scrollable') ?>">
<div class="control-scrollbar" data-control="scrollbar">
<?php endif ?>
<input
type="hidden"
name="<?= $field->getName() ?>"
value="0" />
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
$checkboxId = 'checkbox_'.$field->getId().'_'.$index;
if (is_string($option)) $option = [$option];
?>
<div class="checkbox custom-checkbox">
<input
type="checkbox"
id="<?= $checkboxId ?>"
name="<?= $field->getName() ?>[]"
value="<?= $value ?>"
<?= in_array($value, $checkedValues) ? 'checked="checked"' : '' ?>>
<label for="<?= $checkboxId ?>">
<?= e(trans($option[0])) ?>
</label>
<?php if (isset($option[1])): ?>
<p class="help-block"><?= e(trans($option[1])) ?></p>
<?php endif ?>
</div>
<?php endforeach ?>
<?php if (count($fieldOptions) > 10): ?>
</div>
</div>
<?php endif ?>
<?php endif ?>
<?php endif ?>
</div>
<?php else: ?>

View File

@ -0,0 +1,10 @@
<!-- Section -->
<div class="field-section">
<?php if ($field->label): ?>
<h4><?= e(trans($field->label)) ?></h4>
<?php endif ?>
<?php if ($field->comment): ?>
<p class="help-block"><?= e(trans($field->comment)) ?></p>
<?php endif ?>
</div>

View File

@ -83,7 +83,13 @@ class ServiceProvider extends ModuleServiceProvider
'permissions' => ['cms.manage_pages', 'cms.manage_layouts', 'cms.manage_partials']
]
]
],
'media' => [
'label' => 'cms::lang.media.menu_label',
'icon' => 'icon-folder',
'url' => Backend::url('cms/media'),
'permissions' => ['cms.*'],
'order' => 20
]
]);
});

View File

@ -56,10 +56,8 @@ class MediaLibrary
*/
protected function init()
{
$this->storagePath = Config::get('cms.storage.media.path', '/storage/app/media');
$this->storageDisk = Storage::disk(
Config::get('cms.storage.media.disk', 'local'));
$this->storageFolder = $this->validatePath(
$this->storagePath = rtrim(Config::get('cms.storage.media.path', '/storage/app/media'), '/');
$this->storageFolder = self::validatePath(
Config::get('cms.storage.media.folder', 'media'), true);
$this->ignoreNames = Config::get('cms.storage.media.ignore', $this->defaultIgnoreNames);
@ -74,7 +72,7 @@ class MediaLibrary
*/
public function listFolderContents($folder = '/', $sortBy = 'title')
{
$folder = $this->validatePath($folder);
$folder = self::validatePath($folder);
$fullFolderPath = $this->getMediaPath($folder);
/*
@ -109,12 +107,28 @@ class MediaLibrary
}
/**
* Returns URL of a Library file.
* @param string $path Specifies a file path relative to the Library root.
* Determines if a file with the specified path exists in the library.
* @param string $path Specifies the file path relative the the Library root.
* @return boolean Returns TRUE if the file exists.
*/
public function url($path)
public function exists($path)
{
$path = self::validatePath($path);
$fullPath = $this->getMediaPath($path);
return $this->getStorageDisk()->exists($fullPath);
}
/**
* Returns a file contents.
* @param string $path Specifies the file path relative the the Library root.
* @return string Returns the file contents
*/
public function get($path)
{
$path = self::validatePath($path);
$fullPath = $this->getMediaPath($path);
return $this->getStorageDisk()->get($fullPath);
}
/**
@ -137,7 +151,7 @@ class MediaLibrary
* @param boolean $normalizeOnly Specifies if only the normalization, without validation should be performed.
* @return string Returns a normalized path.
*/
protected function validatePath($path, $normalizeOnly = false)
public static function validatePath($path, $normalizeOnly = false)
{
$path = str_replace('\\', '/', $path);
$path = '/'.trim($path, '/');
@ -171,7 +185,7 @@ class MediaLibrary
*/
protected function getMediaRelativePath($path)
{
$path = $this->validatePath($path, true);
$path = self::validatePath($path, true);
if (substr($path, 0, $this->storageFolderNameLength) == $this->storageFolder)
return substr($path, $this->storageFolderNameLength);
@ -202,10 +216,18 @@ class MediaLibrary
if (!$this->isVisible($relativePath))
return;
$lastModified = $this->storageDisk->lastModified($path);
$size = $itemType == MediaLibraryItem::TYPE_FILE ? $this->storageDisk->size($path) : $this->getFolderItemCount($path);
/*
* S3 doesn't allow getting the last modified timestamp for folders,
* so this feature is disabled - folders timestamp is always NULL.
*/
$lastModified = $itemType == MediaLibraryItem::TYPE_FILE ?
$this->getStorageDisk()->lastModified($path) : null;
return new MediaLibraryItem($relativePath, $size, $lastModified, $itemType);
$size = $itemType == MediaLibraryItem::TYPE_FILE ?
$this->getStorageDisk()->size($path) : $this->getFolderItemCount($path);
$publicUrl = $this->storagePath.$relativePath;
return new MediaLibraryItem($relativePath, $size, $lastModified, $itemType, $publicUrl);
}
/**
@ -216,8 +238,8 @@ class MediaLibrary
protected function getFolderItemCount($path)
{
$folderItems = array_merge(
$this->storageDisk->files($path),
$this->storageDisk->directories($path));
$this->getStorageDisk()->files($path),
$this->getStorageDisk()->directories($path));
$size = 0;
foreach ($folderItems as $folderItem) {
@ -240,13 +262,13 @@ class MediaLibrary
'folders' => []
];
$files = $this->storageDisk->files($fullFolderPath);
$files = $this->getStorageDisk()->files($fullFolderPath);
foreach ($files as $file) {
if ($libraryItem = $this->initLibraryItem($file, MediaLibraryItem::TYPE_FILE))
$result['files'][] = $libraryItem;
}
$folders = $this->storageDisk->directories($fullFolderPath);
$folders = $this->getStorageDisk()->directories($fullFolderPath);
foreach ($folders as $folder) {
if ($libraryItem = $this->initLibraryItem($folder, MediaLibraryItem::TYPE_FOLDER))
$result['folders'][] = $libraryItem;
@ -284,4 +306,20 @@ class MediaLibrary
}
});
}
/**
* Initializes and returns the Media Library disk.
* This method should always be used instead of trying to access the
* $storageDisk property directly as initializing the disc requires
* communicating with the remote storage.
* @return mixed Returns the storage disk object.
*/
protected function getStorageDisk()
{
if ($this->storageDisk)
return $this->storageDisk;
return $this->storageDisk = Storage::disk(
Config::get('cms.storage.media.disk', 'local'));
}
}

View File

@ -1,5 +1,8 @@
<?php namespace Cms\Classes;
use Config;
use File;
/**
* Represents a file or folder in the Media Library.
*
@ -9,9 +12,13 @@
class MediaLibraryItem
{
const TYPE_FILE = 'file';
const TYPE_FOLDER = 'folder';
const FILE_TYPE_IMAGE = 'image';
const FILE_TYPE_VIDEO = 'video';
const FILE_TYPE_AUDIO = 'audio';
const FILE_TYPE_DOCUMENT = 'document';
/**
* @var string Specifies the item path relative to the Library root.
*/
@ -34,16 +41,93 @@ class MediaLibraryItem
*/
public $type;
public function __construct($path, $size, $lastModified, $type)
/**
* @var string Specifies the public URL of the item.
*/
public $publicUrl;
/**
* @var array Contains a default list of files and directories to ignore.
* The list can be customized with the following configuration options:
* - cms.storage.media.image_extensions
* - cms.storage.media.video_extensions
* - cms.storage.media.audo_extensions
*/
protected static $defaultTypeExtensions = [
'image' => ['gif', 'png', 'jpg', 'jpeg', 'bmp'],
'video' => ['mp4', 'avi', 'mov', 'mpg'],
'audio' => ['mp3', 'wav', 'wma', 'm4a']
];
protected static $imageExtensions;
protected static $videoExtensions;
protected static $audioExtensions;
public function __construct($path, $size, $lastModified, $type, $publicUrl)
{
$this->path = $path;
$this->size = $size;
$this->lastModified = $lastModified;
$this->type = $type;
$this->publicUrl = $publicUrl;
}
public function isFile()
{
return $this->type == self::TYPE_FILE;
}
/**
* Returns the file type by its name.
* The known file types are: image, video, audio, document
* @return string Returns the file type or NULL if the item is a folder.
*/
public function getFileType()
{
if (!$this->isFile())
return null;
if (!self::$imageExtensions) {
self::$imageExtensions = Config::get('cms.storage.media.image_extensions', self::$defaultTypeExtensions['image']);
self::$videoExtensions = Config::get('cms.storage.media.video_extensions', self::$defaultTypeExtensions['video']);
self::$audioExtensions = Config::get('cms.storage.media.audio_extensions', self::$defaultTypeExtensions['audio']);
}
$extension = pathinfo($this->path, PATHINFO_EXTENSION);
if (!strlen($extension))
return self::FILE_TYPE_DOCUMENT;
if (in_array($extension, self::$imageExtensions))
return self::FILE_TYPE_IMAGE;
if (in_array($extension, self::$videoExtensions))
return self::FILE_TYPE_VIDEO;
if (in_array($extension, self::$audioExtensions))
return self::FILE_TYPE_AUDIO;
return self::FILE_TYPE_DOCUMENT;
}
/**
* Returns the item size as string.
* For file-type items the size is the number of bytes. For folder-type items
* the size is the number of items contained by the item.
* @return string Returns the size as string.
*/
public function sizeToString()
{
return $this->type == self::TYPE_FILE ?
File::sizeToString($this->size) :
$this->size.' '.trans('cms::lang.media.folder_size_items');
}
/**
* Returns the item last modification date as string.
* @return string Returns the item last modification date as string.
*/
public function lastModifiedAsString()
{
return $this->lastModified ? date('M d, Y', $this->lastModified) : null;
}
}

View File

@ -0,0 +1,35 @@
<?php namespace Cms\Controllers;
use BackendMenu;
use Backend\Classes\Controller;
use Cms\Widgets\MediaManager;
/**
* CMS Media Manager
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class Media extends Controller
{
public $requiredPermissions = ['cms.*'];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
BackendMenu::setContext('October.Cms', 'media', true);
$this->pageTitle = 'cms::lang.media.menu_label';
$manager = new MediaManager($this, 'manager');
$manager->bindToController();
}
public function index()
{
$this->bodyClass = 'compact-container';
}
}

View File

@ -0,0 +1,7 @@
<?= Block::put('head') ?><?= Block::endPut() ?>
<?= Block::put('body') ?>
<div class="layout">
<?= $this->widget->manager->render() ?>
</div>
<?= Block::endPut() ?>

View File

@ -181,6 +181,27 @@ return [
'manage_themes' => 'Manage themes'
],
'media' => [
'invalid_path' => "Invalid file path specified: ':path'."
'invalid_path' => "Invalid file path specified: ':path'.",
'menu_label' => 'Media',
'upload' => 'Upload',
'add_folder' => 'Add folder',
'search' => 'Search',
'filter_everything' => 'Everything',
'filter_images' => 'Images',
'filter_video' => 'Video',
'filter_audio' => 'Audio',
'filter_documents' => 'Documents',
'library' => 'Library',
'folder_size_items' => 'item(s)',
'size' => 'Size',
'title' => 'Title',
'last_modified' => 'Last modified',
'public_url' => 'Public URL',
'click_here' => 'Click here',
'thumbnail_error' => 'Error generating thumbnail.',
'return_to_parent' => 'Return to the parent folder',
'return_to_parent_label' => 'Go up ..',
'nothing_selected' => 'Nothing is selected.',
'multiple_selected' => 'Multiple items selected.'
]
];

View File

@ -0,0 +1,444 @@
<?php namespace Cms\Widgets;
use URL;
use Str;
use Lang;
use File;
use Input;
use Request;
use Response;
use Exception;
use SystemException;
use ApplicationException;
use Backend\Classes\WidgetBase;
use Cms\Classes\MediaLibrary;
use Cms\Classes\MediaLibraryItem;
use October\Rain\Database\Attach\Resizer;
/**
* Media Manager widget.
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class MediaManager extends WidgetBase
{
const FOLDER_ROOT = '/';
const VIEW_MODE_GRID = 'grid';
const VIEW_MODE_LIST = 'list';
const VIEW_MODE_TILES = 'tiles';
protected $brokenImageHash = null;
public function __construct($controller, $alias)
{
$this->alias = $alias;
parent::__construct($controller, []);
$this->bindToController();
}
/**
* Renders the widget.
* @return string
*/
public function render()
{
$this->prepareVars();
return $this->makePartial('body');
}
/*
* Event handlers
*/
public function onSearch()
{
}
public function onGoToFolder()
{
$path = Input::get('path');
if (Input::get('clearCache'))
MediaLibrary::instance()->resetCache();
$this->setCurrentFolder($path);
$this->prepareVars();
return [
'#'.$this->getId('item-list') => $this->makePartial('item-list'),
'#'.$this->getId('folder-path') => $this->makePartial('folder-path')
];
}
public function onGenerateThumbnails()
{
$batch = Input::get('batch');
if (!is_array($batch))
return;
$result = [];
foreach ($batch as $thumbnailInfo)
$result[] = $this->generateThumbnail($thumbnailInfo);
return [
'generatedThumbnails'=>$result
];
}
public function onGetSidebarThumbnail()
{
$path = Input::get('path');
$lastModified = Input::get('lastModified');
$thumbnailParams = $this->getThumbnailParams();
$thumbnailParams['width'] = 300;
$thumbnailParams['height'] = 255;
$thumbnailParams['mode'] = 'auto';
$path = MediaLibrary::validatePath($path);
if (!is_numeric($lastModified))
throw new ApplicationException('Invalid input data');
// If the thumbnail file exists - just return the thumbnail marup,
// otherwise generate a new thumbnail.
$thumbnailPath = $this->thumbnailExists($thumbnailParams, $path, $lastModified);
if ($thumbnailPath) {
return [
'markup'=>$this->makePartial('thumbnail-image', [
'isError' => $this->thumbnailIsError($thumbnailPath),
'imageUrl' => $this->getThumbnailImageUrl($thumbnailPath)
])
];
}
$thumbnailInfo = $thumbnailParams;
$thumbnailInfo['path'] = $path;
$thumbnailInfo['lastModified'] = $lastModified;
$thumbnailInfo['id'] = 'sidebar-thumbnail';
return $this->generateThumbnail($thumbnailInfo, $thumbnailParams, true);
}
public function onChangeView()
{
$viewMode = Input::get('view');
$path = Input::get('path');
$this->setViewMode($viewMode);
$this->setCurrentFolder($path);
$this->prepareVars();
return [
'#'.$this->getId('item-list') => $this->makePartial('item-list'),
'#'.$this->getId('folder-path') => $this->makePartial('folder-path'),
'#'.$this->getId('view-mode-buttons') => $this->makePartial('view-mode-buttons')
];
}
/*
* Methods for th internal use
*/
protected function prepareVars()
{
clearstatcache();
$folder = $this->getCurrentFolder();
$viewMode = $this->getViewMode();
$this->vars['items'] = $this->listFolderItems($folder);
$this->vars['currentFolder'] = $folder;
$this->vars['isRootFolder'] = $folder == self::FOLDER_ROOT;
$this->vars['pathSegments'] = $this->splitPathToSegments($folder);
$this->vars['viewMode'] = $viewMode;
$this->vars['thumbnailParams'] = $this->getThumbnailParams($viewMode);
}
protected function listFolderItems($folder)
{
return MediaLibrary::instance()->listFolderContents($folder);
}
protected function getCurrentFolder()
{
$folder = $this->getSession('media_folder', self::FOLDER_ROOT);
return $folder;
}
protected function setCurrentFolder($path)
{
$path = MediaLibrary::validatePath($path);
$this->putSession('media_folder', $path);
}
protected function itemTypeToIconClass($item, $itemType)
{
if ($item->type == MediaLibraryItem::TYPE_FOLDER)
return 'icon-folder';
switch ($itemType) {
case MediaLibraryItem::FILE_TYPE_IMAGE : return "icon-picture-o";
case MediaLibraryItem::FILE_TYPE_VIDEO : return "icon-video-camera";
case MediaLibraryItem::FILE_TYPE_AUDIO : return "icon-volume-up";
default : return "icon-file";
}
}
protected function splitPathToSegments($path)
{
$path = MediaLibrary::validatePath($path, true);
$result = [];
do {
$result[] = $path;
} while (($path = dirname($path)) != '/');
return array_reverse($result);
}
/**
* Adds widget specific asset files. Use $this->addJs() and $this->addCss()
* to register new assets to include on the page.
* @return void
*/
protected function loadAssets()
{
$this->addCss('css/mediamanager.css', 'core');
$this->addJs('js/mediamanager.js', 'core');
}
protected function getViewMode()
{
return $this->getSession('view_mode', self::VIEW_MODE_GRID);
}
protected function setViewMode($viewMode)
{
if (!in_array($viewMode, [self::VIEW_MODE_GRID, self::VIEW_MODE_LIST, self::VIEW_MODE_TILES]))
throw new SystemException('Invalid input data');
return $this->putSession('view_mode', $viewMode);
}
protected function getThumbnailParams($viewMode = null)
{
$result = [
'mode' => 'crop',
'ext' => 'png'
];
if ($viewMode) {
if ($viewMode == self::VIEW_MODE_LIST) {
$result['width'] = 75;
$result['height'] = 75;
}
else {
$result['width'] = 165;
$result['height'] = 165;
}
}
return $result;
}
protected function getThumbnailImagePath($thumbnailParams, $itemPath, $lastModified)
{
$itemSignature = md5($itemPath).$lastModified;
$thumbFile = 'thumb_' .
$itemSignature . '_' .
$thumbnailParams['width'] . 'x' .
$thumbnailParams['height'] . '_' .
$thumbnailParams['mode'] . '.' .
$thumbnailParams['ext'];
$partition = implode('/', array_slice(str_split($itemSignature, 3), 0, 3)) . '/';
$result = $this->getThumbnailDirectory().$partition.$thumbFile;
return $result;
}
protected function getThumbnailImageUrl($imagePath)
{
return URL::to('/storage/temp'.$imagePath);
}
protected function thumbnailExists($thumbnailParams, $itemPath, $lastModified)
{
$thumbnailPath = $this->getThumbnailImagePath($thumbnailParams, $itemPath, $lastModified);
$fullPath = temp_path(ltrim($thumbnailPath, '/'));
if (File::exists($fullPath))
return $thumbnailPath;
return false;
}
protected function thumbnailIsError($thumbnailPath)
{
$fullPath = temp_path(ltrim($thumbnailPath, '/'));
return hash_file('crc32', $fullPath) == $this->getBrokenImageHash();
}
protected function getLocalTempFilePath($fileName)
{
$fileName = md5($fileName.uniqid().microtime());
$path = temp_path() . '/media';
if (!File::isDirectory($path))
File::makeDirectory($path, 0777, true, true);
return $path.'/'.$fileName;
}
protected function getThumbnailDirectory()
{
return '/public/';
}
protected function getPlaceholderId($item)
{
return 'placeholder'.md5($item->path.'-'.$item->lastModified.uniqid(microtime()));
}
protected function generateThumbnail($thumbnailInfo, $thumbnailParams = null)
{
$tempFilePath = null;
$fullThumbnailPath = null;
$thumbnailPath = null;
$markup = null;
try {
// Get and validate input data
$path = $thumbnailInfo['path'];
$width = $thumbnailInfo['width'];
$height = $thumbnailInfo['height'];
$lastModified = $thumbnailInfo['lastModified'];
if (!is_numeric($width) || !is_numeric($height) || !is_numeric($lastModified))
throw new ApplicationException('Invalid input data');
if (!$thumbnailParams) {
$thumbnailParams = $this->getThumbnailParams();
$thumbnailParams['width'] = $width;
$thumbnailParams['height'] = $height;
}
$thumbnailPath = $this->getThumbnailImagePath($thumbnailParams, $path, $lastModified);
$fullThumbnailPath = temp_path(ltrim($thumbnailPath, '/'));
// Save the file locally
$library = MediaLibrary::instance();
$tempFilePath = $this->getLocalTempFilePath($path);
if (!@File::put($tempFilePath, $library->get($path)))
throw new SystemException('Error saving remote file to a temporary location');
// Resize the thumbnail and save to the thumbnails directory
$this->resizeImage($fullThumbnailPath, $thumbnailParams, $tempFilePath);
// Delete the temporary file
File::delete($tempFilePath);
$markup = $this->makePartial('thumbnail-image', [
'isError' => false,
'imageUrl' => $this->getThumbnailImageUrl($thumbnailPath)
]);
}
catch (Exception $ex) {
if ($tempFilePath)
File::delete($tempFilePath);
if ($fullThumbnailPath)
$this->copyBrokenImage($fullThumbnailPath);
$markup = $this->makePartial('thumbnail-image', ['isError' => true]);
// TODO: We need to log all types of exceptions here
traceLog($ex->getMessage());
}
if ($markup && ($id = $thumbnailInfo['id']))
return [
'id'=>$id,
'markup'=>$markup
];
}
protected function resizeImage($fullThumbnailPath, $thumbnailParams, $tempFilePath)
{
$thumbnailDir = dirname($fullThumbnailPath);
if (!File::isDirectory($thumbnailDir)) {
if (File::makeDirectory($thumbnailDir, 0777, true) === false)
throw new SystemException('Error creating thumbnail directory');
}
$targetDimensions = $this->getTargetDimensions($thumbnailParams['width'], $thumbnailParams['height'], $tempFilePath);
$targetWidth = $targetDimensions[0];
$targetHeight = $targetDimensions[1];
$resizer = Resizer::open($tempFilePath);
$resizer->resize($targetWidth, $targetHeight, $thumbnailParams['mode'], [0, 0]);
$resizer->save($fullThumbnailPath, 95);
File::chmod($fullThumbnailPath);
}
protected function getBrokenImagePath()
{
return __DIR__.'/mediamanager/assets/images/broken-thumbnail.gif';
}
protected function getBrokenImageHash()
{
if ($this->brokenImageHash)
return $this->brokenImageHash;
$fullPath = $this->getBrokenImagePath();
return $this->brokenImageHash = hash_file('crc32', $fullPath);
}
protected function copyBrokenImage($path)
{
try {
$thumbnailDir = dirname($path);
if (!File::isDirectory($thumbnailDir)) {
if (File::makeDirectory($thumbnailDir, 0777, true) === false)
return;
}
File::copy($this->getBrokenImagePath(), $path);
}
catch (Exception $ex) {
traceLog($ex->getMessage());
}
}
protected function getTargetDimensions($width, $height, $originalImagePath)
{
$originalDimensions = [$width, $height];
try {
$dimensions = getimagesize($originalImagePath);
if (!$dimensions)
return $originalDimensions;
if ($dimensions[0] > $width || $dimensions[1] > $height)
return $originalDimensions;
return $dimensions;
}
catch (Exception $ex) {
return $originalDimensions;
}
}
}

View File

@ -0,0 +1,288 @@
div[data-control="media-manager"] audio,
div[data-control="media-manager"] video {
width: 100%;
}
div[data-control="media-manager"] video {
background: #ecf0f1;
max-height: 225px;
}
div[data-control="media-manager"] .media-player-fallback {
font-size: 13px;
color: #95a5a6;
background: #ecf0f1;
line-height: 180%;
}
div[data-control="media-manager"] .media-player-fallback.panel-embedded {
padding: 20px;
margin: -20px -20px 0 -20px;
}
div[data-control="media-manager"] p.thumbnail-error-message {
font-size: 12px;
margin-top: 25px;
color: #bdc3c7;
}
div[data-control="media-manager"] .media-list {
padding: 0 0 20px 20px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
div[data-control="media-manager"] .media-list li {
display: inline-block;
vertical-align: top;
margin: 0 20px 20px 0;
overflow: hidden;
cursor: pointer;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
div[data-control="media-manager"] .media-list li .icon-container {
display: table;
}
div[data-control="media-manager"] .media-list li .icon-container i {
color: #95a5a6;
display: inline-block;
}
div[data-control="media-manager"] .media-list li .icon-container div {
display: table-cell;
text-align: center;
vertical-align: middle;
}
div[data-control="media-manager"] .media-list li .icon-container.image > div.icon-wrapper {
display: none;
}
div[data-control="media-manager"] .media-list li h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 15px;
line-height: 150%;
margin: 15px 0 5px 0;
}
div[data-control="media-manager"] .media-list li p.size {
font-size: 12px;
color: #95a5a6;
}
div[data-control="media-manager"] .media-list li .image-placeholder {
position: relative;
}
div[data-control="media-manager"] .media-list li .image-placeholder i {
padding-top: 0;
padding-left: 2px;
}
div[data-control="media-manager"] .media-list li .image-placeholder[data-loading] i {
display: none;
}
div[data-control="media-manager"] .media-list li .image-placeholder[data-loading]:after {
background-image: url(../../../../../../modules/backend/assets/images/loading-indicator-transparent.svg);
background-position: 50% 50%;
content: ' ';
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
background-size: 28px 28px;
position: absolute;
width: 28px;
height: 28px;
top: 50%;
left: 50%;
margin-top: -14px;
margin-left: -14px;
}
div[data-control="media-manager"] .media-list li i.icon-chain-broken {
padding: 0;
color: #bdc3c7;
}
div[data-control="media-manager"] .media-list li[data-item-type=folder] i {
color: #4da7e8;
}
div[data-control="media-manager"] .media-list.list li {
height: 75px;
width: 260px;
border: 1px solid #ecf0f1;
background: #f6f8f9;
}
div[data-control="media-manager"] .media-list.list li .icon-container {
border-right: 1px solid #f6f8f9;
width: 75px;
height: 75px;
float: left;
}
div[data-control="media-manager"] .media-list.list li .icon-container i {
font-size: 35px;
}
div[data-control="media-manager"] .media-list.list li .icon-container.image {
border-right: 1px solid #ecf0f1!important;
}
div[data-control="media-manager"] .media-list.list li .icon-container p.thumbnail-error-message {
display: none;
}
div[data-control="media-manager"] .media-list.list .icon-wrapper {
width: 75px;
}
div[data-control="media-manager"] .media-list.list li .info {
margin-left: 90px;
}
div[data-control="media-manager"] .media-list.list li .image-placeholder {
width: 75px;
height: 75px;
}
div[data-control="media-manager"] .media-list.list li[data-root] h4 {
margin-top: 27px;
}
div[data-control="media-manager"] .media-list.list li.selected {
background: #4da7e8 !important;
}
div[data-control="media-manager"] .media-list.list li.selected i,
div[data-control="media-manager"] .media-list.list li.selected p.size {
color: #ecf0f1;
}
div[data-control="media-manager"] .media-list.list li.selected h4 {
color: white;
}
div[data-control="media-manager"] .media-list.list li.selected .icon-container {
border-right-color: #4da7e8 !important;
}
div[data-control="media-manager"] .media-list.tiles li {
width: 167px;
margin-bottom: 25px;
}
div[data-control="media-manager"] .media-list.tiles .icon-wrapper {
width: 167px;
}
div[data-control="media-manager"] .media-list.tiles li .image-placeholder {
width: 165px;
height: 165px;
}
div[data-control="media-manager"] .media-list.tiles li .image-placeholder[data-loading]:after {
background-image: url(../../../../../../modules/backend/assets/images/loading-indicator-transparent.svg);
background-position: 50% 50%;
content: ' ';
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
background-size: 55px 55px;
position: absolute;
width: 55px;
height: 55px;
top: 50%;
left: 50%;
margin-top: -27.5px;
margin-left: -27.5px;
}
div[data-control="media-manager"] .media-list.tiles li .icon-container {
width: 167px;
height: 167px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #ecf0f1;
overflow: hidden;
background: #f6f8f9;
}
div[data-control="media-manager"] .media-list.tiles li .icon-container i {
font-size: 55px;
}
div[data-control="media-manager"] .media-list.tiles li .icon-container p {
font-family: 'Open Sans', Arial, sans-serif;
}
div[data-control="media-manager"] .media-list.tiles li.selected .icon-container {
background: #4da7e8 !important;
border-color: #2581b8;
}
div[data-control="media-manager"] .media-list.tiles li.selected .icon-container i,
div[data-control="media-manager"] .media-list.tiles li.selected .icon-container p {
color: #ecf0f1;
}
div[data-control="media-manager"] .media-list.tiles li.selected h4 {
color: #2581b8;
}
div[data-control="media-manager"] .media-list.tiles i.icon-chain-broken {
margin-top: 47px;
}
div[data-control="media-manager"] .media-list.tiles p.size {
margin-bottom: 0;
}
div[data-control="media-manager"] .sidebar-image-placeholder-container {
display: table;
width: 100%;
}
div[data-control="media-manager"] .sidebar-image-placeholder {
display: table-cell;
height: 225px;
position: relative;
vertical-align: middle;
text-align: center;
border-bottom: 1px solid #ecf0f1;
}
div[data-control="media-manager"] .sidebar-image-placeholder[data-loading] {
background: #ecf0f1;
}
div[data-control="media-manager"] .sidebar-image-placeholder[data-loading]:after {
background-image: url(../../../../../../modules/backend/assets/images/loading-indicator-transparent.svg);
background-position: 50% 50%;
content: ' ';
-webkit-animation: spin 1s linear infinite;
animation: spin 1s linear infinite;
background-size: 62px 62px;
position: absolute;
width: 62px;
height: 62px;
top: 50%;
left: 50%;
margin-top: -31px;
margin-left: -31px;
}
div[data-control="media-manager"] .sidebar-image-placeholder i.icon-chain-broken,
div[data-control="media-manager"] .sidebar-image-placeholder i.icon-crop,
div[data-control="media-manager"] .sidebar-image-placeholder i.icon-asterisk {
color: #bdc3c7;
font-size: 55px;
}
div[data-control="media-manager"] .sidebar-image-placeholder.no-border {
border-bottom: none;
}
div[data-control="media-manager"] .sidebar-image-placeholder p {
font-size: 12px;
margin-top: 25px;
color: #bdc3c7;
}
div[data-control="media-manager"] .list-container {
position: relative;
z-index: 100;
}
div[data-control="media-manager"] [data-control="item-list"] {
position: relative;
}
div[data-control="media-manager"] div[data-control="selection-marker"] {
position: absolute;
z-index: 50;
border: 1px dashed #95a5a6;
}
body:not(.no-select) div[data-control="media-manager"] .media-list.tiles li:hover .icon-container {
background: #4da7e8 !important;
border-color: #2581b8;
}
body:not(.no-select) div[data-control="media-manager"] .media-list.tiles li:hover .icon-container i,
body:not(.no-select) div[data-control="media-manager"] .media-list.tiles li:hover .icon-container p {
color: #ecf0f1;
}
body:not(.no-select) div[data-control="media-manager"] .media-list.tiles li:hover h4 {
color: #2581b8;
}
body:not(.no-select) div[data-control="media-manager"] .media-list .list li:hover {
background: #4da7e8 !important;
}
body:not(.no-select) div[data-control="media-manager"] .media-list .list li:hover i,
body:not(.no-select) div[data-control="media-manager"] .media-list .list li:hover p.size {
color: #ecf0f1;
}
body:not(.no-select) div[data-control="media-manager"] .media-list .list li:hover h4 {
color: white;
}
body:not(.no-select) div[data-control="media-manager"] .media-list .list li:hover .icon-container {
border-right-color: #4da7e8 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -0,0 +1,665 @@
/*
* Media manager control class
*
* Dependences:
* - Scrollbar (october.scrollbar.js)
*/
+function ($) { "use strict";
// MEDIA MANAGER CLASS DEFINITION
// ============================
var MediaManager = function(element, options) {
this.$el = $(element)
this.$form = this.$el.closest('form')
this.options = options
// Event handlers
this.navigateHandler = this.onNavigate.bind(this)
this.commandClickHandler = this.onCommandClick.bind(this)
this.itemClickHandler = this.onItemClick.bind(this)
this.listMouseDownHandler = this.onListMouseDown.bind(this)
this.listMouseUpHandler = this.onListMouseUp.bind(this)
this.listMouseMoveHandler = this.onListMouseMove.bind(this)
// Instance-bound methods
this.updateSidebarPreviewBound = this.updateSidebarPreview.bind(this)
this.replacePlaceholderBound = this.replacePlaceholder.bind(this)
this.placeholdersUpdatedBound = this.placeholdersUpdated.bind(this)
this.afterNavigateBound = this.afterNavigate.bind(this)
this.releaseSidebarThumbnailAjaxBound = this.releaseSidebarThumbnailAjax.bind(this)
this.replaceSidebarPlaceholderBound = this.replaceSidebarPlaceholder.bind(this)
// State properties
this.selectTimer = null
this.sidebarPreviewElement = null
this.itemListElement = null
this.thumbnailQueue = []
this.activeThumbnailQueueLength = 0
this.sidebarThumbnailAjax = null
this.selectionMarker = null
this.dropzone = null
//
// Initialization
//
this.init()
}
MediaManager.prototype.dispose = function() {
this.unregisterHandlers()
this.clearSelectTimer()
this.$el = null
this.$form = null
this.updateSidebarPreviewBound = null
this.replacePlaceholderBound = null
this.placeholdersUpdatedBound = null
this.sidebarPreviewElement = null
this.itemListElement = null
this.afterNavigateBound = null
this.replaceSidebarPlaceholderBound = null
this.sidebarThumbnailAjax = null
this.selectionMarker = null
this.thumbnailQueue = []
}
// MEDIA MANAGER INTERNAL METHODS
// ============================
MediaManager.prototype.init = function() {
this.itemListElement = this.$el.find('[data-control="item-list"]').get(0)
this.registerHandlers()
this.updateSidebarPreview()
this.generateThumbnails()
this.initUploader()
}
MediaManager.prototype.registerHandlers = function() {
this.$el.on('dblclick', this.navigateHandler)
this.$el.on('click.tree-path', 'ul.tree-path', this.navigateHandler)
this.$el.on('click.command', '[data-command]', this.commandClickHandler)
this.$el.on('click.item', '[data-type="media-item"]', this.itemClickHandler)
if (this.itemListElement)
this.itemListElement.addEventListener('mousedown', this.listMouseDownHandler)
}
MediaManager.prototype.unregisterHandlers = function() {
this.$el.off('dblclick', this.navigateHandler)
this.$el.off('click.tree-path', this.navigateHandler)
this.$el.off('click.command', this.commandClickHandler)
this.$el.off('click.item', this.itemClickHandler)
if (this.itemListElement) {
this.itemListElement.removeEventListener('mousedown', this.listMouseDownHandler)
this.itemListElement.removeEventListener('mousemove', this.listMouseMoveHandler)
}
document.removeEventListener('mouseup', this.listMouseUpHandler)
this.navigateHandler = null
this.commandClickHandler = null
this.itemClickHandler = null
this.listMouseDownHandler = null
this.listMouseUpHandler = null
this.listMouseMoveHandler = null
}
MediaManager.prototype.changeView = function(view) {
$.oc.stripeLoadIndicator.show()
var data = {
view: view,
path: this.$el.find('[data-type="current-folder"]').val()
}
this.$form.request(this.options.alias+'::onChangeView', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
}
/*
* Selecting
*/
MediaManager.prototype.clearSelectTimer = function() {
if (this.selectTimer == null)
return
clearTimeout(this.selectTimer)
this.selectTimer = null
}
MediaManager.prototype.selectItem = function(node, expandSelection) {
if (!expandSelection) {
var items = this.$el.get(0).querySelectorAll('[data-type="media-item"].selected')
// The class attribute is used only for selecting, it's safe to clear it
for (var i = 0, len = items.length; i < len; i++)
items[i].setAttribute('class', '')
}
if (!expandSelection)
node.setAttribute('class', 'selected')
else {
if (node.getAttribute('class') == 'selected')
node.setAttribute('class', '')
else
node.setAttribute('class', 'selected')
}
this.clearSelectTimer()
if (this.isPreviewSidebarVisible()) {
// Use the timeout to prevent too many AJAX requests
// when the selection changes too quickly (with the keyboard arrows)
this.selectTimer = setTimeout(this.updateSidebarPreviewBound, 100)
}
}
/*
* Navigation
*/
MediaManager.prototype.gotoFolder = function(path, clearCache) {
var data = {
path: path
}
if (clearCache)
data.clearCache = true
$.oc.stripeLoadIndicator.show()
this.$form.request(this.options.alias+'::onGoToFolder', {
data: data
}).always(function() {
$.oc.stripeLoadIndicator.hide()
}).done(this.afterNavigateBound)
}
MediaManager.prototype.afterNavigate = function() {
this.generateThumbnails()
this.updateSidebarPreview(true)
}
/*
* Sidebar
*/
MediaManager.prototype.isPreviewSidebarVisible = function() {
return true
}
MediaManager.prototype.updateSidebarMediaPreview = function(items) {
var previewPanel = this.sidebarPreviewElement,
previewContainer = previewPanel.querySelector('[data-control="media-preview-container"]'),
template = ''
for (var i = 0, len = previewContainer.children.length; i < len; i++)
previewContainer.removeChild(previewContainer.children[i])
if (items.length == 1) {
var item = items[0],
documentType = item.getAttribute('data-document-type')
switch (documentType) {
case 'audio' :
template = previewPanel.querySelector('[data-control="audio-template"]').innerHTML
break;
case 'video' :
template = previewPanel.querySelector('[data-control="video-template"]').innerHTML
break;
case 'image' :
template = previewPanel.querySelector('[data-control="image-template"]').innerHTML
break;
}
previewContainer.innerHTML = template
.replace('{src}', item.getAttribute('data-public-url'))
.replace('{path}', item.getAttribute('data-path'))
.replace('{last-modified}', item.getAttribute('data-last-modified-ts'))
if (documentType == 'image')
this.loadSidebarThumbnail()
}
else if (items.length == 0) {
template = previewPanel.querySelector('[data-control="no-selection-template"]').innerHTML
previewContainer.innerHTML = template
}
else {
template = previewPanel.querySelector('[data-control="multi-selection-template"]').innerHTML
previewContainer.innerHTML = template
}
}
MediaManager.prototype.updateSidebarPreview = function(resetSidebar) {
if (!this.sidebarPreviewElement)
this.sidebarPreviewElement = this.$el.get(0).querySelector('[data-control="preview-sidebar"]')
var items = resetSidebar === undefined ? this.$el.get(0).querySelectorAll('[data-type="media-item"].selected') : [],
previewPanel = this.sidebarPreviewElement
if (items.length == 0) {
// No items are selected
this.sidebarPreviewElement.querySelector('[data-control="sidebar-labels"]').setAttribute('class', 'hide')
}
else if (items.length == 1) {
this.sidebarPreviewElement.querySelector('[data-control="sidebar-labels"]').setAttribute('class', 'panel')
// One item is selected - display the details
var item = items[0]
previewPanel.querySelector('[data-label="size"]').textContent = item.getAttribute('data-size')
previewPanel.querySelector('[data-label="title"]').textContent = item.getAttribute('data-title')
previewPanel.querySelector('[data-label="last-modified"]').textContent = item.getAttribute('data-last-modified')
previewPanel.querySelector('[data-label="public-url"]').setAttribute('href', item.getAttribute('data-public-url'))
}
else {
// Multiple items are selected
this.sidebarPreviewElement.querySelector('[data-control="sidebar-labels"]').setAttribute('class', 'hide')
}
this.updateSidebarMediaPreview(items)
}
MediaManager.prototype.loadSidebarThumbnail = function() {
if (this.sidebarThumbnailAjax) {
try {
this.sidebarThumbnailAjax.abort()
}
catch (e) {}
this.sidebarThumbnailAjax = null
}
var sidebarThumbnail = this.sidebarPreviewElement.querySelector('[data-control="sidebar-thumbnail"]')
if (!sidebarThumbnail)
return
var data = {
path: sidebarThumbnail.getAttribute('data-path'),
lastModified: sidebarThumbnail.getAttribute('data-last-modified')
}
this.sidebarThumbnailAjax = this.$form.request(this.options.alias+'::onGetSidebarThumbnail', {
data: data
})
.done(this.replaceSidebarPlaceholderBound)
.always(this.releaseSidebarThumbnailAjaxBound)
}
MediaManager.prototype.replaceSidebarPlaceholder = function(response) {
var sidebarThumbnail = this.sidebarPreviewElement.querySelector('[data-control="sidebar-thumbnail"]')
if (!sidebarThumbnail)
return
if (!response.markup)
return
sidebarThumbnail.innerHTML = response.markup
sidebarThumbnail.removeAttribute('data-loading')
}
MediaManager.prototype.releaseSidebarThumbnailAjax = function() {
this.sidebarThumbnailAjax = null
}
/*
* Thumbnails
*/
MediaManager.prototype.generateThumbnails = function() {
this.thumbnailQueue = []
var placeholders = this.itemListElement.querySelectorAll('[data-type="media-item"] div.image-placeholder')
for (var i = (placeholders.length-1); i >= 0; i--)
this.thumbnailQueue.push({
id: placeholders[i].getAttribute('id'),
width: placeholders[i].getAttribute('data-width'),
height: placeholders[i].getAttribute('data-height'),
path: placeholders[i].getAttribute('data-path'),
lastModified: placeholders[i].getAttribute('data-last-modified')
})
this.handleThumbnailQueue()
}
MediaManager.prototype.handleThumbnailQueue = function() {
var maxThumbnailQueueLength = 2,
maxThumbnailBatchLength = 3
if (this.activeThumbnailQueueLength >= maxThumbnailQueueLength)
return
for (var i = this.activeThumbnailQueueLength; i < maxThumbnailQueueLength && this.thumbnailQueue.length > 0; i++) {
var batch = []
for (var j = 0; j < maxThumbnailBatchLength && this.thumbnailQueue.length > 0; j++)
batch.push(this.thumbnailQueue.pop())
this.activeThumbnailQueueLength++
this.handleThumbnailBatch(batch).always(this.placeholdersUpdatedBound)
}
}
MediaManager.prototype.handleThumbnailBatch = function(batch) {
var data = {
batch: batch
}
for (var i = 0, len = batch.length; i < len; i++) {
var placeholder = document.getElementById(batch[i].id)
if (placeholder)
placeholder.setAttribute('data-loading', 'true')
}
var promise = this.$form.request(this.options.alias+'::onGenerateThumbnails', {
data: data
})
promise.done(this.replacePlaceholderBound)
return promise
}
MediaManager.prototype.replacePlaceholder = function(response) {
if (!response.generatedThumbnails)
return
for (var i = 0, len = response.generatedThumbnails.length; i < len; i++) {
var thumbnailInfo = response.generatedThumbnails[i]
if (!thumbnailInfo.id || !thumbnailInfo.markup)
continue
var node = document.getElementById(thumbnailInfo.id)
if (!node)
continue
var placeholderContainer = node.parentNode
if (placeholderContainer)
placeholderContainer.innerHTML = thumbnailInfo.markup
}
}
MediaManager.prototype.placeholdersUpdated = function() {
this.activeThumbnailQueueLength--
this.handleThumbnailQueue()
}
/*
* Drag-select
*/
MediaManager.prototype.getAbsolutePosition = function(element) {
// TODO: refactor to a core library
var top = document.body.scrollTop,
left = 0
do {
top += element.offsetTop || 0;
top -= element.scrollTop || 0;
left += element.offsetLeft || 0;
element = element.offsetParent;
} while(element)
return {
top: top,
left: left
}
}
MediaManager.prototype.getEventPagePosition = function(ev) {
// TODO: refactor to a core library
if (ev.pageX || ev.pageY) {
return {
x: ev.pageX,
y: ev.pageY
}
}
else if (ev.clientX || ev.clientY) {
return {
x: (ev.clientX + document.body.scrollLeft + document.documentElement.scrollLeft),
y: (ev.clientY + document.body.scrollTop + document.documentElement.scrollTop)
}
}
return {
x: 0,
y: 0
}
}
MediaManager.prototype.getRelativePosition = function(element, pageX, pageY) {
var absolutePosition = this.getAbsolutePosition(element)
return {
x: (pageX - absolutePosition.left),
y: (pageY - absolutePosition.top)
}
}
MediaManager.prototype.createSelectionMarker = function() {
if (this.selectionMarker)
return
this.selectionMarker = document.createElement('div')
this.selectionMarker.setAttribute('data-control', 'selection-marker')
this.itemListElement.insertBefore(this.selectionMarker, this.itemListElement.firstChild)
}
MediaManager.prototype.doObjectsCollide = function(aTop, aLeft, aWidth, aHeight, bTop, bLeft, bWidth, bHeight) {
return !(
((aTop + aHeight) < (bTop)) ||
(aTop > (bTop + bHeight)) ||
((aLeft + aWidth) < bLeft) ||
(aLeft > (bLeft + bWidth))
)
}
/*
* Uploading
*/
MediaManager.prototype.initUploader = function() {
if (this.itemListElement) {
// this.dropzone = new Dropzone(this.itemListElement, uploaderOptions)
}
// disable
// dropzone.on('error', $.proxy(self.onUploadFail, self))
// dropzone.on('success', $.proxy(self.onUploadSuccess, self))
// dropzone.on('complete', $.proxy(self.onUploadComplete, self))
// dropzone.on('sending', function(file, xhr, formData) {
// $.each(self.$form.serializeArray(), function (index, field) {
// formData.append(field.name, field.value)
// })
// self.onUploadStart()
// })
}
// EVENT HANDLERS
// ============================
MediaManager.prototype.onNavigate = function(ev) {
var $item = $(ev.target).closest('[data-type="media-item"]')
if (!$item.length || !$item.data('path').length)
return
if ($item.data('item-type') == 'folder')
this.gotoFolder($item.data('path'))
return false
}
MediaManager.prototype.onCommandClick = function(ev) {
var command = $(ev.target).data('command')
switch (command) {
case 'refresh' :
this.gotoFolder(
this.$el.find('[data-type="current-folder"]').val(),
true
)
break;
case 'change-view' :
this.changeView($(ev.target).data('view'))
break;
}
return false
}
MediaManager.prototype.onItemClick = function(ev) {
if (ev.currentTarget.hasAttribute('data-root'))
return
this.selectItem(ev.currentTarget, ev.shiftKey)
}
MediaManager.prototype.onListMouseDown = function(ev) {
this.itemListElement.addEventListener('mousemove', this.listMouseMoveHandler)
document.addEventListener('mouseup', this.listMouseUpHandler)
var pagePosition = this.getEventPagePosition(ev),
relativePosition = this.getRelativePosition(this.itemListElement, pagePosition.x, pagePosition.y)
this.selectionStartPoint = relativePosition
this.selectionStarted = false
}
MediaManager.prototype.onListMouseUp = function(ev) {
this.itemListElement.removeEventListener('mousemove', this.listMouseMoveHandler)
document.removeEventListener('mouseup', this.listMouseUpHandler)
$(document.body).removeClass('no-select')
if (this.selectionStarted) {
var items = this.itemListElement.querySelectorAll('[data-type="media-item"]:not([data-root])'),
selectionPosition = this.getAbsolutePosition(this.selectionMarker)
for (var index = 0, len = items.length; index < len; index++) {
var item = items[index],
itemPosition = this.getAbsolutePosition(item)
if (this.doObjectsCollide(
selectionPosition.top,
selectionPosition.left,
this.selectionMarker.offsetWidth,
this.selectionMarker.offsetHeight,
itemPosition.top,
itemPosition.left,
item.offsetWidth,
item.offsetHeight)
) {
if (!ev.shiftKey)
item.setAttribute('class', 'selected')
else {
if (item.getAttribute('class') == 'selected')
item.setAttribute('class', '')
else
item.setAttribute('class', 'selected')
}
}
else if (!ev.shiftKey)
item.setAttribute('class', '')
}
this.updateSidebarPreview()
this.selectionMarker.setAttribute('class', 'hide')
}
this.selectionStarted = false
}
MediaManager.prototype.onListMouseMove = function(ev) {
var pagePosition = this.getEventPagePosition(ev),
relativePosition = this.getRelativePosition(this.itemListElement, pagePosition.x, pagePosition.y)
var deltaX = relativePosition.x - this.selectionStartPoint.x,
deltaY = relativePosition.y - this.selectionStartPoint.y
if (!this.selectionStarted && (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2)) {
// Start processing the selection only if the mouse was moved by
// at least 2 pixels.
this.createSelectionMarker()
this.selectionMarker.setAttribute('class', '')
this.selectionStarted = true
$(document.body).addClass('no-select')
}
if (this.selectionStarted) {
if (deltaX >= 0) {
this.selectionMarker.style.left = this.selectionStartPoint.x + 'px'
this.selectionMarker.style.width = deltaX + 'px'
}
else {
this.selectionMarker.style.left = relativePosition.x + 'px'
this.selectionMarker.style.width = Math.abs(deltaX) + 'px'
}
if (deltaY >= 0) {
this.selectionMarker.style.height = deltaY + 'px'
this.selectionMarker.style.top = this.selectionStartPoint.y + 'px'
}
else {
this.selectionMarker.style.top = relativePosition.y + 'px'
this.selectionMarker.style.height = Math.abs(deltaY) + 'px'
}
}
}
// MEDIA MANAGER PLUGIN DEFINITION
// ============================
MediaManager.DEFAULTS = {
alias: ''
}
var old = $.fn.mediaManager
$.fn.mediaManager = function (option) {
var args = Array.prototype.slice.call(arguments, 1),
result = undefined
this.each(function () {
var $this = $(this)
var data = $this.data('oc.mediaManager')
var options = $.extend({}, MediaManager.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.mediaManager', (data = new MediaManager(this, options)))
if (typeof option == 'string') result = data[option].apply(data, args)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.mediaManager.Constructor = MediaManager
// MEDIA MANAGER NO CONFLICT
// =================
$.fn.mediaManager.noConflict = function () {
$.fn.mediaManager = old
return this
}
// MEDIA MANAGER DATA-API
// ===============
$(document).on('render', function(){
$('div[data-control=media-manager]').mediaManager()
})
}(window.jQuery);

View File

@ -0,0 +1,324 @@
@import "../../../../../backend/assets/less/core/boot.less";
.media-selected-tiles() {
.icon-container {
background: @color-list-hover-bg!important;
border-color: #2581b8;
i, p {
color: #ecf0f1;
}
}
h4 {
color: #2581b8;
}
}
.media-selected-list() {
background: @color-list-hover-bg!important;
i, p.size {
color: #ecf0f1;
}
h4 {
color: white;
}
.icon-container {
border-right-color: @color-list-hover-bg!important;
}
}
div[data-control="media-manager"] {
.loading-indicator-pseudo-absolute(@size) {
background-image: url(../../../../../../modules/backend/assets/images/loading-indicator-transparent.svg);
background-position: 50% 50%;
content: ' ';
.animation(spin 1s linear infinite);
background-size: @size @size;
position: absolute;
width: @size;
height: @size;
top: 50%;
left: 50%;
margin-top: -@size/2;
margin-left: -@size/2;
}
audio, video {
width: 100%;
}
video {
background: #ecf0f1;
max-height: 225px;
}
.media-player-fallback {
font-size: 13px;
color: #95a5a6;
background: #ecf0f1;
line-height: 180%;
&.panel-embedded {
padding: 20px;
margin: -20px -20px 0 -20px;
}
}
.icon-message() {
font-size: 12px;
margin-top: 25px;
color: #bdc3c7;
}
p.thumbnail-error-message {
.icon-message();
}
.media-list {
padding: 0 0 20px 20px;
.user-select(none);
li {
display: inline-block;
vertical-align: top;
margin: 0 20px 20px 0;
overflow: hidden;
cursor: pointer;
.border-radius(3px);
.icon-container {
display: table;
i {
color: #95a5a6;
display: inline-block;
}
div {
display: table-cell;
text-align: center;
vertical-align: middle;
}
}
.icon-container.image {
> div.icon-wrapper {
display: none;
}
}
h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 15px;
line-height: 150%;
margin: 15px 0 5px 0;
}
p.size {
font-size: 12px;
color: #95a5a6;
}
.image-placeholder {
position: relative;
i {
padding-top: 0;
padding-left: 2px;
}
&[data-loading] {
i {
display: none;
}
}
&[data-loading]:after {
.loading-indicator-pseudo-absolute(28px);
}
}
i.icon-chain-broken {
padding: 0;
color: #bdc3c7;
}
&[data-item-type=folder] i {
color: @color-list-hover-bg;
}
}
&.list {
li {
height: 75px;
width: 260px;
border: 1px solid #ecf0f1;
background: #f6f8f9;
}
li .icon-container {
border-right: 1px solid #f6f8f9;
width: 75px;
height: 75px;
float: left;
i {
font-size: 35px;
}
&.image {
border-right: 1px solid #ecf0f1!important;
}
p.thumbnail-error-message {
display: none;
}
}
.icon-wrapper {
width: 75px;
}
li .info {
margin-left: 90px;
}
li .image-placeholder {
width: 75px;
height: 75px;
}
li[data-root] h4 {
margin-top: 27px;
}
li.selected {
.media-selected-list();
}
}
&.tiles {
li {
width: 167px;
margin-bottom: 25px;
}
.icon-wrapper {
width: 167px;
}
li .image-placeholder {
width: 165px;
height: 165px;
&[data-loading]:after {
.loading-indicator-pseudo-absolute(55px);
}
}
li .icon-container {
width: 167px;
height: 167px;
.border-radius(3px);
border: 1px solid #ecf0f1;
overflow: hidden;
background: #f6f8f9;
i {
font-size: 55px;
}
p {
font-family: @font-family-sans-serif;
}
}
li.selected {
.media-selected-tiles();
}
i.icon-chain-broken {
margin-top: 47px;
}
p.size {
margin-bottom: 0;
}
}
}
.sidebar-image-placeholder-container {
display: table;
width: 100%;
}
.sidebar-image-placeholder {
display: table-cell;
height: 225px;
position: relative;
vertical-align: middle;
text-align: center;
border-bottom: 1px solid #ecf0f1;
&[data-loading] {
background: #ecf0f1;
&:after {
.loading-indicator-pseudo-absolute(62px);
}
}
i.icon-chain-broken, i.icon-crop, i.icon-asterisk {
color: #bdc3c7;
font-size: 55px;
}
&.no-border {
border-bottom: none;
}
p {
.icon-message();
}
}
.list-container {
position: relative;
z-index: 100;
}
[data-control="item-list"] {
position: relative;
}
div[data-control="selection-marker"] {
position: absolute;
z-index: 50;
border: 1px dashed #95a5a6;
}
}
body:not(.no-select) {
div[data-control="media-manager"] .media-list {
&.tiles {
li:hover {
.media-selected-tiles();
}
}
.list {
li:hover {
.media-selected-list();
}
}
}
}

View File

@ -0,0 +1,46 @@
<div data-control="media-manager" class="layout" data-alias="<?= $this->alias ?>">
<?= $this->makePartial('toolbar') ?>
<div class="layout-row whiteboard">
<div class="layout">
<div class="layout-row">
<div class="layout-cell width-200 panel border-right">
<?= $this->makePartial('left-sidebar') ?>
</div>
<div class="layout-cell">
<div class="layout">
<div class="layout-row min-size">
<?= $this->makePartial('folder-toolbar') ?>
</div>
<div class="layout-row">
<!-- Main area -->
<div class="layout">
<div class="layout-cell ">
<div class="layout">
<div class="layout-row">
<!-- Main area - list -->
<div data-control="item-list">
<div id="<?= $this->getId('item-list') ?>" >
<?= $this->makePartial('item-list') ?>
</div>
</div>
</div>
<div class="layout-row min-size">
<!-- Main area - bottom toolbar -->
</div>
</div>
</div>
<div class="layout-cell width-300 panel border-left no-padding" data-control="preview-sidebar">
<!-- Right sidebar -->
<?= $this->makePartial('right-sidebar') ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
<ul class="tree-path">
<li class="root"><a href="#" data-type="media-item" data-item-type="folder" data-path="/"><?= e(trans('cms::lang.media.library')) ?></a></li>
<?php foreach ($pathSegments as $segment): ?>
<?php if ($segment != '/'): ?>
<li><a href="#" data-type="media-item" data-item-type="folder" data-path="<?= e($segment) ?>"><?= basename($segment) ?></a></li>
<?php endif ?>
<?php endforeach?>
</ul>

View File

@ -0,0 +1,15 @@
<div class="panel padding-less border-bottom triangle-down">
<div class="layout">
<div class="layout-cell">
<div class="layout-row" id="<?= $this->getId('folder-path') ?>">
<?= $this->makePartial('folder-path') ?>
</div>
</div>
<div class="layout-cell">
<button
type="button"
class="oc-icon-sign-out btn-icon pull-right larger">
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,36 @@
<ul class="media-list <?= $listClass ?>">
<?php if (count($items) > 0 || !$isRootFolder): ?>
<?php if (!$isRootFolder): ?>
<li data-type="media-item" data-item-type="folder" data-root data-path="<?= e(dirname($currentFolder)) ?>">
<div class="icon-container folder">
<div class="icon-wrapper"><i class="icon-folder"></i></div>
</div>
<div class="info">
<h4 title="<?= e(trans('cms::lang.media.return_to_parent')) ?>"><?= e(trans('cms::lang.media.return_to_parent_label')) ?></h4>
</div>
</li>
<?php endif ?>
<?php foreach ($items as $item):
$itemType = $item->getFileType();
?>
<li data-type="media-item"
data-item-type="<?= $item->type ?>"
data-path="<?= e($item->path) ?>"
data-title="<?= e(basename($item->path)) ?>"
data-size="<?= e($item->sizeToString()) ?>"
data-last-modified="<?= e($item->lastModifiedAsString()) ?>"
data-last-modified-ts="<?= $item->lastModified ?>"
data-public-url="<?= e($item->publicUrl) ?>"
data-document-type="<?= e($itemType) ?>"
>
<?= $this->makePartial('item-icon', ['itemType'=>$itemType, 'item'=>$item]) ?>
<div class="info">
<h4 title="<?= e(basename($item->path)) ?>"><?= e(basename($item->path)) ?></h4>
<p class="size"><?= e($item->sizeToString()) ?></p>
</div>
</li>
<?php endforeach ?>
<?php endif ?>
</ul>

View File

@ -0,0 +1,26 @@
<div class="icon-container <?= $itemType ?>">
<div class="icon-wrapper"><i class="<?= $this->itemTypeToIconClass($item, $itemType) ?>"></i></div>
<?php if ($itemType == Cms\Classes\MediaLibraryItem::FILE_TYPE_IMAGE):
$thumbnailPath = $this->thumbnailExists($thumbnailParams, $item->path, $item->lastModified);
?>
<div>
<?php if (!$thumbnailPath): ?>
<div class="image-placeholder"
data-width="<?= $thumbnailParams['width'] ?>"
data-height="<?= $thumbnailParams['height'] ?>"
data-path="<?= e($item->path) ?>"
data-last-modified="<?= $item->lastModified ?>"
id="<?= $this->getPlaceholderId($item) ?>"
>
<div class="icon-wrapper"><i class="<?= $this->itemTypeToIconClass($item, $itemType) ?>"></i></div>
</div>
<?php else: ?>
<?= $this->makePartial('thumbnail-image', [
'isError' => $this->thumbnailIsError($thumbnailPath),
'imageUrl' => $this->getThumbnailImageUrl($thumbnailPath)
]) ?>
<?php endif ?>
</div>
<?php endif ?>
</div>

View File

@ -0,0 +1,12 @@
<div class="panel no-padding padding-top">
<input type="hidden" data-type="current-folder" value="<?= e($currentFolder) ?>"/>
<div class="list-container">
<?php if ($viewMode == Cms\Widgets\MediaManager::VIEW_MODE_GRID): ?>
<?= $this->makePartial('list-grid') ?>
<?php elseif ($viewMode == Cms\Widgets\MediaManager::VIEW_MODE_LIST): ?>
<?= $this->makePartial('list-list') ?>
<?php else: ?>
<?= $this->makePartial('list-tiles') ?>
<?php endif ?>
</div>
</div>

View File

@ -0,0 +1,37 @@
<div data-control="media-preview-container"></div>
<script type="text/template" data-control="audio-template">
<div class="panel no-padding-bottom">
<audio src="{src}" controls>
<div class="media-player-fallback panel-embedded">Your browser doesn't support HTML5 audio.</div>
</audio>
</div>
</script>
<script type="text/template" data-control="video-template">
<video src="{src}" controls poster="<?= URL::to('modules/cms/Widgets/mediamanager/assets/images/video-poster.png') ?>">
<div class="panel media-player-fallback">Your browser doesn't support HTML5 video.</div>
</video>
</script>
<script type="text/template" data-control="image-template">
<div class="sidebar-image-placeholder-container"><div class="sidebar-image-placeholder" data-path="{path}" data-last-modified="{last-modified}" data-loading="true" data-control="sidebar-thumbnail"></div></div>
</script>
<script type="text/template" data-control="no-selection-template">
<div class="sidebar-image-placeholder-container">
<div class="sidebar-image-placeholder no-border">
<i class="icon-crop"></i>
<p><?= e(trans('cms::lang.media.nothing_selected')) ?></p>
</div>
</div>
</script>
<script type="text/template" data-control="multi-selection-template">
<div class="sidebar-image-placeholder-container">
<div class="sidebar-image-placeholder no-border">
<i class="icon-asterisk"></i>
<p><?= e(trans('cms::lang.media.multiple_selected')) ?></p>
</div>
</div>
</script>

View File

@ -0,0 +1,39 @@
<h3 class="section">Display</h3>
<ul class="nav nav-stacked selector-group">
<li role="presentation" class="active">
<a href="#">
<i class="icon-recycle"></i>
<?= e(trans('cms::lang.media.filter_everything')) ?>
</a>
</li>
<li role="presentation">
<a href="#">
<i class="icon-picture-o"></i>
<?= e(trans('cms::lang.media.filter_images')) ?>
</a>
</li>
<li role="presentation">
<a href="#">
<i class="icon-video-camera"></i>
<?= e(trans('cms::lang.media.filter_video')) ?>
</a>
</li>
<li role="presentation">
<a href="#">
<i class="icon-volume-up"></i>
<?= e(trans('cms::lang.media.filter_audio')) ?>
</a>
</li>
<li role="presentation">
<a href="#">
<i class="icon-file"></i>
<?= e(trans('cms::lang.media.filter_documents')) ?>
</a>
</li>
</ul>

View File

@ -0,0 +1,32 @@
<table class="table data">
<tbody class="icons clickable">
<?php if (count($items) > 0 || !$isRootFolder): ?>
<?php if (!$isRootFolder): ?>
<tr data-type="media-item" data-item-type="folder" data-root data-path="<?= e(dirname($currentFolder)) ?>">
<td><i class="icon-folder"></i>..</td>
<td></td>
<td></td>
</tr>
<?php endif ?>
<?php foreach ($items as $item):
$itemType = $item->getFileType();
?>
<tr data-type="media-item"
data-item-type="<?= $item->type ?>"
data-path="<?= e($item->path) ?>"
data-title="<?= e(basename($item->path)) ?>"
data-size="<?= e($item->sizeToString()) ?>"
data-last-modified="<?= e($item->lastModifiedAsString()) ?>"
data-last-modified-ts="<?= $item->lastModified ?>"
data-public-url="<?= e($item->publicUrl) ?>"
data-document-type="<?= e($itemType) ?>"
>
<td><i class="<?= $this->itemTypeToIconClass($item, $itemType) ?>"></i> <?= e(basename($item->path)) ?></td>
<td><?= e($item->sizeToString()) ?></td>
<td><?= e($item->lastModifiedAsString()) ?></td>
</tr>
<?php endforeach ?>
<?php endif ?>
</tbody>
</table>

View File

@ -0,0 +1 @@
<?= $this->makePartial('generic-list', ['listClass'=>'list']) ?>

View File

@ -0,0 +1 @@
<?= $this->makePartial('generic-list', ['listClass'=>'tiles']) ?>

View File

@ -0,0 +1,21 @@
<?= $this->makePartial('item-sidebar-preview') ?>
<div class="panel hide" data-control="sidebar-labels">
<label><?= e(trans('cms::lang.media.title')) ?></label>
<p data-label="title"></p>
<table class="name-value-list">
<tr>
<th><?= e(trans('cms::lang.media.size')) ?></th>
<td data-label="size"></td>
</tr>
<tr>
<th><?= e(trans('cms::lang.media.public_url')) ?></th>
<td><a href="#" data-label="public-url" target="_blank"><?= e(trans('cms::lang.media.click_here')) ?></a></td>
</tr>
<tr>
<th><?= e(trans('cms::lang.media.last_modified')) ?></th>
<td data-label="last-modified"></td>
</tr>
</table>
</div>

View File

@ -0,0 +1,6 @@
<?php if (!$isError): ?>
<img src="<?= $imageUrl ?>"/>
<?php else: ?>
<i class="icon-chain-broken" title="<?= e(trans('cms::lang.media.thumbnail_error')) ?>"></i>
<p class="thumbnail-error-message"><?= e(trans('cms::lang.media.thumbnail_error')) ?></p>
<?php endif ?>

View File

@ -0,0 +1,29 @@
<div class="layout-row min-size">
<div class="layout control-toolbar standalone-paddings">
<div class="layout-cell">
<div class="btn-group offset-right">
<button type="button" class="btn btn-primary oc-icon-upload"
><?= e(trans('cms::lang.media.upload')) ?></button>
<button type="button" class="btn btn-primary oc-icon-folder"
><?= e(trans('cms::lang.media.add_folder')) ?></button>
</div>
<button type="button" class="btn btn-default oc-icon-refresh empty offset-right" data-command="refresh"></button>
<div class="btn-group offset-right" id="<?= $this->getId('view-mode-buttons') ?>">
<?= $this->makePartial('view-mode-buttons') ?>
</div>
</div>
<div class="layout-cell width-fix">
<div class="relative toolbar-item loading-indicator-container size-input-text last">
<input placeholder="<?= e(trans('cms::lang.media.search')) ?>" type="text" name="search" value=""
class="form-control icon search" autocomplete="off"
data-track-input
data-load-indicator
data-load-indicator-opaque
data-request="<?= $this->getEventHandler('onSearch') ?>"
/>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,18 @@
<button
type="button"
class="btn btn-default oc-icon-align-justify empty <?= $viewMode == Cms\Widgets\MediaManager::VIEW_MODE_GRID ? 'on' : '' ?>"
data-command="change-view"
data-view="<?= Cms\Widgets\MediaManager::VIEW_MODE_GRID ?>">
</button>
<button
type="button"
class="btn btn-default oc-icon-th empty <?= $viewMode == Cms\Widgets\MediaManager::VIEW_MODE_LIST ? 'on' : '' ?>"
data-command="change-view"
data-view="<?= Cms\Widgets\MediaManager::VIEW_MODE_LIST ?>">
</button>
<button
type="button"
class="btn btn-default oc-icon-th-large empty <?= $viewMode == Cms\Widgets\MediaManager::VIEW_MODE_TILES ? 'on' : '' ?>"
data-command="change-view"
data-view="<?= Cms\Widgets\MediaManager::VIEW_MODE_TILES ?>">
</button>

View File

@ -120,7 +120,7 @@ if (window.jQuery === undefined)
var errorMsg,
updatePromise = $.Deferred()
if (isUnloading)
if (isUnloading || errorThrown == 'abort')
return
/*