Browse Source
Merge pull request #407 from LasLabs/release/9.0/web_widget_darkroom
Merge pull request #407 from LasLabs/release/9.0/web_widget_darkroom
[9.0][ADD] web_widget_darkroom: Add o2m DarkroomJS widgetpull/592/head
Dave Lasley
8 years ago
committed by
GitHub
24 changed files with 2272 additions and 0 deletions
-
99web_widget_darkroom/README.rst
-
5web_widget_darkroom/__init__.py
-
26web_widget_darkroom/__openerp__.py
-
BINweb_widget_darkroom/static/description/icon.png
-
BINweb_widget_darkroom/static/description/modal_screenshot_1.png
-
BINweb_widget_darkroom/static/description/modal_screenshot_2.png
-
356web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js
-
47web_widget_darkroom/static/lib/darkroomjs/core/plugin.js
-
43web_widget_darkroom/static/lib/darkroomjs/core/transformation.js
-
36web_widget_darkroom/static/lib/darkroomjs/core/utils.js
-
693web_widget_darkroom/static/src/js/plugins/darkroom.crop.js
-
76web_widget_darkroom/static/src/js/plugins/darkroom.history.js
-
64web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js
-
148web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js
-
224web_widget_darkroom/static/src/js/widget_darkroom.js
-
64web_widget_darkroom/static/src/js/widget_darkroom_modal.js
-
11web_widget_darkroom/static/src/less/darkroom.less
-
30web_widget_darkroom/static/src/xml/field_templates.xml
-
5web_widget_darkroom/tests/__init__.py
-
203web_widget_darkroom/tests/test_darkroom_modal.py
-
28web_widget_darkroom/views/assets.xml
-
5web_widget_darkroom/wizards/__init__.py
-
82web_widget_darkroom/wizards/darkroom_modal.py
-
27web_widget_darkroom/wizards/darkroom_modal.xml
@ -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', |
|||
], |
|||
} |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
After Width: 250 | Height: 250 | Size: 24 KiB |
After Width: 950 | Height: 550 | 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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue