Implemented image inserting to Redactor.

This commit is contained in:
alekseybobkov 2015-04-04 18:59:36 -07:00
parent cb0c483857
commit 332d92f538
17 changed files with 511 additions and 43 deletions

View File

@ -78,7 +78,7 @@ var Base=function(){this.proxiedMethods={}}
Base.prototype.dispose=function()
{for(var key in this.proxiedMethods){this.proxiedMethods[key]=null}
this.proxiedMethods=null}
Base.prototype.proxy=function(method,name){if(method.ocProxyId===undefined){$.oc.foundation._proxyCounter++
Base.prototype.proxy=function(method){if(method.ocProxyId===undefined){$.oc.foundation._proxyCounter++
method.ocProxyId=$.oc.foundation._proxyCounter}
if(this.proxiedMethods[method.ocProxyId]!==undefined)
return this.proxiedMethods[method.ocProxyId]

View File

@ -63,7 +63,7 @@
/*
* Creates a proxied method reference or returns an existing proxied method.
*/
Base.prototype.proxy = function(method, name) {
Base.prototype.proxy = function(method) {
if (method.ocProxyId === undefined) {
$.oc.foundation._proxyCounter++
method.ocProxyId = $.oc.foundation._proxyCounter

View File

@ -254,6 +254,10 @@ to{background-position:0 0}
.redactor-toolbar li a{color:#404040;font-size:14px;width:20px;line-height:20px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.redactor-toolbar li a:hover{background-color:#999999;color:#ffffff}
.redactor-toolbar li a:active,.redactor-toolbar li a.redactor-act{background-color:#404040;color:#ffffff}
.redactor-toolbar li a.fa-redactor-btn{padding:9px 10px;line-height:20px}
.redactor-toolbar li a.oc-redactor-button i:before{font-size:16px !important}
.redactor-toolbar li a.oc-autumn-button{color:#c03f31}
.redactor-toolbar li a.oc-autumn-button:hover{color:white !important}
.redactor-editor{border:none;font-size:13px;color:#555555;padding:15px}
.field-richeditor.stretch .redactor-box{display:block;position:relative;height:100% !important;width:100% !important;-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;overflow:hidden}
.field-richeditor.stretch .redactor-box .redactor-toolbar{-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;display:block;border-bottom:none;position:absolute;width:100%;top:0}

View File

@ -1877,8 +1877,8 @@ self.$form.trigger('change')
if(self.$dataLocker)
self.$dataLocker.val(self.syncBefore(this.$editor.html()))}}
if(this.options.fullpage){redactorOptions.fullpage=true}
redactorOptions.plugins=['cleanup','fullscreen','figure','quote','table']
redactorOptions.buttons=['formatting','bold','italic','unorderedlist','orderedlist','image','link','horizontalrule','html'],this.$textarea.redactor(redactorOptions)}
redactorOptions.plugins=['cleanup','fullscreen','figure','quote','table','mediamanager','image']
redactorOptions.buttons=['formatting','bold','italic','unorderedlist','orderedlist','link','horizontalrule','html'],this.$textarea.redactor(redactorOptions)}
RichEditor.prototype.build=function(redactor){this.updateLayout()
$(window).resize($.proxy(this.updateLayout,this))
$(window).on('oc.updateUi',$.proxy(this.updateLayout,this))

View File

@ -81,8 +81,8 @@
// redactorOptions.plugins = ['cleanup', 'fullscreen', 'figure', 'image', 'quote', 'table']
// redactorOptions.buttons = ['formatting', 'bold', 'italic', 'unorderedlist', 'orderedlist', 'link', 'horizontalrule', 'html'],
redactorOptions.plugins = ['cleanup', 'fullscreen', 'figure', 'quote', 'table']
redactorOptions.buttons = ['formatting', 'bold', 'italic', 'unorderedlist', 'orderedlist', 'image', 'link', 'horizontalrule', 'html'],
redactorOptions.plugins = ['cleanup', 'fullscreen', 'figure', 'quote', 'table', 'mediamanager', 'image']
redactorOptions.buttons = ['formatting', 'bold', 'italic', 'unorderedlist', 'orderedlist', 'link', 'horizontalrule', 'html'],
this.$textarea.redactor(redactorOptions)
}

View File

@ -86,6 +86,25 @@
background-color: @color-richeditor-toolbar-btn-bg-active;
color: @color-richeditor-toolbar-btn-color-hover;
}
// For some reasin the line height for fa- buttons was set to 1
// and padding was not matching other buttons. -ab Apr 01 2015
&.fa-redactor-btn {
padding: 9px 10px;
line-height: 20px;
}
&.oc-redactor-button i:before {
font-size: 16px!important;
}
&.oc-autumn-button {
color: #c03f31;
&:hover {
color: white!important;
}
}
}
}

View File

@ -274,6 +274,8 @@ return [
'move_dest_src_match' => 'Please select another destination folder.',
'empty_library' => 'The Media Library is empty. Upload files or create folders to get started.',
'insert' => 'Insert',
'crop_and_insert' => 'Crop & Insert'
'crop_and_insert' => 'Crop & Insert',
'select_single_image' => 'Please select a single image.',
'selection_not_image' => 'The selected item is not an image.'
]
];

View File

@ -32,6 +32,16 @@ class MediaManager extends WidgetBase
protected $brokenImageHash = null;
/**
* @var boolean Determines whether the bottom toolbar is visible.
*/
public $bottomToolbar = false;
/**
* @var boolean Determines whether the Crop & Insert button is visible.
*/
public $cropAndInsertButton = false;
public function __construct($controller, $alias)
{
$this->alias = $alias;
@ -358,9 +368,17 @@ class MediaManager extends WidgetBase
public function onLoadPopup()
{
$this->bottomToolbar = Input::get('bottomToolbar', $this->bottomToolbar);
$this->cropAndInsertButton = Input::get('cropAndInsertButton', $this->cropAndInsertButton);
return $this->makePartial('popup-body');
}
public function onLoadImageCropPopup()
{
return $this->makePartial('image-crop-popup-body');
}
//
// Methods for th internal use
//
@ -514,9 +532,9 @@ class MediaManager extends WidgetBase
{
$this->addCss('css/mediamanager.css', 'core');
// TODO: combine and load combined
// TODO: combine and minify
$this->addJs('js/mediamanager.js', 'core');
$this->addJs('js/mediamanager.popup.js', 'core');
$this->addJs('js/mediamanager.imagecroppopup.js', 'core');
}
protected function setViewMode($viewMode)

View File

@ -1,3 +1,6 @@
div[data-control="media-manager"]:focus {
outline: none;
}
div[data-control="media-manager"] audio,
div[data-control="media-manager"] video {
width: 100%;
@ -343,6 +346,7 @@ div[data-control="media-manager"] div[data-control="selection-marker"] {
position: absolute;
z-index: 250;
border: 1px dashed #95a5a6;
background: rgba(0, 0, 0, 0.1);
}
div[data-control="media-manager"] .upload-progress {
background: #f9f9f9;

View File

@ -0,0 +1,93 @@
/*
* Media manager image editor popup
*/
+function ($) { "use strict";
if ($.oc.mediaManager === undefined)
$.oc.mediaManager = {}
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var MediaManagerImageCropPopup = function(path, options) {
this.$popupRootElement = null
this.options = $.extend({}, MediaManagerImageCropPopup.DEFAULTS, options)
this.path = path
Base.call(this)
this.init()
this.show()
}
MediaManagerImageCropPopup.prototype = Object.create(BaseProto)
MediaManagerImageCropPopup.prototype.constructor = MediaManagerImageCropPopup
MediaManagerImageCropPopup.prototype.dispose = function() {
this.unregisterHandlers()
this.$popupRootElement.remove()
this.$popupRootElement = null
this.$popupElement = null
BaseProto.dispose.call(this)
}
MediaManagerImageCropPopup.prototype.init = function() {
if (this.options.alias === undefined)
throw new Error('Media Manager image crop popup option "alias" is not set.')
this.$popupRootElement = $('<div/>')
this.registerHandlers()
}
MediaManagerImageCropPopup.prototype.show = function() {
var data = {
path: this.path
}
this.$popupRootElement.popup({
extraData: data,
size: 'adaptive',
adaptiveHeight: true,
handler: this.options.alias + '::onLoadImageCropPopup'
})
}
MediaManagerImageCropPopup.prototype.registerHandlers = function() {
this.$popupRootElement.one('hide.oc.popup', this.proxy(this.onPopupHidden))
this.$popupRootElement.one('shown.oc.popup', this.proxy(this.onPopupShown))
}
MediaManagerImageCropPopup.prototype.unregisterHandlers = function() {
}
MediaManagerImageCropPopup.prototype.hide = function() {
if (this.$popupElement)
this.$popupElement.trigger('close.oc.popup')
}
// EVENT HANDLERS
// ============================
MediaManagerImageCropPopup.prototype.onPopupHidden = function(event, element, popup) {
// Release clickedElement reference inside redactor.js
// If we don't do it, the image editor popup DOM elements
// won't be removed from the memory.
$(document).trigger('mousedown')
this.dispose()
}
MediaManagerImageCropPopup.prototype.onPopupShown = function(event, element, popup) {
this.$popupElement = popup
}
MediaManagerImageCropPopup.DEFAULTS = {
alias: undefined,
onDone: undefined
}
$.oc.mediaManager.imageCropPopup = MediaManagerImageCropPopup
}(window.jQuery);

View File

@ -6,7 +6,8 @@
*/
+function ($) { "use strict";
$.oc.mediaManager = {}
if ($.oc.mediaManager === undefined)
$.oc.mediaManager = {}
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
@ -19,7 +20,6 @@
this.$form = this.$el.closest('form')
this.options = options
Base.call(this)
// State properties
@ -73,6 +73,30 @@
BaseProto.dispose.call(this)
}
MediaManager.prototype.getSelectedItems = function(returnNotProcessed) {
var items = this.$el.get(0).querySelectorAll('[data-type="media-item"].selected'),
result = []
if (returnNotProcessed === true)
return items
for (var i=0, len=items.length; i < len; i++) {
var item = items[i],
itemDetails = {
itemType: item.getAttribute('data-item-type'),
path: item.getAttribute('data-path'),
title: item.getAttribute('data-title'),
documentType: item.getAttribute('data-document-type'),
folder: item.getAttribute('data-folder'),
publicUrl: item.getAttribute('data-public-url')
}
result.push(itemDetails)
}
return result
}
// MEDIA MANAGER INTERNAL METHODS
// ============================
@ -84,7 +108,7 @@
this.$el.find('[data-control="bottom-toolbar"]').removeClass('hide')
if (this.options.cropAndInsertButton)
this.$el.find('[data-command="crop-and-insert"]').removeClass('hide')
this.$el.find('[data-popup-command="crop-and-insert"]').removeClass('hide')
}
this.registerHandlers()
@ -116,6 +140,7 @@
this.$el.on('hidden.oc.popup', '[data-command="create-folder"]', this.proxy(this.onFolderPopupHidden))
this.$el.on('shown.oc.popup', '[data-command="move"]', this.proxy(this.onMovePopupShown))
this.$el.on('hidden.oc.popup', '[data-command="move"]', this.proxy(this.onMovePopupHidden))
this.$el.on('keydown', this.proxy(this.onKeyDown))
if (this.itemListElement)
this.itemListElement.addEventListener('mousedown', this.proxy(this.onListMouseDown))
@ -137,6 +162,7 @@
this.$el.off('hidden.oc.popup', '[data-command="create-folder"]', this.proxy(this.onFolderPopupHidden))
this.$el.off('shown.oc.popup', '[data-command="move"]', this.proxy(this.onMovePopupShown))
this.$el.off('hidden.oc.popup', '[data-command="move"]', this.proxy(this.onMovePopupHidden))
this.$el.off('keydown', this.proxy(this.onKeyDown))
if (this.itemListElement) {
this.itemListElement.removeEventListener('mousedown', this.proxy(this.onListMouseDown))
@ -437,6 +463,9 @@
}
MediaManager.prototype.replaceSidebarPlaceholder = function(response) {
if (!this.sidebarPreviewElement)
return
var sidebarThumbnail = this.sidebarPreviewElement.querySelector('[data-control="sidebar-thumbnail"]')
if (!sidebarThumbnail)
return
@ -674,6 +703,33 @@
})
}
//
// Cropping images
//
MediaManager.prototype.cropSelectedImage = function(callback) {
var selectedItems = this.getSelectedItems(true)
if (selectedItems.length != 1) {
alert(this.options.selectSingleImage)
return
}
if (selectedItems[0].getAttribute('data-document-type') !== 'image') {
alert(this.options.selectionNotImage)
return
}
new $.oc.mediaManager.imageCropPopup(
selectedItems[0].getAttribute('data-path'), {
alias: this.options.alias
})
}
MediaManager.prototype.onImageCropped = function(imageItem) {
}
//
// Search
//
@ -890,6 +946,11 @@
this.resetSearch()
this.gotoFolder($item.data('path'), true)
}
}
else if ($item.data('item-type') == 'file') {
// Trigger the Insert popup command if a file item
// was double clicked.
this.$el.trigger('popupcommand', ['insert'])
}
return false
@ -926,6 +987,14 @@
case 'toggle-sidebar':
this.toggleSidebar(ev)
break;
case 'popup-command':
var popupCommand = $(ev.currentTarget).data('popup-command')
if (popupCommand !== 'crop-and-insert')
this.$el.trigger('popupcommand', [popupCommand])
else
this.cropSelectedImage(this.proxy(this.onImageCropped))
break;
}
return false
@ -1057,6 +1126,23 @@
this.execNavigationRequest('onSetSorting', data)
}
MediaManager.prototype.onKeyDown = function(ev) {
var eventHandled = false
if (ev.which == 13) {
// Trigger the Insert popup command when Enter
// key is pressed
this.$el.trigger('popupcommand', ['insert'])
eventHandled = true
}
if (eventHandled) {
ev.preventDefault()
ev.stopPropagation()
}
}
// MEDIA MANAGER PLUGIN DEFINITION
// ============================
@ -1065,6 +1151,8 @@
deleteEmpty: 'Please select files to delete.',
deleteConfirm: 'Do you really want to delete the selected file(s)?',
moveEmpty: 'Please select files to move.',
selectSingleImage: 'Please select a single image.',
selectionNotImage: 'The selected item is not an image.',
bottomToolbar: false,
cropAndInsertButton: false
}

View File

@ -1,38 +1,131 @@
/*
* Media manager popup
*
*/
+function ($) { "use strict";
if ($.oc.mediaManager === undefined)
$.oc.mediaManager = {}
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var MediaManagerPopup = function(options) {
this.$popupRootElement = null
this.options = $.extend({}, MediaManagerPopup.DEFAULTS, options)
Base.call(this)
this.init()
this.show()
}
MediaManagerPopup.prototype = Object.create(BaseProto)
MediaManagerPopup.prototype.constructor = MediaManagerPopup
MediaManagerPopup.prototype.show = function() {
var $popupRootElement = $('<div/>')
MediaManagerPopup.prototype.dispose = function() {
this.unregisterHandlers()
this.$popupRootElement.remove()
this.$popupRootElement = null
this.$popupElement = null
BaseProto.dispose.call(this)
}
MediaManagerPopup.prototype.init = function() {
if (this.options.alias === undefined)
throw new Error('Media Manager popup option "alias" is not set.')
$popupRootElement.popup({
this.$popupRootElement = $('<div/>')
this.registerHandlers()
}
MediaManagerPopup.prototype.registerHandlers = function() {
this.$popupRootElement.one('hide.oc.popup', this.proxy(this.onPopupHidden))
this.$popupRootElement.one('shown.oc.popup', this.proxy(this.onPopupShown))
}
MediaManagerPopup.prototype.unregisterHandlers = function() {
this.$popupElement.off('popupcommand', this.proxy(this.onPopupCommand))
this.$popupRootElement.off('popupcommand', this.proxy(this.onPopupCommand))
}
MediaManagerPopup.prototype.show = function() {
var data = {
bottomToolbar: this.options.bottomToolbar ? 1 : 0,
cropAndInsertButton: this.options.cropAndInsertButton ? 1 : 0
}
console.log(data)
this.$popupRootElement.popup({
extraData: data,
size: 'adaptive',
adaptiveHeight: true,
handler: this.options.alias + '::onLoadPopup'
})
}
MediaManagerPopup.prototype.hide = function() {
if (this.$popupElement)
this.$popupElement.trigger('close.oc.popup')
}
MediaManagerPopup.prototype.getMediaManagerElement = function() {
return this.$popupElement.find('[data-control="media-manager"]')
}
MediaManagerPopup.prototype.insertMedia = function() {
var items = this.getMediaManagerElement().mediaManager('getSelectedItems')
if (this.options.onInsert !== undefined)
this.options.onInsert.call(this, items)
}
// EVENT HANDLERS
// ============================
MediaManagerPopup.prototype.onPopupHidden = function(event, element, popup) {
var mediaManager = this.getMediaManagerElement()
mediaManager.mediaManager('dispose')
mediaManager.remove()
// Release clickedElement reference inside redactor.js
// If we don't do it, the Media Manager popup DOM elements
// won't be removed from the memory.
$(document).trigger('mousedown')
this.dispose()
if (this.options.onClose !== undefined)
this.options.onClose.call(this)
}
MediaManagerPopup.prototype.onPopupShown = function(event, element, popup) {
this.$popupElement = popup
this.$popupElement.on('popupcommand', this.proxy(this.onPopupCommand))
// Unfocus the Redactor field, otherwise all keyboard commands
// in the Media Manager popup translate to Redactor.
this.getMediaManagerElement().find('[data-control="sorting"]').focus().blur()
}
MediaManagerPopup.prototype.onPopupCommand = function(ev, command) {
switch (command) {
case 'insert' :
this.insertMedia()
break;
}
return false
}
MediaManagerPopup.DEFAULTS = {
alias: undefined
alias: undefined,
bottomToolbar: true,
cropAndInsertButton: false,
onInsert: undefined,
onClose: undefined
}
$.oc.mediaManager.popup = MediaManagerPopup

View File

@ -0,0 +1,131 @@
if (!RedactorPlugins) var RedactorPlugins = {};
RedactorPlugins.mediamanager = function()
{
// This plugin uses Redactor's undocumented internals directly (this.link).
// If Redactor changes the internal link API, we will need
// to rewrite the code.
function hideLinkTooltips() {
$('.redactor-link-tooltip').remove()
}
return {
init: function()
{
// Insert link button
var buttonInsertLink = this.button.add('mmInsertMediaLink', 'Insert Media Link');
this.button.setAwesome('mmInsertMediaLink', 'icon-link');
buttonInsertLink.addClass('oc-redactor-button oc-autumn-button')
this.button.addCallback(buttonInsertLink, this.mediamanager.onInsertLink);
// Insert image button
var buttonInsertImage = this.button.add('mmInsertImageLink', 'Insert Media Image');
buttonInsertImage.addClass('re-image oc-autumn-button')
buttonInsertImage.removeClass('redactor-btn-image')
this.button.addCallback(buttonInsertImage, this.mediamanager.onInsertImage);
},
onInsertLink: function(buttonName)
{
var that = this
hideLinkTooltips()
this.selection.save()
this.link.getData()
new $.oc.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: false,
onInsert: function(items) {
if (!items.length) {
alert('Please select files to insert links to.')
return
}
if (items.length > 1) {
alert('Please select a single file.')
return
}
var text = that.link.text,
textIsEmpty = $.trim(text) === ''
for (var i=0, len=items.length; i<len; i++) {
var text = textIsEmpty ? items[i].title : text
that.link.set(text, items[i].publicUrl, '')
}
this.hide()
}
})
},
onInsertImage: function(buttonName)
{
hideLinkTooltips()
if (!this.selection.getCurrent())
this.focus.setStart();
this.selection.save()
var that = this
new $.oc.mediaManager.popup({
alias: 'ocmediamanager',
cropAndInsertButton: true,
onInsert: function(items) {
if (!items.length) {
alert('Please select image(s) to insert.')
return
}
that.selection.restore()
// The code is partially borrowed from redactor.js, image plugin
var isP = that.utils.isCurrentOrParent('P'),
html = '',
imagesInserted = 0
for (var i=0, len=items.length; i<len; i++) {
if (items[i].documentType !== 'image') {
alert('The file "'+items[i].title+'" is not an image.')
continue
}
var $img = $('<img>')
.attr('src', items[i].publicUrl)
.attr('data-redactor-inserted-image', 'true')
html += that.utils.getOuterHtml($img)
imagesInserted++
}
if (imagesInserted > 0) {
if (isP)
html = '<blockquote>' + html + '</blockquote>'
that.selection.restore()
that.buffer.set()
that.insert.html(html, false);
var $image = that.$editor.find('img[data-redactor-inserted-image=true]').removeAttr('data-redactor-inserted-image')
if (isP)
$image.parent().contents().unwrap().wrap('<p />')
else {
if (that.opts.linebreaks)
$image.before('<br>').after('<br>')
}
}
if (imagesInserted !== 0)
this.hide()
}
})
}
};
};

View File

@ -32,6 +32,10 @@
}
div[data-control="media-manager"] {
&:focus {
outline: none;
}
.loading-indicator-pseudo-absolute(@size) {
background-image: url(../../../../../../modules/backend/assets/images/loading-indicator-transparent.svg);
background-position: 50% 50%;
@ -403,6 +407,7 @@ div[data-control="media-manager"] {
position: absolute;
z-index: 250;
border: 1px dashed #95a5a6;
background: rgba(0,0,0,0.1);
}
.upload-progress {

View File

@ -5,6 +5,11 @@
data-delete-empty="<?= e(trans('cms::lang.media.delete_empty')) ?>"
data-delete-confirm="<?= e(trans('cms::lang.media.delete_confirm')) ?>"
data-move-empty="<?= e(trans('cms::lang.media.move_empty')) ?>"
data-select-single-image="<?= e(trans('cms::lang.media.select_single_image')) ?>"
data-selection-not-image="<?= e(trans('cms::lang.media.selection_not_image')) ?>"
data-bottom-toolbar="<?= $this->bottomToolbar ? 'true' : 'false' ?>"
data-crop-and-insert-button="<?= $this->cropAndInsertButton ? 'true' : 'false' ?>"
tabindex="0"
>
<?= $this->makePartial('toolbar') ?>
@ -48,32 +53,7 @@
</div>
<div class="layout-row min-size hide" data-control="bottom-toolbar">
<div class="panel no-padding-bottom border-top">
<div class="form-buttons">
<div class="pull-right">
<button
type="button"
data-command="insert"
class="btn btn-primary">
<?= e(trans('cms::lang.media.insert')) ?>
</button>
<button
type="button"
data-command="crop-and-insert"
class="btn btn-primary hide">
<?= e(trans('cms::lang.media.crop_and_insert')) ?>
</button>
<button
type="button"
data-command="insert"
class="btn btn-default no-margin-right">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
</div>
</div>
<?= $this->makePartial('bottom-toolbar') ?>
</div>
</div>
</div>

View File

@ -0,0 +1,28 @@
<div class="panel no-padding-bottom border-top">
<div class="form-buttons">
<div class="pull-right">
<button
type="button"
data-command="popup-command"
data-popup-command="insert"
class="btn btn-primary">
<?= e(trans('cms::lang.media.insert')) ?>
</button>
<button
type="button"
data-command="popup-command"
data-popup-command="crop-and-insert"
class="btn btn-primary hide">
<?= e(trans('cms::lang.media.crop_and_insert')) ?>
</button>
<button
type="button"
data-dismiss="popup"
class="btn btn-default no-margin-right">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
<?= Form::open(['class'=>'layout', 'onsubmit'=>'return false']) ?>
<p>Something</p>
<?= Form::close() ?>