forked from Techsystech/web
Merge pull request #407 from LasLabs/release/9.0/web_widget_darkroom
[9.0][ADD] web_widget_darkroom: Add o2m DarkroomJS widget9.0
commit
6ede3b1673
|
@ -0,0 +1,99 @@
|
|||
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
|
||||
================================
|
||||
DarkroomJS Image Editing for Web
|
||||
================================
|
||||
|
||||
This module provides a `DarkroomJS`_ (v2.0.1) web widget for use with image
|
||||
fields. It also adds a Darkroom button to the normal image widget, which can
|
||||
be used to edit the image via Darkroom in a modal.
|
||||
|
||||
.. _DarkroomJS: https://github.com/MattKetmo/darkroomjs
|
||||
|
||||
The widget currently supports the following operations and can be extended to
|
||||
allow others:
|
||||
|
||||
* Zoom and pan
|
||||
* Rotate
|
||||
* Crop
|
||||
* Step back in history client-side (before save)
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
After installing the module, you can use it in the following ways:
|
||||
|
||||
* Specify the ``darkroom`` widget when adding an image field to a view.
|
||||
Configuration values can be provided using the ``options`` attribute::
|
||||
|
||||
<field name="image" widget="darkroom" options="{'minWidth': 100}"/>
|
||||
|
||||
The widget passes options directly through to DarkroomJS, which supports the
|
||||
following:
|
||||
|
||||
* minWidth
|
||||
* minHeight
|
||||
* maxWidth
|
||||
* maxHeight
|
||||
* ratio (aspect ratio)
|
||||
* backgroundColor
|
||||
|
||||
* Open a form view that contains an image in edit mode and hover over the
|
||||
image widget. You should see a Darkoom button that can be clicked to open
|
||||
the image in a Darkroom modal, where it can be edited and the changes can be
|
||||
saved.
|
||||
|
||||
.. image:: /web_widget_darkroom/static/description/modal_screenshot_1.png
|
||||
:alt: Darkroom Modal Screenshot 1
|
||||
:class: img-thumbnail
|
||||
:height: 260
|
||||
|
||||
.. image:: /web_widget_darkroom/static/description/modal_screenshot_2.png
|
||||
:alt: Darkroom Modal Screenshot 2
|
||||
:class: img-thumbnail col-xs-offset-1
|
||||
:height: 260
|
||||
|
||||
Known Issues / Roadmap
|
||||
======================
|
||||
|
||||
* Darkroom modals are currently not supported during record creation
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_. In
|
||||
case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smash it by providing detailed and welcome
|
||||
feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Images
|
||||
------
|
||||
|
||||
* Odoo Community Association:
|
||||
`Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Dave Lasley <dave@laslabs.com>
|
||||
* Oleg Bulkin <obulkin@laslabs.com>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from . import wizards
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
{
|
||||
'name': 'Web DarkroomJS Image Editing',
|
||||
'summary': 'Provides web widget for image editing and adds it to standard'
|
||||
' image widget as modal',
|
||||
'version': '9.0.1.0.1',
|
||||
'category': 'Web',
|
||||
'website': 'https://laslabs.com/',
|
||||
'author': 'LasLabs, Odoo Community Association (OCA)',
|
||||
'license': 'LGPL-3',
|
||||
'application': False,
|
||||
'installable': True,
|
||||
'depends': [
|
||||
'web',
|
||||
],
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'wizards/darkroom_modal.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/field_templates.xml',
|
||||
],
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
|
@ -0,0 +1,356 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Darkroom = Darkroom;
|
||||
|
||||
// Core object of DarkroomJS.
|
||||
// Basically it's a single object, instanciable via an element
|
||||
// (it could be a CSS selector or a DOM element), some custom options,
|
||||
// and a list of plugin objects (or none to use default ones).
|
||||
function Darkroom(element, options, plugins) {
|
||||
return this.constructor(element, options, plugins);
|
||||
}
|
||||
|
||||
// Create an empty list of plugin objects, which will be filled by
|
||||
// other plugin scripts. This is the default plugin list if none is
|
||||
// specified in Darkroom's constructor.
|
||||
Darkroom.plugins = [];
|
||||
|
||||
Darkroom.prototype = {
|
||||
// Reference to the main container element
|
||||
containerElement: null,
|
||||
|
||||
// Reference to the Fabric canvas object
|
||||
canvas: null,
|
||||
|
||||
// Reference to the Fabric image object
|
||||
image: null,
|
||||
|
||||
// Reference to the Fabric source canvas object
|
||||
sourceCanvas: null,
|
||||
|
||||
// Reference to the Fabric source image object
|
||||
sourceImage: null,
|
||||
|
||||
// Track of the original image element
|
||||
originalImageElement: null,
|
||||
|
||||
// Stack of transformations to apply to the image source
|
||||
transformations: [],
|
||||
|
||||
// Default options
|
||||
defaults: {
|
||||
// Canvas properties (dimension, ratio, color)
|
||||
minWidth: null,
|
||||
minHeight: null,
|
||||
maxWidth: null,
|
||||
maxHeight: null,
|
||||
ratio: null,
|
||||
backgroundColor: '#fff',
|
||||
|
||||
// Plugins options
|
||||
plugins: {},
|
||||
|
||||
// Post-initialisation callback
|
||||
initialize: function() { /* noop */ }
|
||||
},
|
||||
|
||||
// List of the instancied plugins
|
||||
plugins: {},
|
||||
|
||||
// This options are a merge between `defaults` and the options passed
|
||||
// through the constructor
|
||||
options: {},
|
||||
|
||||
constructor: function(element, options) {
|
||||
this.options = Darkroom.Utils.extend(options, this.defaults);
|
||||
|
||||
if (typeof element === 'string')
|
||||
element = document.querySelector(element);
|
||||
if (null === element)
|
||||
return;
|
||||
|
||||
var image = new Image();
|
||||
var parent = element.parentElement;
|
||||
image.onload = function() {
|
||||
// Initialize the DOM/Fabric elements
|
||||
this._initializeDOM(element, parent);
|
||||
this._initializeImage();
|
||||
|
||||
// Then initialize the plugins
|
||||
this._initializePlugins(Darkroom.plugins);
|
||||
|
||||
// Public method to adjust image according to the canvas
|
||||
this.refresh(function() {
|
||||
// Execute a custom callback after initialization
|
||||
this.options.initialize.bind(this).call();
|
||||
}.bind(this));
|
||||
}.bind(this);
|
||||
|
||||
image.src = element.src;
|
||||
},
|
||||
|
||||
selfDestroy: function() {
|
||||
var container = this.containerElement;
|
||||
var image = new Image();
|
||||
image.onload = function() {
|
||||
container.parentNode.replaceChild(image, container);
|
||||
};
|
||||
|
||||
image.src = this.sourceImage.toDataURL();
|
||||
},
|
||||
|
||||
// Add ability to attach event listener on the core object.
|
||||
// It uses the canvas element to process events.
|
||||
addEventListener: function(eventName, callback) {
|
||||
var el = this.canvas.getElement();
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener(eventName, callback);
|
||||
} else if (el.attachEvent) {
|
||||
el.attachEvent('on' + eventName, callback);
|
||||
}
|
||||
},
|
||||
|
||||
dispatchEvent: function(eventName) {
|
||||
// Use the old way of creating event to be IE compatible
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
|
||||
var event = document.createEvent('Event');
|
||||
event.initEvent(eventName, true, true);
|
||||
|
||||
this.canvas.getElement().dispatchEvent(event);
|
||||
},
|
||||
|
||||
// Adjust image & canvas dimension according to min/max width/height
|
||||
// and ratio specified in the options.
|
||||
// This method should be called after each image transformation.
|
||||
refresh: function(next) {
|
||||
var clone = new Image();
|
||||
clone.onload = function() {
|
||||
this._replaceCurrentImage(new fabric.Image(clone));
|
||||
if (next) next();
|
||||
}.bind(this);
|
||||
clone.src = this.sourceImage.toDataURL();
|
||||
},
|
||||
|
||||
_replaceCurrentImage: function(newImage) {
|
||||
if (this.image) {
|
||||
this.image.remove();
|
||||
}
|
||||
|
||||
this.image = newImage;
|
||||
this.image.selectable = false;
|
||||
|
||||
// Adjust width or height according to specified ratio
|
||||
var viewport = Darkroom.Utils.computeImageViewPort(this.image);
|
||||
var canvasWidth = viewport.width;
|
||||
var canvasHeight = viewport.height;
|
||||
|
||||
if (null !== this.options.ratio) {
|
||||
var canvasRatio = +this.options.ratio;
|
||||
var currentRatio = canvasWidth / canvasHeight;
|
||||
|
||||
if (currentRatio > canvasRatio) {
|
||||
canvasHeight = canvasWidth / canvasRatio;
|
||||
} else if (currentRatio < canvasRatio) {
|
||||
canvasWidth = canvasHeight * canvasRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// Then scale the image to fit into dimension limits
|
||||
var scaleMin = 1;
|
||||
var scaleMax = 1;
|
||||
var scaleX = 1;
|
||||
var scaleY = 1;
|
||||
|
||||
if (null !== this.options.maxWidth && this.options.maxWidth < canvasWidth) {
|
||||
scaleX = this.options.maxWidth / canvasWidth;
|
||||
}
|
||||
if (null !== this.options.maxHeight && this.options.maxHeight < canvasHeight) {
|
||||
scaleY = this.options.maxHeight / canvasHeight;
|
||||
}
|
||||
scaleMin = Math.min(scaleX, scaleY);
|
||||
|
||||
scaleX = 1;
|
||||
scaleY = 1;
|
||||
if (null !== this.options.minWidth && this.options.minWidth > canvasWidth) {
|
||||
scaleX = this.options.minWidth / canvasWidth;
|
||||
}
|
||||
if (null !== this.options.minHeight && this.options.minHeight > canvasHeight) {
|
||||
scaleY = this.options.minHeight / canvasHeight;
|
||||
}
|
||||
scaleMax = Math.max(scaleX, scaleY);
|
||||
|
||||
var scale = scaleMax * scaleMin; // one should be equals to 1
|
||||
|
||||
canvasWidth *= scale;
|
||||
canvasHeight *= scale;
|
||||
|
||||
// Finally place the image in the center of the canvas
|
||||
this.image.setScaleX(1 * scale);
|
||||
this.image.setScaleY(1 * scale);
|
||||
this.canvas.add(this.image);
|
||||
this.canvas.setWidth(canvasWidth);
|
||||
this.canvas.setHeight(canvasHeight);
|
||||
this.canvas.centerObject(this.image);
|
||||
this.image.setCoords();
|
||||
},
|
||||
|
||||
// Apply the transformation on the current image and save it in the
|
||||
// transformations stack (in order to reconstitute the previous states
|
||||
// of the image).
|
||||
applyTransformation: function(transformation) {
|
||||
this.transformations.push(transformation);
|
||||
|
||||
transformation.applyTransformation(
|
||||
this.sourceCanvas,
|
||||
this.sourceImage,
|
||||
this._postTransformation.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
_postTransformation: function(newImage) {
|
||||
if (newImage)
|
||||
this.sourceImage = newImage;
|
||||
|
||||
this.refresh(function() {
|
||||
this.dispatchEvent('core:transformation');
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
// Initialize image from original element plus re-apply every
|
||||
// transformations.
|
||||
reinitializeImage: function() {
|
||||
this.sourceImage.remove();
|
||||
this._initializeImage();
|
||||
this._popTransformation(this.transformations.slice());
|
||||
},
|
||||
|
||||
_popTransformation: function(transformations) {
|
||||
if (0 === transformations.length) {
|
||||
this.dispatchEvent('core:reinitialized');
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
var transformation = transformations.shift();
|
||||
|
||||
var next = function(newImage) {
|
||||
if (newImage) this.sourceImage = newImage;
|
||||
this._popTransformation(transformations);
|
||||
};
|
||||
|
||||
transformation.applyTransformation(
|
||||
this.sourceCanvas,
|
||||
this.sourceImage,
|
||||
next.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
// Create the DOM elements and instanciate the Fabric canvas.
|
||||
// The image element is replaced by a new `div` element.
|
||||
// However the original image is re-injected in order to keep a trace of it.
|
||||
_initializeDOM: function(imageElement) {
|
||||
// Container
|
||||
var mainContainerElement = document.createElement('div');
|
||||
mainContainerElement.className = 'darkroom-container';
|
||||
|
||||
// Toolbar
|
||||
var toolbarElement = document.createElement('div');
|
||||
toolbarElement.className = 'darkroom-toolbar';
|
||||
mainContainerElement.appendChild(toolbarElement);
|
||||
|
||||
// Viewport canvas
|
||||
var canvasContainerElement = document.createElement('div');
|
||||
canvasContainerElement.className = 'darkroom-image-container';
|
||||
var canvasElement = document.createElement('canvas');
|
||||
canvasContainerElement.appendChild(canvasElement);
|
||||
mainContainerElement.appendChild(canvasContainerElement);
|
||||
|
||||
// Source canvas
|
||||
var sourceCanvasContainerElement = document.createElement('div');
|
||||
sourceCanvasContainerElement.className = 'darkroom-source-container';
|
||||
sourceCanvasContainerElement.style.display = 'none';
|
||||
var sourceCanvasElement = document.createElement('canvas');
|
||||
sourceCanvasContainerElement.appendChild(sourceCanvasElement);
|
||||
mainContainerElement.appendChild(sourceCanvasContainerElement);
|
||||
|
||||
// Original image
|
||||
imageElement.parentNode.replaceChild(mainContainerElement, imageElement);
|
||||
imageElement.style.display = 'none';
|
||||
mainContainerElement.appendChild(imageElement);
|
||||
|
||||
// Instanciate object from elements
|
||||
this.containerElement = mainContainerElement;
|
||||
this.originalImageElement = imageElement;
|
||||
|
||||
this.toolbar = new Darkroom.UI.Toolbar(toolbarElement);
|
||||
|
||||
this.canvas = new fabric.Canvas(canvasElement, {
|
||||
selection: false,
|
||||
backgroundColor: this.options.backgroundColor,
|
||||
});
|
||||
|
||||
this.sourceCanvas = new fabric.Canvas(sourceCanvasElement, {
|
||||
selection: false,
|
||||
backgroundColor: this.options.backgroundColor,
|
||||
});
|
||||
},
|
||||
|
||||
// Instanciate the Fabric image object.
|
||||
// The image is created as a static element with no control,
|
||||
// then it is add in the Fabric canvas object.
|
||||
_initializeImage: function() {
|
||||
this.sourceImage = new fabric.Image(this.originalImageElement, {
|
||||
// Some options to make the image static
|
||||
selectable: false,
|
||||
evented: false,
|
||||
lockMovementX: true,
|
||||
lockMovementY: true,
|
||||
lockRotation: true,
|
||||
lockScalingX: true,
|
||||
lockScalingY: true,
|
||||
lockUniScaling: true,
|
||||
hasControls: false,
|
||||
hasBorders: false,
|
||||
});
|
||||
|
||||
this.sourceCanvas.add(this.sourceImage);
|
||||
|
||||
// Adjust width or height according to specified ratio
|
||||
var viewport = Darkroom.Utils.computeImageViewPort(this.sourceImage);
|
||||
var canvasWidth = viewport.width;
|
||||
var canvasHeight = viewport.height;
|
||||
|
||||
this.sourceCanvas.setWidth(canvasWidth);
|
||||
this.sourceCanvas.setHeight(canvasHeight);
|
||||
this.sourceCanvas.centerObject(this.sourceImage);
|
||||
this.sourceImage.setCoords();
|
||||
},
|
||||
|
||||
// Initialize every plugins.
|
||||
// Note that plugins are instanciated in the same order than they
|
||||
// are declared in the parameter object.
|
||||
_initializePlugins: function(plugins) {
|
||||
for (var name in plugins) {
|
||||
var plugin = plugins[name];
|
||||
var options = this.options.plugins[name];
|
||||
|
||||
// Setting false into the plugin options will disable the plugin
|
||||
if (options === false)
|
||||
continue;
|
||||
|
||||
// Avoid any issues with _proto_
|
||||
if (!plugins.hasOwnProperty(name))
|
||||
continue;
|
||||
|
||||
this.plugins[name] = new plugin(this, options);
|
||||
}
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Darkroom.Plugin = Plugin;
|
||||
|
||||
// Define a plugin object. This is the (abstract) parent class which
|
||||
// has to be extended for each plugin.
|
||||
function Plugin(darkroom, options) {
|
||||
this.darkroom = darkroom;
|
||||
this.options = Darkroom.Utils.extend(options, this.defaults);
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
Plugin.prototype = {
|
||||
defaults: {},
|
||||
initialize: function() { /* no-op */ }
|
||||
};
|
||||
|
||||
// Inspired by Backbone.js extend capability.
|
||||
Plugin.extend = function(protoProps) {
|
||||
var parent = this;
|
||||
var child;
|
||||
|
||||
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
||||
child = protoProps.constructor;
|
||||
} else {
|
||||
child = function() { return parent.apply(this, arguments); };
|
||||
}
|
||||
|
||||
Darkroom.Utils.extend(child, parent);
|
||||
|
||||
var Surrogate = function() { this.constructor = child; };
|
||||
Surrogate.prototype = parent.prototype;
|
||||
child.prototype = new Surrogate();
|
||||
|
||||
if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps);
|
||||
child.__super__ = parent.prototype;
|
||||
|
||||
return child;
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Darkroom.Transformation = Transformation;
|
||||
|
||||
function Transformation(options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
Transformation.prototype = {
|
||||
applyTransformation: function() { /* no-op */ }
|
||||
};
|
||||
|
||||
// Inspired by Backbone.js extend capability.
|
||||
Transformation.extend = function(protoProps) {
|
||||
var parent = this;
|
||||
var child;
|
||||
|
||||
if (protoProps && protoProps.hasOwnProperty('constructor')) {
|
||||
child = protoProps.constructor;
|
||||
} else {
|
||||
child = function() { return parent.apply(this, arguments); };
|
||||
}
|
||||
|
||||
Darkroom.Utils.extend(child, parent);
|
||||
|
||||
var Surrogate = function() { this.constructor = child; };
|
||||
Surrogate.prototype = parent.prototype;
|
||||
child.prototype = new Surrogate();
|
||||
|
||||
if (protoProps) Darkroom.Utils.extend(child.prototype, protoProps);
|
||||
|
||||
child.__super__ = parent.prototype;
|
||||
|
||||
return child;
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Darkroom.Utils = {
|
||||
extend: extend,
|
||||
computeImageViewPort: computeImageViewPort,
|
||||
};
|
||||
|
||||
// Utility method to easily extend objects.
|
||||
function extend(b, a) {
|
||||
var prop;
|
||||
if (b === undefined) {
|
||||
return a;
|
||||
}
|
||||
|
||||
for (prop in a) {
|
||||
if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop) === false) {
|
||||
b[prop] = a[prop];
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
function computeImageViewPort(image) {
|
||||
return {
|
||||
height: Math.abs(image.getWidth() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getHeight() * (Math.cos(image.getAngle() * Math.PI/180))),
|
||||
width: Math.abs(image.getHeight() * (Math.sin(image.getAngle() * Math.PI/180))) + Math.abs(image.getWidth() * (Math.cos(image.getAngle() * Math.PI/180))),
|
||||
};
|
||||
}
|
||||
})();
|
|
@ -0,0 +1,693 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_crop', function(){
|
||||
'use strict';
|
||||
|
||||
var DarkroomPluginCrop = function() {
|
||||
var Crop = Darkroom.Transformation.extend({
|
||||
applyTransformation: function(canvas, image, next) {
|
||||
// Snapshot the image delimited by the crop zone
|
||||
var snapshot = new Image();
|
||||
snapshot.onload = function() {
|
||||
var width = this.width;
|
||||
var height = this.height;
|
||||
|
||||
// Validate image
|
||||
if (height < 1 || width < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var imgInstance = new fabric.Image(this, {
|
||||
// Options to make the image static
|
||||
selectable: false,
|
||||
evented: false,
|
||||
lockMovementX: true,
|
||||
lockMovementY: true,
|
||||
lockRotation: true,
|
||||
lockScalingX: true,
|
||||
lockScalingY: true,
|
||||
lockUniScaling: true,
|
||||
hasControls: false,
|
||||
hasBorders: false,
|
||||
});
|
||||
|
||||
// Update canvas size
|
||||
canvas.setWidth(width);
|
||||
canvas.setHeight(height);
|
||||
|
||||
// Add image
|
||||
image.remove();
|
||||
canvas.add(imgInstance);
|
||||
|
||||
next(imgInstance);
|
||||
};
|
||||
|
||||
var viewport = Darkroom.Utils.computeImageViewPort(image);
|
||||
var imageWidth = viewport.width;
|
||||
var imageHeight = viewport.height;
|
||||
|
||||
var left = this.options.left * imageWidth;
|
||||
var top = this.options.top * imageHeight;
|
||||
var width = Math.min(this.options.width * imageWidth, imageWidth - left);
|
||||
var height = Math.min(this.options.height * imageHeight, imageHeight - top);
|
||||
|
||||
snapshot.src = canvas.toDataURL({
|
||||
left: left,
|
||||
top: top,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var CropZone = fabric.util.createClass(fabric.Rect, {
|
||||
_render: function(ctx) {
|
||||
this.callSuper('_render', ctx);
|
||||
var dashWidth = 7;
|
||||
|
||||
// Set original scale
|
||||
var flipX = this.flipX ? -1 : 1;
|
||||
var flipY = this.flipY ? -1 : 1;
|
||||
var scaleX = flipX / this.scaleX;
|
||||
var scaleY = flipY / this.scaleY;
|
||||
ctx.scale(scaleX, scaleY);
|
||||
|
||||
// Overlay rendering
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
this._renderOverlay(ctx);
|
||||
|
||||
// Set dashed borders
|
||||
if (typeof ctx.setLineDash !== 'undefined') {
|
||||
ctx.setLineDash([dashWidth, dashWidth]);
|
||||
} else if (typeof ctx.mozDash !== 'undefined') {
|
||||
ctx.mozDash = [dashWidth, dashWidth];
|
||||
}
|
||||
|
||||
// First lines rendering with black
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.2)';
|
||||
this._renderBorders(ctx);
|
||||
this._renderGrid(ctx);
|
||||
|
||||
// Re render lines in white
|
||||
ctx.lineDashOffset = dashWidth;
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
|
||||
this._renderBorders(ctx);
|
||||
this._renderGrid(ctx);
|
||||
|
||||
// Reset scale
|
||||
ctx.scale(1/scaleX, 1/scaleY);
|
||||
},
|
||||
|
||||
_renderOverlay: function(ctx) {
|
||||
var canvas = ctx.canvas;
|
||||
var borderOffset = 0;
|
||||
|
||||
//
|
||||
// x0 x1 x2 x3
|
||||
// y0 +------------------------+
|
||||
// |\\\\\\\\\\\\\\\\\\\\\\\\|
|
||||
// |\\\\\\\\\\\\\\\\\\\\\\\\|
|
||||
// y1 +------+---------+-------+
|
||||
// |\\\\\\| |\\\\\\\|
|
||||
// |\\\\\\| 0 |\\\\\\\|
|
||||
// |\\\\\\| |\\\\\\\|
|
||||
// y2 +------+---------+-------+
|
||||
// |\\\\\\\\\\\\\\\\\\\\\\\\|
|
||||
// |\\\\\\\\\\\\\\\\\\\\\\\\|
|
||||
// y3 +------------------------+
|
||||
//
|
||||
|
||||
var x0 = Math.ceil(-this.getWidth() / 2 - this.getLeft());
|
||||
var x1 = Math.ceil(-this.getWidth() / 2);
|
||||
var x2 = Math.ceil(this.getWidth() / 2);
|
||||
var x3 = Math.ceil(this.getWidth() / 2 + (canvas.width - this.getWidth() - this.getLeft()));
|
||||
|
||||
var y0 = Math.ceil(-this.getHeight() / 2 - this.getTop());
|
||||
var y1 = Math.ceil(-this.getHeight() / 2);
|
||||
var y2 = Math.ceil(this.getHeight() / 2);
|
||||
var y3 = Math.ceil(this.getHeight() / 2 + (canvas.height - this.getHeight() - this.getTop()));
|
||||
|
||||
// Upper rect
|
||||
ctx.fillRect(x0, y0, x3 - x0, y1 - y0 + borderOffset);
|
||||
|
||||
// Left rect
|
||||
ctx.fillRect(x0, y1, x1 - x0, y2 - y1 + borderOffset);
|
||||
|
||||
// Right rect
|
||||
ctx.fillRect(x2, y1, x3 - x2, y2 - y1 + borderOffset);
|
||||
|
||||
// Down rect
|
||||
ctx.fillRect(x0, y2, x3 - x0, y3 - y2);
|
||||
},
|
||||
|
||||
_renderBorders: function(ctx) {
|
||||
ctx.beginPath();
|
||||
// upper left
|
||||
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2);
|
||||
// upper right
|
||||
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2);
|
||||
// down right
|
||||
ctx.lineTo(this.getWidth()/2, this.getHeight()/2);
|
||||
// down left
|
||||
ctx.lineTo(-this.getWidth()/2, this.getHeight()/2);
|
||||
// upper left
|
||||
ctx.lineTo(-this.getWidth()/2, -this.getHeight()/2);
|
||||
ctx.stroke();
|
||||
},
|
||||
|
||||
_renderGrid: function(ctx) {
|
||||
// Vertical lines
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.getWidth()/2 + 1/3 * this.getWidth(), -this.getHeight()/2);
|
||||
ctx.lineTo(-this.getWidth()/2 + 1/3 * this.getWidth(), this.getHeight()/2);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.getWidth()/2 + 2/3 * this.getWidth(), -this.getHeight()/2);
|
||||
ctx.lineTo(-this.getWidth()/2 + 2/3 * this.getWidth(), this.getHeight()/2);
|
||||
ctx.stroke();
|
||||
|
||||
// Horizontal lines
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight());
|
||||
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 1/3 * this.getHeight());
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight());
|
||||
ctx.lineTo(this.getWidth()/2, -this.getHeight()/2 + 2/3 * this.getHeight());
|
||||
ctx.stroke();
|
||||
},
|
||||
});
|
||||
|
||||
Darkroom.plugins.crop = Darkroom.Plugin.extend({
|
||||
// Init point
|
||||
startX: null,
|
||||
startY: null,
|
||||
|
||||
// Keycrop
|
||||
isKeyCroping: false,
|
||||
isKeyLeft: false,
|
||||
isKeyUp: false,
|
||||
|
||||
defaults: {
|
||||
// Min crop dimensions
|
||||
minHeight: 1,
|
||||
minWidth: 1,
|
||||
// Ensure crop ratio
|
||||
ratio: null,
|
||||
// Quick crop feature (set a key code to enable it)
|
||||
quickCropKey: false,
|
||||
},
|
||||
|
||||
initialize: function InitDarkroomCropPlugin() {
|
||||
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
|
||||
|
||||
this.cropButton = buttonGroup.createButton({
|
||||
image: 'fa fa-crop',
|
||||
editOnly: true,
|
||||
});
|
||||
this.okButton = buttonGroup.createButton({
|
||||
image: 'fa fa-check',
|
||||
editOnly: true,
|
||||
type: 'success',
|
||||
hide: true
|
||||
});
|
||||
this.cancelButton = buttonGroup.createButton({
|
||||
image: 'fa fa-times',
|
||||
editOnly: true,
|
||||
type: 'danger',
|
||||
hide: true
|
||||
});
|
||||
|
||||
// Button click events
|
||||
this.cropButton.addEventListener('click', this.toggleCrop.bind(this));
|
||||
this.okButton.addEventListener('click', this.cropCurrentZone.bind(this));
|
||||
this.cancelButton.addEventListener('click', this.releaseFocus.bind(this));
|
||||
|
||||
// Canvas events
|
||||
this.darkroom.canvas.on('mouse:down', this.onMouseDown.bind(this));
|
||||
this.darkroom.canvas.on('mouse:move', this.onMouseMove.bind(this));
|
||||
this.darkroom.canvas.on('mouse:up', this.onMouseUp.bind(this));
|
||||
this.darkroom.canvas.on('object:moving', this.onObjectMoving.bind(this));
|
||||
this.darkroom.canvas.on('object:scaling', this.onObjectScaling.bind(this));
|
||||
|
||||
fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this));
|
||||
fabric.util.addListener(fabric.document, 'keyup', this.onKeyUp.bind(this));
|
||||
|
||||
this.darkroom.addEventListener('core:transformation', this.releaseFocus.bind(this));
|
||||
},
|
||||
|
||||
// Avoid crop zone to go beyond the canvas edges
|
||||
onObjectMoving: function(event) {
|
||||
if (!this.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentObject = event.target;
|
||||
if (currentObject !== this.cropZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
var x = currentObject.getLeft(), y = currentObject.getTop();
|
||||
var w = currentObject.getWidth(), h = currentObject.getHeight();
|
||||
var maxX = canvas.getWidth() - w;
|
||||
var maxY = canvas.getHeight() - h;
|
||||
|
||||
if (x < 0) {
|
||||
currentObject.set('left', 0);
|
||||
}
|
||||
if (y < 0) {
|
||||
currentObject.set('top', 0);
|
||||
}
|
||||
if (x > maxX) {
|
||||
currentObject.set('left', maxX);
|
||||
}
|
||||
if (y > maxY) {
|
||||
currentObject.set('top', maxY);
|
||||
}
|
||||
|
||||
this.darkroom.dispatchEvent('crop:update');
|
||||
},
|
||||
|
||||
// Prevent crop zone from going beyond the canvas edges (like mouseMove)
|
||||
onObjectScaling: function(event) {
|
||||
if (!this.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var preventScaling = false;
|
||||
var currentObject = event.target;
|
||||
if (currentObject !== this.cropZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
|
||||
var minX = currentObject.getLeft();
|
||||
var minY = currentObject.getTop();
|
||||
var maxX = currentObject.getLeft() + currentObject.getWidth();
|
||||
var maxY = currentObject.getTop() + currentObject.getHeight();
|
||||
|
||||
if (this.options.ratio !== null) {
|
||||
if (minX < 0 || maxX > canvas.getWidth() || minY < 0 || maxY > canvas.getHeight()) {
|
||||
preventScaling = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (minX < 0 || maxX > canvas.getWidth() || preventScaling) {
|
||||
var lastScaleX = this.lastScaleX || 1;
|
||||
currentObject.setScaleX(lastScaleX);
|
||||
}
|
||||
if (minX < 0) {
|
||||
currentObject.setLeft(0);
|
||||
}
|
||||
|
||||
if (minY < 0 || maxY > canvas.getHeight() || preventScaling) {
|
||||
var lastScaleY = this.lastScaleY || 1;
|
||||
currentObject.setScaleY(lastScaleY);
|
||||
}
|
||||
if (minY < 0) {
|
||||
currentObject.setTop(0);
|
||||
}
|
||||
|
||||
if (currentObject.getWidth() < this.options.minWidth) {
|
||||
currentObject.scaleToWidth(this.options.minWidth);
|
||||
}
|
||||
if (currentObject.getHeight() < this.options.minHeight) {
|
||||
currentObject.scaleToHeight(this.options.minHeight);
|
||||
}
|
||||
|
||||
this.lastScaleX = currentObject.getScaleX();
|
||||
this.lastScaleY = currentObject.getScaleY();
|
||||
|
||||
this.darkroom.dispatchEvent('crop:update');
|
||||
},
|
||||
|
||||
// Init crop zone
|
||||
onMouseDown: function(event) {
|
||||
if (!this.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
|
||||
// Recalculate offset, in case canvas was manipulated since last `calcOffset`
|
||||
canvas.calcOffset();
|
||||
var pointer = canvas.getPointer(event.e);
|
||||
var x = pointer.x;
|
||||
var y = pointer.y;
|
||||
var point = new fabric.Point(x, y);
|
||||
|
||||
// Check if user want to scale or drag the crop zone.
|
||||
var activeObject = canvas.getActiveObject();
|
||||
if (activeObject === this.cropZone || this.cropZone.containsPoint(point)) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.discardActiveObject();
|
||||
this.cropZone.setWidth(0);
|
||||
this.cropZone.setHeight(0);
|
||||
this.cropZone.setScaleX(1);
|
||||
this.cropZone.setScaleY(1);
|
||||
|
||||
this.startX = x;
|
||||
this.startY = y;
|
||||
},
|
||||
|
||||
// Extend crop zone
|
||||
onMouseMove: function(event) {
|
||||
// Quick crop feature
|
||||
if (this.isKeyCroping) {
|
||||
return this.onMouseMoveKeyCrop(event);
|
||||
}
|
||||
if (this.startX === null || this.startY === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
var pointer = canvas.getPointer(event.e);
|
||||
var x = pointer.x;
|
||||
var y = pointer.y;
|
||||
|
||||
this._renderCropZone(this.startX, this.startY, x, y);
|
||||
},
|
||||
|
||||
onMouseMoveKeyCrop: function(event) {
|
||||
var canvas = this.darkroom.canvas;
|
||||
var zone = this.cropZone;
|
||||
|
||||
var pointer = canvas.getPointer(event.e);
|
||||
var x = pointer.x;
|
||||
var y = pointer.y;
|
||||
|
||||
if (!zone.left || !zone.top) {
|
||||
zone.setTop(y);
|
||||
zone.setLeft(x);
|
||||
}
|
||||
|
||||
this.isKeyLeft = x < zone.left + zone.width / 2;
|
||||
this.isKeyUp = y < zone.top + zone.height / 2;
|
||||
|
||||
this._renderCropZone(
|
||||
Math.min(zone.left, x),
|
||||
Math.min(zone.top, y),
|
||||
Math.max(zone.left+zone.width, x),
|
||||
Math.max(zone.top+zone.height, y)
|
||||
);
|
||||
},
|
||||
|
||||
// Finish crop zone
|
||||
onMouseUp: function() {
|
||||
if (this.startX === null || this.startY === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
this.cropZone.setCoords();
|
||||
canvas.setActiveObject(this.cropZone);
|
||||
canvas.calcOffset();
|
||||
|
||||
this.startX = null;
|
||||
this.startY = null;
|
||||
},
|
||||
|
||||
onKeyDown: function(event) {
|
||||
if (this.options.quickCropKey === false || event.keyCode !== this.options.quickCropKey || this.isKeyCroping) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Active quick crop flow
|
||||
this.isKeyCroping = true ;
|
||||
this.darkroom.canvas.discardActiveObject();
|
||||
this.cropZone.setWidth(0);
|
||||
this.cropZone.setHeight(0);
|
||||
this.cropZone.setScaleX(1);
|
||||
this.cropZone.setScaleY(1);
|
||||
this.cropZone.setTop(0);
|
||||
this.cropZone.setLeft(0);
|
||||
},
|
||||
|
||||
onKeyUp: function(event) {
|
||||
if (this.options.quickCropKey === false || event.keyCode !== this.options.quickCropKey || !this.isKeyCroping) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Inactive quick crop flow
|
||||
this.isKeyCroping = false;
|
||||
this.startX = 1;
|
||||
this.startY = 1;
|
||||
this.onMouseUp();
|
||||
},
|
||||
|
||||
selectZone: function(x, y, width, height, forceDimension) {
|
||||
if (!this.hasFocus()) {
|
||||
this.requireFocus();
|
||||
}
|
||||
|
||||
if (forceDimension) {
|
||||
this.cropZone.set({
|
||||
'left': x,
|
||||
'top': y,
|
||||
'width': width,
|
||||
'height': height,
|
||||
});
|
||||
} else {
|
||||
this._renderCropZone(x, y, x+width, y+height);
|
||||
}
|
||||
|
||||
var canvas = this.darkroom.canvas;
|
||||
canvas.bringToFront(this.cropZone);
|
||||
this.cropZone.setCoords();
|
||||
canvas.setActiveObject(this.cropZone);
|
||||
canvas.calcOffset();
|
||||
|
||||
this.darkroom.dispatchEvent('crop:update');
|
||||
},
|
||||
|
||||
toggleCrop: function() {
|
||||
if (this.hasFocus()) {
|
||||
this.releaseFocus();
|
||||
} else {
|
||||
this.requireFocus();
|
||||
}
|
||||
},
|
||||
|
||||
cropCurrentZone: function() {
|
||||
if (!this.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid croping empty zone
|
||||
if (this.cropZone.width < 1 && this.cropZone.height < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var image = this.darkroom.image;
|
||||
|
||||
// Compute crop zone dimensions
|
||||
var top = this.cropZone.getTop() - image.getTop();
|
||||
var left = this.cropZone.getLeft() - image.getLeft();
|
||||
var width = this.cropZone.getWidth();
|
||||
var height = this.cropZone.getHeight();
|
||||
|
||||
// Adjust dimensions to image only
|
||||
if (top < 0) {
|
||||
height += top;
|
||||
top = 0;
|
||||
}
|
||||
|
||||
if (left < 0) {
|
||||
width += left;
|
||||
left = 0;
|
||||
}
|
||||
|
||||
// Apply crop transformation. Make sure to use relative
|
||||
// dimension since the crop will be applied on the source image.
|
||||
this.darkroom.applyTransformation(new Crop({
|
||||
top: top / image.getHeight(),
|
||||
left: left / image.getWidth(),
|
||||
width: width / image.getWidth(),
|
||||
height: height / image.getHeight(),
|
||||
}));
|
||||
},
|
||||
|
||||
// Test whether crop zone is set
|
||||
hasFocus: function() {
|
||||
return typeof this.cropZone !== 'undefined';
|
||||
},
|
||||
|
||||
// Create the crop zone
|
||||
requireFocus: function() {
|
||||
this.cropZone = new CropZone({
|
||||
fill: 'transparent',
|
||||
hasBorders: false,
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
cornerColor: '#444',
|
||||
cornerSize: 8,
|
||||
transparentCorners: false,
|
||||
lockRotation: true,
|
||||
hasRotatingPoint: false,
|
||||
});
|
||||
|
||||
if (this.options.ratio !== null) {
|
||||
this.cropZone.set('lockUniScaling', true);
|
||||
}
|
||||
|
||||
this.darkroom.canvas.add(this.cropZone);
|
||||
this.darkroom.canvas.defaultCursor = 'crosshair';
|
||||
|
||||
this.cropButton.active(true);
|
||||
this.okButton.hide(false);
|
||||
this.cancelButton.hide(false);
|
||||
},
|
||||
|
||||
// Remove the crop zone
|
||||
releaseFocus: function() {
|
||||
if (typeof this.cropZone === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cropZone.remove();
|
||||
this.cropZone = undefined;
|
||||
|
||||
this.cropButton.active(false);
|
||||
this.okButton.hide(true);
|
||||
this.cancelButton.hide(true);
|
||||
|
||||
this.darkroom.canvas.defaultCursor = 'default';
|
||||
this.darkroom.dispatchEvent('crop:update');
|
||||
},
|
||||
|
||||
_renderCropZone: function(fromX, fromY, toX, toY) {
|
||||
var canvas = this.darkroom.canvas;
|
||||
|
||||
var isRight = toX > fromX;
|
||||
var isLeft = !isRight;
|
||||
var isDown = toY > fromY;
|
||||
var isUp = !isDown;
|
||||
|
||||
var minWidth = Math.min(Number(this.options.minWidth), canvas.getWidth());
|
||||
var minHeight = Math.min(Number(this.options.minHeight), canvas.getHeight());
|
||||
|
||||
// Define corner coordinates
|
||||
var leftX = Math.min(fromX, toX);
|
||||
var rightX = Math.max(fromX, toX);
|
||||
var topY = Math.min(fromY, toY);
|
||||
var bottomY = Math.max(fromY, toY);
|
||||
|
||||
// Replace current point into the canvas
|
||||
leftX = Math.max(0, leftX);
|
||||
rightX = Math.min(canvas.getWidth(), rightX);
|
||||
topY = Math.max(0, topY);
|
||||
bottomY = Math.min(canvas.getHeight(), bottomY);
|
||||
|
||||
// Recalibrate coordinates according to given options
|
||||
if (rightX - leftX < minWidth) {
|
||||
if (isRight) {
|
||||
rightX = leftX + minWidth;
|
||||
} else {
|
||||
leftX = rightX - minWidth;
|
||||
}
|
||||
}
|
||||
if (bottomY - topY < minHeight) {
|
||||
if (isDown) {
|
||||
bottomY = topY + minHeight;
|
||||
} else {
|
||||
topY = bottomY - minHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate truncate according to canvas dimensions
|
||||
if (leftX < 0) {
|
||||
// Translate to the left
|
||||
rightX += Math.abs(leftX);
|
||||
leftX = 0;
|
||||
}
|
||||
if (rightX > canvas.getWidth()) {
|
||||
// Translate to the right
|
||||
leftX -= rightX - canvas.getWidth();
|
||||
rightX = canvas.getWidth();
|
||||
}
|
||||
if (topY < 0) {
|
||||
// Translate to the bottom
|
||||
bottomY += Math.abs(topY);
|
||||
topY = 0;
|
||||
}
|
||||
if (bottomY > canvas.getHeight()) {
|
||||
// Translate to the right
|
||||
topY -= bottomY - canvas.getHeight();
|
||||
bottomY = canvas.getHeight();
|
||||
}
|
||||
|
||||
var width = rightX - leftX;
|
||||
var height = bottomY - topY;
|
||||
var currentRatio = width / height;
|
||||
|
||||
if (this.options.ratio && Number(this.options.ratio) !== currentRatio) {
|
||||
var ratio = Number(this.options.ratio);
|
||||
var newWidth = 0, newHeight = 0;
|
||||
|
||||
if(this.isKeyCroping) {
|
||||
isLeft = this.isKeyLeft;
|
||||
isUp = this.isKeyUp;
|
||||
}
|
||||
|
||||
if (currentRatio < ratio) {
|
||||
newWidth = height * ratio;
|
||||
if (isLeft) {
|
||||
leftX -= newWidth - width;
|
||||
}
|
||||
width = newWidth;
|
||||
} else if (currentRatio > ratio) {
|
||||
newHeight = height / (ratio * height/width);
|
||||
if (isUp) {
|
||||
topY -= newHeight - height;
|
||||
}
|
||||
height = newHeight;
|
||||
}
|
||||
|
||||
if (leftX < 0) {
|
||||
leftX = 0;
|
||||
//TODO
|
||||
}
|
||||
if (topY < 0) {
|
||||
topY = 0;
|
||||
//TODO
|
||||
}
|
||||
if (leftX + width > canvas.getWidth()) {
|
||||
newWidth = canvas.getWidth() - leftX;
|
||||
height = newWidth * height / width;
|
||||
width = newWidth;
|
||||
if (isUp) {
|
||||
topY = fromY - height;
|
||||
}
|
||||
}
|
||||
if (topY + height > canvas.getHeight()) {
|
||||
newHeight = canvas.getHeight() - topY;
|
||||
width = width * newHeight / height;
|
||||
height = newHeight;
|
||||
if (isLeft) {
|
||||
leftX = fromX - width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply coordinates
|
||||
this.cropZone.left = leftX;
|
||||
this.cropZone.top = topY;
|
||||
this.cropZone.width = width;
|
||||
this.cropZone.height = height;
|
||||
|
||||
this.darkroom.canvas.bringToFront(this.cropZone);
|
||||
this.darkroom.dispatchEvent('crop:update');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {DarkroomPluginCrop: DarkroomPluginCrop};
|
||||
});
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_history', function() {
|
||||
'use strict';
|
||||
|
||||
var DarkroomPluginHistory = function() {
|
||||
Darkroom.plugins.history = Darkroom.Plugin.extend({
|
||||
undoTransformations: [],
|
||||
|
||||
initialize: function InitDarkroomHistoryPlugin() {
|
||||
this._initButtons();
|
||||
this.darkroom.addEventListener('core:transformation', this._onTranformationApplied.bind(this));
|
||||
},
|
||||
|
||||
undo: function() {
|
||||
if (this.darkroom.transformations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastTransformation = this.darkroom.transformations.pop();
|
||||
this.undoTransformations.unshift(lastTransformation);
|
||||
|
||||
this.darkroom.reinitializeImage();
|
||||
this._updateButtons();
|
||||
},
|
||||
|
||||
redo: function() {
|
||||
if (this.undoTransformations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cancelTransformation = this.undoTransformations.shift();
|
||||
this.darkroom.transformations.push(cancelTransformation);
|
||||
|
||||
this.darkroom.reinitializeImage();
|
||||
this._updateButtons();
|
||||
},
|
||||
|
||||
_initButtons: function() {
|
||||
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
|
||||
|
||||
this.backButton = buttonGroup.createButton({
|
||||
image: 'fa fa-step-backward',
|
||||
disabled: true,
|
||||
editOnly: true,
|
||||
});
|
||||
this.forwardButton = buttonGroup.createButton({
|
||||
image: 'fa fa-step-forward',
|
||||
disabled: true,
|
||||
editOnly: true,
|
||||
});
|
||||
|
||||
this.backButton.addEventListener('click', this.undo.bind(this));
|
||||
this.forwardButton.addEventListener('click', this.redo.bind(this));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_updateButtons: function() {
|
||||
this.backButton.disable(this.darkroom.transformations.length === 0);
|
||||
this.forwardButton.disable(this.undoTransformations.length === 0);
|
||||
},
|
||||
|
||||
_onTranformationApplied: function() {
|
||||
this.undoTransformations = [];
|
||||
this._updateButtons();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {DarkroomPluginHistory: DarkroomPluginHistory};
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_rotate', function() {
|
||||
'use strict';
|
||||
|
||||
var DarkroomPluginRotate = function() {
|
||||
var Rotation = Darkroom.Transformation.extend({
|
||||
applyTransformation: function(canvas, image, next) {
|
||||
var angle = (image.getAngle() + this.options.angle) % 360;
|
||||
image.rotate(angle);
|
||||
|
||||
var height = Math.abs(image.getWidth()*Math.sin(angle*Math.PI/180))+Math.abs(image.getHeight()*Math.cos(angle*Math.PI/180));
|
||||
var width = Math.abs(image.getHeight()*Math.sin(angle*Math.PI/180))+Math.abs(image.getWidth()*Math.cos(angle*Math.PI/180));
|
||||
|
||||
canvas.setWidth(width);
|
||||
canvas.setHeight(height);
|
||||
|
||||
canvas.centerObject(image);
|
||||
image.setCoords();
|
||||
canvas.renderAll();
|
||||
|
||||
next();
|
||||
},
|
||||
});
|
||||
|
||||
Darkroom.plugins.rotate = Darkroom.Plugin.extend({
|
||||
initialize: function InitDarkroomRotatePlugin() {
|
||||
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
|
||||
|
||||
var leftButton = buttonGroup.createButton({
|
||||
image: 'fa fa-undo oe_edit_only',
|
||||
editOnly: true,
|
||||
});
|
||||
var rightButton = buttonGroup.createButton({
|
||||
image: 'fa fa-repeat oe_edit_only',
|
||||
editOnly: true,
|
||||
});
|
||||
|
||||
leftButton.addEventListener('click', this.rotateLeft.bind(this));
|
||||
rightButton.addEventListener('click', this.rotateRight.bind(this));
|
||||
},
|
||||
|
||||
rotateLeft: function rotateLeft() {
|
||||
this.rotate(-90);
|
||||
},
|
||||
|
||||
rotateRight: function rotateRight() {
|
||||
this.rotate(90);
|
||||
},
|
||||
|
||||
rotate: function rotate(angle) {
|
||||
this.darkroom.applyTransformation(
|
||||
new Rotation({angle: angle})
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {DarkroomPluginRotate: DarkroomPluginRotate};
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_zoom', function() {
|
||||
'use strict';
|
||||
|
||||
var DarkroomPluginZoom = function() {
|
||||
Darkroom.plugins.zoom = Darkroom.Plugin.extend({
|
||||
inZoom: false,
|
||||
zoomLevel: 0,
|
||||
zoomFactor: 0.1,
|
||||
|
||||
initialize: function() {
|
||||
var self = this;
|
||||
var buttonGroup = this.darkroom.toolbar.createButtonGroup();
|
||||
|
||||
this.zoomButton = buttonGroup.createButton({
|
||||
image: 'fa fa-search',
|
||||
});
|
||||
this.zoomInButton = buttonGroup.createButton({
|
||||
image: 'fa fa-plus',
|
||||
});
|
||||
this.zoomOutButton = buttonGroup.createButton({
|
||||
image: 'fa fa-minus',
|
||||
});
|
||||
this.cancelButton = buttonGroup.createButton({
|
||||
image: 'fa fa-times',
|
||||
type: 'danger',
|
||||
hide: true
|
||||
});
|
||||
|
||||
// Button click events
|
||||
this.zoomButton.addEventListener('click', this.toggleZoom.bind(this));
|
||||
this.zoomInButton.addEventListener('click', this.zoomIn.bind(this));
|
||||
this.zoomOutButton.addEventListener('click', this.zoomOut.bind(this));
|
||||
this.cancelButton.addEventListener('click', this.releaseFocus.bind(this));
|
||||
|
||||
// Canvas events
|
||||
this.darkroom.canvas.on('mouse:down', this.onMouseDown.bind(this));
|
||||
this.darkroom.canvas.on('mouse:move', this.onMouseMove.bind(this));
|
||||
this.darkroom.canvas.on('mouse:up', this.onMouseUp.bind(this));
|
||||
$(this.darkroom.canvas.wrapperEl).on('mousewheel', function(event){
|
||||
self.onMouseWheel(event);
|
||||
});
|
||||
|
||||
this.toggleElements(false);
|
||||
},
|
||||
|
||||
toggleZoom: function() {
|
||||
if (this.hasFocus()) {
|
||||
this.releaseFocus();
|
||||
} else {
|
||||
this.requireFocus();
|
||||
}
|
||||
},
|
||||
|
||||
hasFocus: function() {
|
||||
return this.inZoom;
|
||||
},
|
||||
|
||||
releaseFocus: function() {
|
||||
this.toggleElements(false);
|
||||
},
|
||||
|
||||
requireFocus: function() {
|
||||
this.toggleElements(true);
|
||||
},
|
||||
|
||||
toggleElements: function(bool) {
|
||||
var toggle = bool;
|
||||
if (typeof bool === 'undefined') {
|
||||
toggle = !this.hasFocus();
|
||||
}
|
||||
|
||||
this.zoomButton.active(toggle);
|
||||
this.inZoom = toggle;
|
||||
this.zoomInButton.hide(!toggle);
|
||||
this.zoomOutButton.hide(!toggle);
|
||||
this.cancelButton.hide(!toggle);
|
||||
this.darkroom.canvas.default_cursor = toggle ? 'move' : 'default';
|
||||
},
|
||||
|
||||
zoomIn: function() {
|
||||
return this.setZoomLevel(this.zoomFactor, this.getCenterPoint());
|
||||
},
|
||||
|
||||
zoomOut: function() {
|
||||
return this.setZoomLevel(-this.zoomFactor, this.getCenterPoint());
|
||||
},
|
||||
|
||||
// Return fabric.Point object for center of canvas
|
||||
getCenterPoint: function() {
|
||||
var center = this.darkroom.canvas.getCenter();
|
||||
return new fabric.Point(center.left, center.top);
|
||||
},
|
||||
|
||||
// Set internal zoom
|
||||
setZoomLevel: function(factor, point) {
|
||||
var zoomLevel = this.zoomLevel + factor;
|
||||
if (zoomLevel < 0) {
|
||||
zoomLevel = 0;
|
||||
}
|
||||
if (zoomLevel === this.zoomLevel) {
|
||||
return false;
|
||||
}
|
||||
if (point) {
|
||||
var canvas = this.darkroom.canvas;
|
||||
// Add one for zero index
|
||||
canvas.zoomToPoint(point, zoomLevel + 1);
|
||||
this.zoomLevel = zoomLevel;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
onMouseWheel: function(event) {
|
||||
if (this.hasFocus() && event && event.originalEvent) {
|
||||
var modifier = event.originalEvent.wheelDelta < 0 ? -1 : 1;
|
||||
var pointer = this.darkroom.canvas.getPointer(event.originalEvent);
|
||||
var mousePoint = new fabric.Point(pointer.x, pointer.y);
|
||||
this.setZoomLevel(modifier * this.zoomFactor, mousePoint);
|
||||
return event.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown: function() {
|
||||
if (this.hasFocus()) {
|
||||
this.panning = true;
|
||||
}
|
||||
},
|
||||
|
||||
onMouseUp: function() {
|
||||
this.panning = false;
|
||||
},
|
||||
|
||||
onMouseMove: function(event) {
|
||||
if (this.panning && event && event.e) {
|
||||
var delta = new fabric.Point(event.e.movementX, event.e.movementY);
|
||||
this.darkroom.canvas.relativePan(delta);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {DarkroomPluginZoom: DarkroomPluginZoom};
|
||||
});
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* Copyright 2013 Matthieu Moquet
|
||||
* Copyright 2016-2017 LasLabs Inc.
|
||||
* License MIT (https://opensource.org/licenses/MIT)
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_widget', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var common = require('web.form_common');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
var _ = require('_');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
var FieldDarkroomImage = common.AbstractField.extend(common.ReinitializeFieldMixin, {
|
||||
className: 'darkroom-widget',
|
||||
template: 'FieldDarkroomImage',
|
||||
placeholder: "/web/static/src/img/placeholder.png",
|
||||
darkroom: null,
|
||||
no_rerender: false,
|
||||
|
||||
defaults: {
|
||||
// Canvas initialization size
|
||||
minWidth: 100,
|
||||
minHeight: 100,
|
||||
maxWidth: 700,
|
||||
maxHeight: 500,
|
||||
|
||||
// Plugin options
|
||||
plugins: {
|
||||
crop: {
|
||||
minHeight: 50,
|
||||
minWidth: 50,
|
||||
ratio: 1
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
init: function(field_manager, node) {
|
||||
this._super(field_manager, node);
|
||||
this.options = _.defaults(this.options, this.defaults);
|
||||
},
|
||||
|
||||
_init_darkroom: function() {
|
||||
if (!this.darkroom) {
|
||||
this._init_darkroom_icons();
|
||||
this._init_darkroom_ui();
|
||||
this._init_darkroom_plugins();
|
||||
}
|
||||
},
|
||||
|
||||
_init_darkroom_icons: function() {
|
||||
var element = document.createElement('div');
|
||||
element.id = 'darkroom-icons';
|
||||
element.style.height = 0;
|
||||
element.style.width = 0;
|
||||
element.style.position = 'absolute';
|
||||
element.style.visibility = 'hidden';
|
||||
element.innerHTML = '<!-- inject:svg --><!-- endinject -->';
|
||||
this.el.appendChild(element);
|
||||
},
|
||||
|
||||
_init_darkroom_plugins: function() {
|
||||
require('web_widget_darkroom.darkroom_crop').DarkroomPluginCrop();
|
||||
require('web_widget_darkroom.darkroom_history').DarkroomPluginHistory();
|
||||
require('web_widget_darkroom.darkroom_rotate').DarkroomPluginRotate();
|
||||
require('web_widget_darkroom.darkroom_zoom').DarkroomPluginZoom();
|
||||
},
|
||||
|
||||
_init_darkroom_ui: function() {
|
||||
// Button object
|
||||
function Button(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
Button.prototype = {
|
||||
addEventListener: function(eventName, listener) {
|
||||
if (this.element.addEventListener) {
|
||||
this.element.addEventListener(eventName, listener);
|
||||
} else if (this.element.attachEvent) {
|
||||
this.element.attachEvent('on' + eventName, listener);
|
||||
}
|
||||
},
|
||||
removeEventListener: function(eventName, listener) {
|
||||
if (this.element.removeEventListener) {
|
||||
this.element.removeEventListener(eventName, listener);
|
||||
} else if (this.element.detachEvent) {
|
||||
this.element.detachEvent('on' + eventName, listener);
|
||||
}
|
||||
},
|
||||
active: function(bool) {
|
||||
if (bool) {
|
||||
this.element.classList.add('darkroom-button-active');
|
||||
} else {
|
||||
this.element.classList.remove('darkroom-button-active');
|
||||
}
|
||||
},
|
||||
hide: function(bool) {
|
||||
if (bool) {
|
||||
this.element.classList.add('hidden');
|
||||
} else {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
},
|
||||
disable: function(bool) {
|
||||
this.element.disabled = bool;
|
||||
},
|
||||
};
|
||||
|
||||
// ButtonGroup object
|
||||
function ButtonGroup(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
ButtonGroup.prototype = {
|
||||
createButton: function(options) {
|
||||
var defaults = {
|
||||
image: 'fa fa-question-circle',
|
||||
type: 'default',
|
||||
group: 'default',
|
||||
hide: false,
|
||||
disabled: false,
|
||||
editOnly: false,
|
||||
addClass: '',
|
||||
};
|
||||
var optionsMerged = Darkroom.Utils.extend(options, defaults);
|
||||
|
||||
var buttonElement = document.createElement('button');
|
||||
buttonElement.type = 'button';
|
||||
buttonElement.className = 'darkroom-button darkroom-button-' + optionsMerged.type;
|
||||
buttonElement.innerHTML = '<i class="' + optionsMerged.image + ' fa-2x"></i>';
|
||||
if (optionsMerged.editOnly) {
|
||||
buttonElement.classList.add('oe_edit_only');
|
||||
}
|
||||
if (optionsMerged.addClass) {
|
||||
buttonElement.classList.add(optionsMerged.addClass);
|
||||
}
|
||||
this.element.appendChild(buttonElement);
|
||||
|
||||
var button = new Button(buttonElement);
|
||||
button.hide(optionsMerged.hide);
|
||||
button.disable(optionsMerged.disabled);
|
||||
|
||||
return button;
|
||||
}
|
||||
};
|
||||
|
||||
// Toolbar object
|
||||
function Toolbar(element) {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
Toolbar.prototype = {
|
||||
createButtonGroup: function() {
|
||||
var buttonGroupElement = document.createElement('div');
|
||||
buttonGroupElement.className = 'darkroom-button-group';
|
||||
this.element.appendChild(buttonGroupElement);
|
||||
|
||||
return new ButtonGroup(buttonGroupElement);
|
||||
}
|
||||
};
|
||||
|
||||
Darkroom.UI = {
|
||||
Toolbar: Toolbar,
|
||||
ButtonGroup: ButtonGroup,
|
||||
Button: Button,
|
||||
};
|
||||
},
|
||||
|
||||
destroy_content: function() {
|
||||
if (this.darkroom && this.darkroom.containerElement) {
|
||||
this.darkroom.containerElement.remove();
|
||||
this.darkroom = null;
|
||||
}
|
||||
},
|
||||
|
||||
set_value: function(value) {
|
||||
return this._super(value);
|
||||
},
|
||||
|
||||
render_value: function() {
|
||||
this.destroy_content();
|
||||
this._init_darkroom();
|
||||
|
||||
var url = null;
|
||||
if (this.get('value') && !utils.is_bin_size(this.get('value'))) {
|
||||
url = 'data:image/png;base64,' + this.get('value');
|
||||
} else if (this.get('value')) {
|
||||
var id = JSON.stringify(this.view.datarecord.id || null);
|
||||
var field = this.name;
|
||||
if (this.options.preview_image) {
|
||||
field = this.options.preview_image;
|
||||
}
|
||||
url = session.url('/web/image', {
|
||||
model: this.view.dataset.model,
|
||||
id: id,
|
||||
field: field,
|
||||
unique: (this.view.datarecord.__last_update || '').replace(/[^0-9]/g, ''),
|
||||
});
|
||||
} else {
|
||||
url = this.placeholder;
|
||||
}
|
||||
|
||||
var $img = $(QWeb.render("FieldBinaryImage-img", {widget: this, url: url}));
|
||||
this.$el.find('> img').remove();
|
||||
this.$el.append($img);
|
||||
this.darkroom = new Darkroom($img.get(0), this.options);
|
||||
this.darkroom.widget = this;
|
||||
},
|
||||
|
||||
commit_value: function() {
|
||||
if (this.darkroom.sourceImage) {
|
||||
this.set_value(this.darkroom.sourceImage.toDataURL().split(',')[1]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
core.form_widget_registry.add("darkroom", FieldDarkroomImage);
|
||||
|
||||
return {FieldDarkroomImage: FieldDarkroomImage};
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Copyright 2017 LasLabs Inc.
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
**/
|
||||
|
||||
odoo.define('web_widget_darkroom.darkroom_modal_button', function(require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
var DataModel = require('web.DataModel');
|
||||
|
||||
core.form_widget_registry.get('image').include({
|
||||
// Used in template to prevent Darkroom buttons from being added to
|
||||
// forms for new records, which are not supported
|
||||
darkroom_supported: function() {
|
||||
if (this.field_manager.dataset.index === null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
render_value: function() {
|
||||
this._super();
|
||||
|
||||
var imageWidget = this;
|
||||
var activeModel = imageWidget.field_manager.dataset._model.name;
|
||||
var activeRecordId = imageWidget.field_manager.datarecord.id;
|
||||
var activeField = imageWidget.node.attrs.name;
|
||||
|
||||
var updateImage = function() {
|
||||
var ActiveModel = new DataModel(activeModel);
|
||||
ActiveModel.query([activeField]).
|
||||
filter([['id', '=', activeRecordId]]).
|
||||
all().
|
||||
then(function(result) {
|
||||
imageWidget.set_value(result[0].image);
|
||||
});
|
||||
};
|
||||
|
||||
var openModal = function() {
|
||||
var context = {
|
||||
active_model: activeModel,
|
||||
active_record_id: activeRecordId,
|
||||
active_field: activeField,
|
||||
};
|
||||
var modalAction = {
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'darkroom.modal',
|
||||
name: 'Darkroom',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: context,
|
||||
};
|
||||
var options = {on_close: updateImage};
|
||||
imageWidget.do_action(modalAction, options);
|
||||
};
|
||||
|
||||
var $button = this.$('.oe_form_binary_image_darkroom_modal');
|
||||
if ($button.length > 0) {
|
||||
$button.click(openModal);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
.darkroom-button-group {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.darkroom-button-active {
|
||||
color: @odoo-brand-primary;
|
||||
}
|
||||
|
||||
.oe_form_field_image_controls i {
|
||||
margin: 0 5%;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Copyright 2016-2017 LasLabs Inc.
|
||||
License LGPL-3 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
|
||||
<templates id="field_templates" xml:space="preserve">
|
||||
<t t-name="FieldDarkroomImage">
|
||||
<span class="oe_form_field o_form_field_darkroom" t-att-style="widget.node.attrs.style">
|
||||
<t t-if="!widget.get('effective_readonly')">
|
||||
<div class="darkroom-toolbar"/>
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<t t-extend="FieldBinaryImage">
|
||||
<t t-jquery=".oe_form_binary_file_edit" t-operation="after">
|
||||
<t t-if="widget.darkroom_supported()">
|
||||
<i class="fa fa-picture-o fa-lg oe_form_binary_image_darkroom_modal" title="Darkroom"></i>
|
||||
</t>
|
||||
</t>
|
||||
<t t-jquery=".oe_form_binary_file_edit" t-operation="replace">
|
||||
<i class="fa fa-pencil fa-lg oe_form_binary_file_edit" title="Edit"></i>
|
||||
</t>
|
||||
<t t-jquery=".oe_form_binary_file_clear" t-operation="replace">
|
||||
<i class="fa fa-trash-o fa-lg oe_form_binary_file_clear" title="Clear"></i>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from . import test_darkroom_modal
|
|
@ -0,0 +1,203 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from openerp.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestDarkroomModal(TransactionCase):
|
||||
|
||||
def test_default_res_model_id_model_in_context(self):
|
||||
"""Should return correct ir.model record when context has model name"""
|
||||
active_model = 'res.users'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
})
|
||||
test_result = test_model._default_res_model_id()
|
||||
|
||||
expected = self.env['ir.model'].search([('model', '=', active_model)])
|
||||
self.assertEqual(test_result, expected)
|
||||
|
||||
def test_default_res_model_id_no_valid_info_in_context(self):
|
||||
"""Should return empty ir.model recordset when missing/invalid info"""
|
||||
test_model = self.env['darkroom.modal'].with_context({})
|
||||
test_result = test_model._default_res_model_id()
|
||||
|
||||
self.assertEqual(test_result, self.env['ir.model'])
|
||||
|
||||
def test_default_res_record_id_id_in_context(self):
|
||||
"""Should return correct value when ID in context"""
|
||||
active_record_id = 5
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_record_id': active_record_id,
|
||||
})
|
||||
test_result = test_model._default_res_record_id()
|
||||
|
||||
self.assertEqual(test_result, active_record_id)
|
||||
|
||||
def test_default_res_record_id_no_id_in_context(self):
|
||||
"""Should return 0 when no ID in context"""
|
||||
test_model = self.env['darkroom.modal'].with_context({})
|
||||
test_result = test_model._default_res_record_id()
|
||||
|
||||
self.assertEqual(test_result, 0)
|
||||
|
||||
def test_default_res_record_model_and_id_in_context(self):
|
||||
"""Should return correct record when context has model name and ID"""
|
||||
active_model = 'res.users'
|
||||
active_record_id = 1
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_record_id': active_record_id,
|
||||
})
|
||||
test_result = test_model._default_res_record()
|
||||
|
||||
expected = self.env[active_model].browse(active_record_id)
|
||||
self.assertEqual(test_result, expected)
|
||||
|
||||
def test_default_res_record_model_but_no_id_in_context(self):
|
||||
"""Should return right empty recordset if model but no ID in context"""
|
||||
active_model = 'res.users'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
})
|
||||
test_result = test_model._default_res_record()
|
||||
|
||||
self.assertEqual(test_result, self.env[active_model])
|
||||
|
||||
def test_default_res_record_no_valid_model_info_in_context(self):
|
||||
"""Should return None if context has missing/invalid model info"""
|
||||
active_model = 'bad.model.name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
})
|
||||
test_result = test_model._default_res_record()
|
||||
|
||||
self.assertIsNone(test_result)
|
||||
|
||||
def test_default_res_field_id_model_and_field_in_context(self):
|
||||
"""Should return correct ir.model.fields record when info in context"""
|
||||
active_model = 'res.users'
|
||||
active_field = 'name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_res_field_id()
|
||||
|
||||
self.assertEqual(test_result.name, active_field)
|
||||
self.assertEqual(test_result.model_id.model, active_model)
|
||||
|
||||
def test_default_res_field_id_no_valid_field_in_context(self):
|
||||
"""Should return empty recordset if field info missing/invalid"""
|
||||
active_model = 'res.users'
|
||||
active_field = 'totally.not.a.real.field.name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_res_field_id()
|
||||
|
||||
self.assertEqual(test_result, self.env['ir.model.fields'])
|
||||
|
||||
def test_default_res_field_id_no_valid_model_in_context(self):
|
||||
"""Should return empty recordset if model info missing/invalid"""
|
||||
active_field = 'name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_res_field_id()
|
||||
|
||||
self.assertEqual(test_result, self.env['ir.model.fields'])
|
||||
|
||||
def test_default_image_all_info_in_context(self):
|
||||
"""Should return value of correct field if all info in context"""
|
||||
active_model = 'res.users'
|
||||
active_record_id = 1
|
||||
active_field = 'name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_record_id': active_record_id,
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_image()
|
||||
|
||||
expected = self.env[active_model].browse(active_record_id).name
|
||||
self.assertEqual(test_result, expected)
|
||||
|
||||
def test_default_image_no_valid_field_in_context(self):
|
||||
"""Should return None if missing/invalid field info in context"""
|
||||
active_model = 'res.users'
|
||||
active_record_id = 1
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_record_id': active_record_id,
|
||||
})
|
||||
test_result = test_model._default_image()
|
||||
|
||||
self.assertIsNone(test_result)
|
||||
|
||||
def test_default_image_no_valid_id_in_context(self):
|
||||
"""Should return False/None if missing/invalid record ID in context"""
|
||||
active_model = 'res.users'
|
||||
active_field = 'name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_image()
|
||||
|
||||
self.assertFalse(test_result)
|
||||
|
||||
def test_default_image_no_valid_model_in_context(self):
|
||||
"""Should return None if missing/invalid model info in context"""
|
||||
active_record_id = 1
|
||||
active_field = 'name'
|
||||
test_model = self.env['darkroom.modal'].with_context({
|
||||
'active_record_id': active_record_id,
|
||||
'active_field': active_field,
|
||||
})
|
||||
test_result = test_model._default_image()
|
||||
|
||||
self.assertIsNone(test_result)
|
||||
|
||||
def test_action_save_record_count_in_self(self):
|
||||
"""Should raise correct error if not called on recordset of 1"""
|
||||
test_wizard = self.env['darkroom.modal'].with_context({
|
||||
'active_model': 'res.users',
|
||||
'active_record_id': 1,
|
||||
'active_field': 'name',
|
||||
}).create({})
|
||||
test_wizard_set = test_wizard + test_wizard.copy()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.env['darkroom.modal'].action_save()
|
||||
with self.assertRaises(ValueError):
|
||||
test_wizard_set.action_save()
|
||||
|
||||
def test_action_save_update_source(self):
|
||||
"""Should update source record correctly"""
|
||||
active_model = 'res.users'
|
||||
active_record_id = 1
|
||||
test_wizard = self.env['darkroom.modal'].with_context({
|
||||
'active_model': active_model,
|
||||
'active_record_id': active_record_id,
|
||||
'active_field': 'name',
|
||||
}).create({})
|
||||
test_name = 'Test Name'
|
||||
test_wizard.image = test_name
|
||||
test_wizard.action_save()
|
||||
|
||||
result = self.env[active_model].browse(active_record_id).name
|
||||
self.assertEqual(result, test_name)
|
||||
|
||||
def test_action_save_return_action(self):
|
||||
"""Should return correct action"""
|
||||
test_wizard = self.env['darkroom.modal'].with_context({
|
||||
'active_model': 'res.users',
|
||||
'active_record_id': 1,
|
||||
'active_field': 'name',
|
||||
}).create({})
|
||||
test_value = test_wizard.action_save()
|
||||
|
||||
self.assertEqual(test_value, {'type': 'ir.actions.act_window_close'})
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Copyright 2016-2017 LasLabs Inc.
|
||||
License LGPL-3 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
<template id="assets_darkroom" name="web_widget_darkroom Assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<link href="/web_widget_darkroom/static/src/less/darkroom.less" rel="stylesheet" type="text/less"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.5.0/fabric.require.min.js"/>
|
||||
|
||||
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js"/>
|
||||
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/plugin.js"/>
|
||||
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/transformation.js"/>
|
||||
<script src="/web_widget_darkroom/static/lib/darkroomjs/core/utils.js"/>
|
||||
|
||||
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js"/>
|
||||
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.history.js"/>
|
||||
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js"/>
|
||||
<script src="/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js"/>
|
||||
|
||||
<script src="/web_widget_darkroom/static/src/js/widget_darkroom.js"/>
|
||||
<script src="/web_widget_darkroom/static/src/js/widget_darkroom_modal.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from . import darkroom_modal
|
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 LasLabs Inc.
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from openerp import api, fields, models
|
||||
from openerp.exceptions import MissingError
|
||||
|
||||
|
||||
class DarkroomModal(models.TransientModel):
|
||||
_name = 'darkroom.modal'
|
||||
_description = 'Darkroom Modal - Wizard Model'
|
||||
|
||||
@api.model
|
||||
def _default_res_model_id(self):
|
||||
res_model_name = self.env.context.get('active_model')
|
||||
return self.env['ir.model'].search([('model', '=', res_model_name)])
|
||||
|
||||
@api.model
|
||||
def _default_res_record_id(self):
|
||||
return self.env.context.get('active_record_id', 0)
|
||||
|
||||
@api.model
|
||||
def _default_res_record(self):
|
||||
res_model_name = self._default_res_model_id().model
|
||||
try:
|
||||
res_model_model = self.env[res_model_name]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
return res_model_model.browse(self._default_res_record_id())
|
||||
|
||||
@api.model
|
||||
def _default_res_field_id(self):
|
||||
res_model_id = self._default_res_model_id()
|
||||
res_field_name = self.env.context.get('active_field')
|
||||
return self.env['ir.model.fields'].search([
|
||||
('model_id', '=', res_model_id.id),
|
||||
('name', '=', res_field_name),
|
||||
])
|
||||
|
||||
@api.model
|
||||
def _default_image(self):
|
||||
res_record = self._default_res_record()
|
||||
res_field_name = self._default_res_field_id().name
|
||||
|
||||
try:
|
||||
return getattr(res_record, res_field_name, None)
|
||||
except (TypeError, MissingError):
|
||||
return None
|
||||
|
||||
res_model_id = fields.Many2one(
|
||||
comodel_name='ir.model',
|
||||
string='Source Model',
|
||||
required=True,
|
||||
default=lambda s: s._default_res_model_id(),
|
||||
)
|
||||
res_record_id = fields.Integer(
|
||||
string='Source Record ID',
|
||||
required=True,
|
||||
default=lambda s: s._default_res_record_id(),
|
||||
)
|
||||
res_field_id = fields.Many2one(
|
||||
comodel_name='ir.model.fields',
|
||||
string='Source Field',
|
||||
required=True,
|
||||
default=lambda s: s._default_res_field_id(),
|
||||
)
|
||||
image = fields.Binary(
|
||||
string='Darkroom Image',
|
||||
required=True,
|
||||
default=lambda s: s._default_image(),
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def action_save(self):
|
||||
self.ensure_one()
|
||||
|
||||
res_record = self._default_res_record()
|
||||
res_field_name = self._default_res_field_id().name
|
||||
setattr(res_record, res_field_name, self.image)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Copyright 2017 LasLabs Inc.
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
<record id="darkroom_modal_view_form" model="ir.ui.view">
|
||||
<field name="name">Darkroom Modal Wizard</field>
|
||||
<field name="model">darkroom.modal</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Darkroom Modal">
|
||||
<header />
|
||||
<sheet>
|
||||
<group name="data">
|
||||
<field name="image" widget="darkroom" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button special="cancel" string="Cancel" class="pull-left"/>
|
||||
<button name="action_save" type="object" string="Save" class="oe_highlight pull-right"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
Loading…
Reference in New Issue