From e5d65e7a8d2aa18a20253a201c3ab084f19b40b1 Mon Sep 17 00:00:00 2001 From: Oleg Bulkin Date: Wed, 15 Mar 2017 15:14:30 -0700 Subject: [PATCH] web_widget_darkroom: Modal, fixes, cleanup * Fix bugs involving the crop and pan functionality by modifying crop and zoom plugins and Darkroom widget * Add Darkroom modal to normal image widget, using darkroom.modal wizard model to provide backend support for modal view * Remove res.users view changes introduced for demo purposes (not needed due to modal functionality) * Clean up existing code, removing many unnecessary DarkroomJS files --- web_widget_darkroom/README.rst | 90 +- web_widget_darkroom/__init__.py | 4 +- web_widget_darkroom/__openerp__.py | 34 +- web_widget_darkroom/demo/res_users.xml | 24 - .../static/description/modal_screenshot_1.png | Bin 0 -> 24676 bytes .../static/description/modal_screenshot_2.png | Bin 0 -> 57284 bytes .../static/lib/darkroomjs/.editorconfig | 9 - .../static/lib/darkroomjs/.gitignore | 5 - .../static/lib/darkroomjs/CHANGELOG.md | 23 - .../static/lib/darkroomjs/LICENSE | 18 - .../static/lib/darkroomjs/README.md | 88 -- .../static/lib/darkroomjs/bower.json | 31 - .../static/lib/darkroomjs/core/darkroom.js | 356 +++++ .../static/lib/darkroomjs/core/plugin.js | 47 + .../lib/darkroomjs/core/transformation.js | 43 + .../static/lib/darkroomjs/core/utils.js | 36 + .../static/lib/darkroomjs/gh-pages.sh | 21 - .../static/lib/darkroomjs/gulpfile.js | 112 -- .../lib/darkroomjs/lib/css/_layout.scss | 12 - .../lib/darkroomjs/lib/css/_toolbar.scss | 99 -- .../lib/darkroomjs/lib/css/darkroom.scss | 2 - .../static/lib/darkroomjs/lib/icons/close.svg | 4 - .../static/lib/darkroomjs/lib/icons/crop.svg | 4 - .../static/lib/darkroomjs/lib/icons/done.svg | 4 - .../static/lib/darkroomjs/lib/icons/redo.svg | 4 - .../lib/darkroomjs/lib/icons/rotate-left.svg | 4 - .../lib/darkroomjs/lib/icons/rotate-right.svg | 4 - .../static/lib/darkroomjs/lib/icons/save.svg | 4 - .../static/lib/darkroomjs/lib/icons/undo.svg | 4 - .../lib/darkroomjs/lib/js/core/bootstrap.js | 14 - .../lib/darkroomjs/lib/js/core/darkroom.js | 354 ----- .../lib/darkroomjs/lib/js/core/plugin.js | 43 - .../darkroomjs/lib/js/core/transformation.js | 38 - .../static/lib/darkroomjs/lib/js/core/ui.js | 91 -- .../lib/darkroomjs/lib/js/core/utils.js | 31 - .../lib/js/plugins/darkroom.crop.js | 669 -------- .../lib/js/plugins/darkroom.history.js | 66 - .../lib/js/plugins/darkroom.rotate.js | 57 - .../lib/js/plugins/darkroom.save.js | 23 - .../static/lib/darkroomjs/package.json | 39 - .../static/src/css/darkroom.css | 11 - .../static/src/js/darkroom_plugins.js | 20 - .../static/src/js/plugins/darkroom.crop.js | 1366 +++++++++-------- .../static/src/js/plugins/darkroom.history.js | 152 +- .../static/src/js/plugins/darkroom.rotate.js | 122 +- .../static/src/js/plugins/darkroom.zoom.js | 340 ++-- .../static/src/js/widget_darkroom.js | 450 +++--- .../static/src/js/widget_darkroom.js.orig | 246 --- .../static/src/js/widget_darkroom_modal.js | 64 + .../static/src/less/darkroom.less | 11 + .../static/src/xml/field_templates.xml | 21 +- web_widget_darkroom/tests/__init__.py | 5 + .../tests/test_darkroom_modal.py | 203 +++ web_widget_darkroom/views/assets.xml | 29 +- web_widget_darkroom/wizards/__init__.py | 5 + web_widget_darkroom/wizards/darkroom_modal.py | 82 + .../wizards/darkroom_modal.xml | 27 + 57 files changed, 2158 insertions(+), 3507 deletions(-) mode change 100755 => 100644 web_widget_darkroom/__init__.py mode change 100755 => 100644 web_widget_darkroom/__openerp__.py delete mode 100644 web_widget_darkroom/demo/res_users.xml create mode 100644 web_widget_darkroom/static/description/modal_screenshot_1.png create mode 100644 web_widget_darkroom/static/description/modal_screenshot_2.png delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/.editorconfig delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/.gitignore delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/CHANGELOG.md delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/LICENSE delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/README.md delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/bower.json create mode 100755 web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js create mode 100755 web_widget_darkroom/static/lib/darkroomjs/core/plugin.js create mode 100755 web_widget_darkroom/static/lib/darkroomjs/core/transformation.js create mode 100755 web_widget_darkroom/static/lib/darkroomjs/core/utils.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/gh-pages.sh delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/gulpfile.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/css/_layout.scss delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/css/_toolbar.scss delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/css/darkroom.scss delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/close.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/crop.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/done.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/redo.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-left.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-right.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/save.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/icons/undo.svg delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/bootstrap.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/darkroom.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/plugin.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/transformation.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/ui.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/core/utils.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.crop.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.history.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.rotate.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.save.js delete mode 100755 web_widget_darkroom/static/lib/darkroomjs/package.json delete mode 100755 web_widget_darkroom/static/src/css/darkroom.css delete mode 100644 web_widget_darkroom/static/src/js/darkroom_plugins.js delete mode 100644 web_widget_darkroom/static/src/js/widget_darkroom.js.orig create mode 100644 web_widget_darkroom/static/src/js/widget_darkroom_modal.js create mode 100755 web_widget_darkroom/static/src/less/darkroom.less create mode 100644 web_widget_darkroom/tests/__init__.py create mode 100644 web_widget_darkroom/tests/test_darkroom_modal.py create mode 100644 web_widget_darkroom/wizards/__init__.py create mode 100644 web_widget_darkroom/wizards/darkroom_modal.py create mode 100644 web_widget_darkroom/wizards/darkroom_modal.xml diff --git a/web_widget_darkroom/README.rst b/web_widget_darkroom/README.rst index befb0c9f..02e280d6 100755 --- a/web_widget_darkroom/README.rst +++ b/web_widget_darkroom/README.rst @@ -1,70 +1,72 @@ -.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg +.. 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 -====================== -Odoo DarkroomJS Widget -====================== +================================ +DarkroomJS Image Editing for Web +================================ -This module provides a `DarkroomJS`_ web widget for use with images fields. +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 -This widget will allow you to perform the following actions on images: +The widget currently supports the following operations and can be extended to +allow others: - * Zoom - * Rotate - * Crop - * Step back in history client-side (before save) +* Zoom and pan +* Rotate +* Crop +* Step back in history client-side (before save) - Usage ===== -To use this module, you need to: - -* Install web_widget_darkroom -* Add the to any One2many image relation by using the `darkroom` widget. Options can be passed through to Darkroom using the `options` key:: +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:: -The Odoo DarkroomJS widget passes options directly through to Darkroom, which are copied from the source below:: + - // Default options - defaults: { - // Canvas properties (dimension, ratio, color) - minWidth: null, - minHeight: null, - maxWidth: null, - maxHeight: null, - ratio: null, - backgroundColor: '#fff', + The widget passes options directly through to DarkroomJS, which supports the + following: - // Plugins options - plugins: {}, + * minWidth + * minHeight + * maxWidth + * maxHeight + * ratio (aspect ratio) + * backgroundColor - // Post-initialisation callback - initialize: function() { /* noop */ } - }, +* 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 -==================== - -* Plugins are not able to be added without inheriting, then redefining the widget in the registry due to JS inheritance. - ** This is not scalable because there would need to be an explicit dependency chain in order to avoid registry overwrite. - +Known Issues / Roadmap +====================== +* Darkroom modals are currently not supported during record creation Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smashing it by providing a detailed and welcomed feedback. +Bugs are tracked on `GitHub 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 ======= @@ -72,12 +74,14 @@ Credits Images ------ -* Odoo Community Association: `Icon `_. +* Odoo Community Association: + `Icon `_. Contributors ------------ * Dave Lasley +* Oleg Bulkin Maintainer ---------- diff --git a/web_widget_darkroom/__init__.py b/web_widget_darkroom/__init__.py old mode 100755 new mode 100644 index 08d9d6b0..c1a869f5 --- a/web_widget_darkroom/__init__.py +++ b/web_widget_darkroom/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2016 LasLabs Inc. +# Copyright 2016-2017 LasLabs Inc. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from . import wizards diff --git a/web_widget_darkroom/__openerp__.py b/web_widget_darkroom/__openerp__.py old mode 100755 new mode 100644 index 9bc1c568..47a4b742 --- a/web_widget_darkroom/__openerp__.py +++ b/web_widget_darkroom/__openerp__.py @@ -1,28 +1,26 @@ # -*- coding: utf-8 -*- -# Copyright 2016 LasLabs Inc. +# Copyright 2016-2017 LasLabs Inc. # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). { - "name": "Web Darkroom Image Widget", - "summary": "Widget provides a dynamic, editable canvas for use on any" - " One2many image field in backend form views.", - "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", + '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": [ + 'data': [ 'views/assets.xml', + 'wizards/darkroom_modal.xml', ], 'qweb': [ - "static/src/xml/field_templates.xml", + 'static/src/xml/field_templates.xml', ], - 'demo': [ - 'demo/res_users.xml', - ] } diff --git a/web_widget_darkroom/demo/res_users.xml b/web_widget_darkroom/demo/res_users.xml deleted file mode 100644 index 43a17825..00000000 --- a/web_widget_darkroom/demo/res_users.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - res.users.form.darkroom - res.users - - - - - - - - - - - - diff --git a/web_widget_darkroom/static/description/modal_screenshot_1.png b/web_widget_darkroom/static/description/modal_screenshot_1.png new file mode 100644 index 0000000000000000000000000000000000000000..5505d68b2e2415320fbb9a0ce521a10641852a8a GIT binary patch literal 24676 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 sB{{j~=hv$veRFg3%1?jvUOw3!A<3S4k;wJOV literal 0 HcmV?d00001 diff --git a/web_widget_darkroom/static/description/modal_screenshot_2.png b/web_widget_darkroom/static/description/modal_screenshot_2.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0ce5691809cd1bcb5097ad12a4d0f7a8b2afac GIT binary patch literal 57284 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 q$OGiSU6P&$Z6$eL^ zMkb~TUr}#dl)xB$=EffHaV^yJ5SjJ#gvERu$C!n6BR=6Q4B{N$N$e;4`Xz7v0zk-? zyHh_eD+)(1U8#961^5?=LkB15Va_O7H}|0<+bG{VH3fqCES8sA4AGxfx1L1Yc2;v< z!8^a9(ori7r&gHV9W@K51PjP;Y5%P7=uV2AcF#Y}(`{bqaucUSHt4g3|BBI4V?$L9 zV-t&Uq8vBJccE|Z=v=C$^3~c#k@uBoJaY;2J2?P@52B33GM3!;Hy^hYYI;mWQcFSQ zvvGA*%=X)*Z~XH1lzD3v-LBNx5ibDYpu+v_isx@{`E-rF)Q*m5AXAPbH03D8>_rm}F*FWl*a?t2o?M|&T{4{d$u zFb!}f9Evxo5-6Y8$B$_8zQ_D&!AX7C{3mxRT0uNr!dFkWa(L%>;JpF-O6p@}XUElf z)~vJhopZ;q`t9Ruls8^PHp#?&>_J}2g?Di9Gic~o?<^S75rxQ36%G=wsLRL)7GqvR zJkSU-`plNIr%MtPqb-7I+@aO7J#w@y3FAC1vb5CP`o6ifPL0V$|9fH;Bv;OHll9)6 zF6pjlPZi*R&(2A z+2=-OUMcnSh1=6LtQ}MV%L~-QA8<(5^{8AFq5z~EDu|{9v$ppR#{?KR}O@+ol*tg>D2`w=1={+Ow4t=1n#%Ws{Y5{ ziAIGX#Jus`9mxhGEdX^u&AvL>u*t`fI*o;tSI^UA87!z6tc@6d0>8M)skiFU68m10 zc&eyI7ap29`*4gWIWEwKM)n6`a1X2DlsF!Kw}d@EP_!BW6~)3@anh6 zffNf}e95Syo6f{B|)o>n)P z5IcL%XW{i01ar8(LM5~aY;0OYxL;8oe0AzJsAal}aCJTf0|H_k{oFl2 zYiz#I1hZEWvMKS)d15nr5Tgt`Z%KYgLtt;7M2DPqn%=gwm@%;n^Ph$To;t}M$Di7n zmy!I2#)DLAqw}$Jhy0fB{#2=)Wd0dW+%{GK?l(6oeFoTQ1~_9B^vahlvj>VE$fP%QPAK2L7Xu? zJz4IzwZ!<;Ok~4biIEmEt|p5}ropMT^VAx)6+5nleh!@jJhDATC9u}jt1SOKwX&2K zJmDP4a3iRDSC|qO-$>B`>yE7OcC;B>9+9QjqE3V8t$~-5S>mB6@e(Y*?#LCdQPouc z7)x=;C)hEpX8JfGu6@zQhg#pGhDhpO;5q&!zcBwBE@7(A;PC2r%GjvGm%H;VTt+M^ zm^@oQ-`ot_6!hIe$2m<}w4F3;k#13Jx5jF}sYfAH+w%@=@chd5j{k@(`5Fe-dr(w| zW~{}GJtm%kDkhvSdn7B(!=-@5K>q&$CR_WX1UvF9TS!Y{=7X0OFVmut9ai7GZL7e_Kh?n zwce14$ko?U`GGJD_u7*Hj0%h4EOX~JI>L3u?{nP!?6nTdjgF%$%6RFrS+K=mImc(_ zU0#VOMF|i8mdJ)~;hmse7wmj;K=tt^e|~^Z&Xh)4#keHY)F?f_%$5&)3=4*7Wer8* zP&^UwQk61OR^rX(s+Ql88A|-XwgOjOX3V(wkrB-IrRAkbT@#qF;LHylo#S-Nr%Lx1 z?kQv1H`nhG)fKuoMtQp_ZEXC2UFfq*7C14$!TcsU8jZ>K@h~Z-LKAkx7={i$m-OTM zk)K?{GG(Z-<*uBij^~?QKUj^)|LzW-Q085}h+y81wQ4A&)28}c+2LD;)v>9u7$&@6 zPc1Bs2Ow<9QC3-ghCb@1qp6HC!Ex&|&*~JFn~Q1_#dFX<8!6H`5ur!*qW{q?o_`pO zJrv3%pQN1>)RDIX1qaDQ;qIj zhRBW7*EWDIJ?8i}<}PK-LRd1!+k1;;j{}CE^(y{YSt4qSI7n+@Cv3j078CgKYtg1g zqVDq{AsCy>@g{Z9GPuaKsV?lmEU<)*3%L< z)&5OrhD8bCv&ZxjGoS_&M?rTCdpmoDM<+kVGiEFnZc#a}w2(oZWyMd;!SnveuD4{d zc#6oZI6I$Rkc`CLC6X)!Mm4sQDZa0J~sLL8%=)Ti07kgz}{6p7m9Lr!NnMcN5Z zy9JI5MG~E)Yx7!Oe{XM3zzK*~^jMnJg#V+bdoxXN!Sm4m8tSrc&5TEY0)!j3wp=o@ z=w=+cte`f5rKHg{yEgo_dLC!c?gY+s+AX#iPyE___C61z#-&Y#LXD-?*?d3u7vfqJ zq}3eb{Y$+CW3zc^UE=A6-Z}KW=cUw@eS@R)vej=ck0Z1Upq8mRlY3kNUDW zTbsFQdfu7I?*{zUH60pQ9O=w!Y3YSJK^iAFrTQ5!EG+6oE1<&XM(>)boh|ow2@nrAtg)21BaCv$fJt(!@KQ$GYo7NlJQu zRmgnch*|9S%Ng~U3?^+siDMB&1yP~CmO&VGq()#S%455$e_u8u*BABi)$JmW=M-bO zSE+8+2inr|dHOHI1_Q(8W9Su%7tj(Okvk-G_E%0D>G($WCNQ5sJCw{T%rPE*zP7hl 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 - -``` - -You can also pass some options: - -```javascript -new Darkroom('#target', { - // Canvas initialization size - minWidth: 100, - minHeight: 100, - maxWidth: 500, - maxHeight: 500, - - // Plugins options - plugins: { - crop: { - minHeight: 50, - minWidth: 50, - ratio: 1 - }, - save: false // disable plugin - }, - - // Post initialization method - initialize: function() { - // Active crop selection - this.plugins['crop'].requireFocus(); - - // Add custom listener - this.addEventListener('core:transformation', function() { /* ... */ }); - } -}); -``` - -## Why? - -It's easy to get a javascript script to crop an image in a web page. -But if your want more features like rotation or brightness adjustment, then you -will have to do it yourself. No more jQuery plugins here. -It only uses the power of HTML5 canvas to make what ever you want with your image. - -## The concept - -The library is designed to be easily extendable. The core script only transforms -the target image to a canvas with a FabricJS instance, and creates an empty toolbar. -All the features are then implemented in separate plugins. - -Each plugin is responsible for creating its own functionality. -Buttons can easily be added to the toolbar and binded with those features. - -## Contributing - -Run `npm develop` to build and watch the files while developing. - -## License - -DarkroomJS is released under the MIT License. See the [bundled LICENSE file](LICENSE) -for details. - diff --git a/web_widget_darkroom/static/lib/darkroomjs/bower.json b/web_widget_darkroom/static/lib/darkroomjs/bower.json deleted file mode 100755 index aa38ffe7..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/bower.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "darkroom", - "description": "Extensible image editing tool via HTML canvas", - "version": "2.0.0", - "homepage": "http://mattketmo.github.io/darkroomjs/", - "authors": [ - "Matthieu Moquet " - ], - "license": "MIT", - "dependencies": { - "fabric": "~1.4.*" - }, - "main": [ - "build/darkroom.css", - "build/darkroom.js" - ], - "moduleType": [ - "globals" - ], - "keywords": [ - "image", - "canvas", - "crop" - ], - "ignore": [ - "node_modules", - "bower_components", - "test", - "tests" - ] -} diff --git a/web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js b/web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js new file mode 100755 index 00000000..328fed20 --- /dev/null +++ b/web_widget_darkroom/static/lib/darkroomjs/core/darkroom.js @@ -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); + } + }, + }; +})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/core/plugin.js b/web_widget_darkroom/static/lib/darkroomjs/core/plugin.js new file mode 100755 index 00000000..03334418 --- /dev/null +++ b/web_widget_darkroom/static/lib/darkroomjs/core/plugin.js @@ -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; + }; +})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/core/transformation.js b/web_widget_darkroom/static/lib/darkroomjs/core/transformation.js new file mode 100755 index 00000000..8ea8441a --- /dev/null +++ b/web_widget_darkroom/static/lib/darkroomjs/core/transformation.js @@ -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; + }; +})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/core/utils.js b/web_widget_darkroom/static/lib/darkroomjs/core/utils.js new file mode 100755 index 00000000..2e0a5e21 --- /dev/null +++ b/web_widget_darkroom/static/lib/darkroomjs/core/utils.js @@ -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))), + }; + } +})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/gh-pages.sh b/web_widget_darkroom/static/lib/darkroomjs/gh-pages.sh deleted file mode 100755 index 2ae99a52..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/gh-pages.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -# Update gh-pages branch -git branch -D gh-pages -git checkout -b gh-pages HEAD - -# Build assets -rm -rf build -gulp build --prod - -# Put build into demo folder -rm demo/build -cp -r build demo/build - -# Commit -git add -f demo -git commit -m "Build GH pages" - -# Push & reset -git push origin `git subtree split --prefix demo HEAD`:gh-pages --force -git checkout - diff --git a/web_widget_darkroom/static/lib/darkroomjs/gulpfile.js b/web_widget_darkroom/static/lib/darkroomjs/gulpfile.js deleted file mode 100755 index dced1e88..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/gulpfile.js +++ /dev/null @@ -1,112 +0,0 @@ -var concat = require('gulp-concat') -var connect = require('gulp-connect') -var gulp = require('gulp') -var gutil = require('gulp-util') -var htmlJsStr = require('js-string-escape') -var inject = require('gulp-inject') -var plumber = require('gulp-plumber') -var rimraf = require('rimraf') -var sass = require('gulp-sass') -var sourcemaps = require('gulp-sourcemaps') -var spawn = require("child_process").spawn -var streamqueue = require('streamqueue') -var svgmin = require('gulp-svgmin') -var svgstore = require('gulp-svgstore') -var uglify = require('gulp-uglify') - - -// -// Variables -// -var srcDir = './lib'; -var distDir = './build'; -var isDebug = !gutil.env.prod; - -// -// Default -// -gulp.task('default', ['build'], function() { - gulp.start('watch'); -}); - -// -// Clean -// -gulp.task('clean', function(cb) { - rimraf(distDir, cb); -}); - -// -// Build -// -gulp.task('build', ['clean'], function() { - gulp.start('scripts', 'styles'); -}); - -// -// Watch -// -gulp.task('watch', ['server'], function() { - gulp.watch(srcDir + '/js/**/*.js', ['scripts']); - - gulp.watch(srcDir + '/css/**/*.scss', ['styles']); -}); - -// -// Server -// -gulp.task('server', function() { - connect.server({ - root: './demo', - port: 2222, - livereload: false - }); -}); - -// -// Javascript -// -gulp.task('scripts', function () { - var svgs = gulp.src(srcDir + '/icons/*.svg') - .pipe(svgmin()) - .pipe(svgstore({inlineSvg: true})) - // .pipe(gulp.dest(distDir)); - - function fileContents (filePath, file) { - return file.contents.toString(); - } - - var files = [ - srcDir + '/js/core/bootstrap.js', - srcDir + '/js/core/darkroom.js', - srcDir + '/js/core/*.js', - // srcDir + '/js/plugins/*.js', - srcDir + '/js/plugins/darkroom.history.js', - srcDir + '/js/plugins/darkroom.rotate.js', - srcDir + '/js/plugins/darkroom.crop.js', - srcDir + '/js/plugins/darkroom.save.js', - ]; - - gulp.src(files) - .pipe(plumber()) - .pipe(isDebug ? sourcemaps.init() : gutil.noop()) - .pipe(concat('darkroom.js', {newLine: ';'})) - .pipe(inject(svgs, { transform: fileContents })) - .pipe(isDebug ? gutil.noop() : uglify({mangle: false})) - .pipe(isDebug ? sourcemaps.write() : gutil.noop()) - .pipe(gulp.dest(distDir)) -}) - -// -// Stylesheet -// -gulp.task('styles', function () { - gulp.src(srcDir + '/css/darkroom.scss') - .pipe(plumber()) - .pipe(isDebug ? sourcemaps.init() : gutil.noop()) - .pipe(sass({ - outputStyle: isDebug ? 'nested' : 'compressed' - })) - .pipe(isDebug ? sourcemaps.write() : gutil.noop()) - .pipe(gulp.dest(distDir)) -}) diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/css/_layout.scss b/web_widget_darkroom/static/lib/darkroomjs/lib/css/_layout.scss deleted file mode 100755 index 693cd38a..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/css/_layout.scss +++ /dev/null @@ -1,12 +0,0 @@ -.darkroom-container { - position: relative; -} - -.darkroom-image-container { - top: 0; - left: 0; -} - -.darkroom-image-container img { - // display: none; -} diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/css/_toolbar.scss b/web_widget_darkroom/static/lib/darkroomjs/lib/css/_toolbar.scss deleted file mode 100755 index f90aaac5..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/css/_toolbar.scss +++ /dev/null @@ -1,99 +0,0 @@ -// -// Toolbar -// -.darkroom-toolbar { - display: block; - position: absolute; - top: -45px; - left: 0; - background: #444; - height: 40px; - min-width: 40px; - z-index: 99; - border-radius: 2px; - white-space: nowrap; - padding: 0 5px; - - // Triangle - &:before { - content: ""; - position: absolute; - bottom: -7px; - left: 20px; - width: 0; - height: 0; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-top: 7px solid #444; - } -} - -// -// Button Group -// -.darkroom-button-group { - display: inline-block; - margin: 0; - padding: 0; - // border-right: 1px solid #777; - - &:last-child { - border-right: none; - } -} - - -// -// Button -// -.darkroom-button { - box-sizing: border-box; - background: transparent; - border: none; - outline: none; - padding: 2px 0 0 0; - width: 40px; - height: 40px; - - &:hover { - cursor: pointer; - background: #555; - } - &:active { - cursor: pointer; - background: #333; - } - - &:disabled .darkroom-icon { - fill: #666; - } - &:disabled:hover { - cursor: default; - /*cursor: not-allowed;*/ - background: transparent; - } - &.darkroom-button-active .darkroom-icon { - fill: #33b5e5; - } - &.darkroom-button-hidden { - display: none; - } - &.darkroom-button-success .darkroom-icon { - fill: #99cc00; - } - &.darkroom-button-warning .darkroom-icon { - fill: #FFBB33; - } - &.darkroom-button-danger .darkroom-icon { - fill: #FF4444; - } -} - -// -// Icon -// -.darkroom-icon { - width: 24px; - height: 24px; - fill: #fff; -} diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/css/darkroom.scss b/web_widget_darkroom/static/lib/darkroomjs/lib/css/darkroom.scss deleted file mode 100755 index 22d3411c..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/css/darkroom.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'layout'; -@import 'toolbar'; diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/close.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/close.svg deleted file mode 100755 index 6c375ca8..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/crop.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/crop.svg deleted file mode 100755 index f3affcc4..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/crop.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/done.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/done.svg deleted file mode 100755 index 0af0fb9f..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/done.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/redo.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/redo.svg deleted file mode 100755 index fb1cc3a1..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/redo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-left.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-left.svg deleted file mode 100755 index dd87ec4f..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-left.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-right.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-right.svg deleted file mode 100755 index fdfae402..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/rotate-right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/save.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/save.svg deleted file mode 100755 index 2a4e62bb..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/save.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/undo.svg b/web_widget_darkroom/static/lib/darkroomjs/lib/icons/undo.svg deleted file mode 100755 index 273f4918..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/icons/undo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/bootstrap.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/bootstrap.js deleted file mode 100755 index 34f32795..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/bootstrap.js +++ /dev/null @@ -1,14 +0,0 @@ -(function() { -'use strict'; - -// Inject SVG icons into the DOM -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 = ''; -document.body.appendChild(element); - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/darkroom.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/darkroom.js deleted file mode 100755 index 7ea5b363..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/darkroom.js +++ /dev/null @@ -1,354 +0,0 @@ -(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'ss constructor. -Darkroom.plugins = []; - -Darkroom.prototype = { - // Refenrece 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, plugins) { - 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.crossOrigin = 'anonymous'; - 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); - } - }, -} - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/plugin.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/plugin.js deleted file mode 100755 index 07b35a05..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/plugin.js +++ /dev/null @@ -1,43 +0,0 @@ -(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() { } -} - -// 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; -} - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/transformation.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/transformation.js deleted file mode 100755 index e0d27461..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/transformation.js +++ /dev/null @@ -1,38 +0,0 @@ -(function() { -'use strict'; - -Darkroom.Transformation = Transformation; - -function Transformation(options) { - this.options = options; -} - -Transformation.prototype = { - applyTransformation: function(image) { /* 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; -} - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/ui.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/ui.js deleted file mode 100755 index b4d752a8..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/ui.js +++ /dev/null @@ -1,91 +0,0 @@ -(function() { -'use strict'; - -Darkroom.UI = { - Toolbar: Toolbar, - ButtonGroup: ButtonGroup, - Button: Button, -}; - -// Toolbar object. -function Toolbar(element) { - this.element = element; -} - -Toolbar.prototype = { - createButtonGroup: function(options) { - var buttonGroup = document.createElement('div'); - buttonGroup.className = 'darkroom-button-group'; - this.element.appendChild(buttonGroup); - - return new ButtonGroup(buttonGroup); - } -}; - -// ButtonGroup object. -function ButtonGroup(element) { - this.element = element; -} - -ButtonGroup.prototype = { - createButton: function(options) { - var defaults = { - image: 'help', - type: 'default', - group: 'default', - hide: false, - disabled: false - }; - - options = Darkroom.Utils.extend(options, defaults); - - var buttonElement = document.createElement('button'); - buttonElement.type = 'button'; - buttonElement.className = 'darkroom-button darkroom-button-' + options.type; - buttonElement.innerHTML = ''; - this.element.appendChild(buttonElement); - - var button = new Button(buttonElement); - button.hide(options.hide); - button.disable(options.disabled); - - return button; - } -} - -// 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); - } - }, - active: function(value) { - if (value) - this.element.classList.add('darkroom-button-active'); - else - this.element.classList.remove('darkroom-button-active'); - }, - hide: function(value) { - if (value) - this.element.classList.add('darkroom-button-hidden'); - else - this.element.classList.remove('darkroom-button-hidden'); - }, - disable: function(value) { - this.element.disabled = (value) ? true : false; - } -}; - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/utils.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/utils.js deleted file mode 100755 index f4de5f9a..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/core/utils.js +++ /dev/null @@ -1,31 +0,0 @@ -(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))), - } -} - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.crop.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.crop.js deleted file mode 100755 index 9c441823..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.crop.js +++ /dev/null @@ -1,669 +0,0 @@ -(function() { -'use strict'; - -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() { - // 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 - }); - - var width = this.width; - var height = this.height; - - // 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 canvas = ctx.canvas; - 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 (ctx.setLineDash !== undefined) - ctx.setLineDash([dashWidth, dashWidth]); - else if (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(); - ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2); // upper left - ctx.lineTo(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.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 dimension - 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: 'crop' - }); - this.okButton = buttonGroup.createButton({ - image: 'done', - type: 'success', - hide: true - }); - this.cancelButton = buttonGroup.createButton({ - image: 'close', - type: 'danger', - hide: true - }); - - // Buttons click - 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 pointer = canvas.getPointer(event.e); - var x = pointer.x; - var y = pointer.y; - - var minX = currentObject.getLeft(); - var minY = currentObject.getTop(); - var maxX = currentObject.getLeft() + currentObject.getWidth(); - var maxY = currentObject.getTop() + currentObject.getHeight(); - - if (null !== this.options.ratio) { - 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 (null === this.startX || null === this.startY) { - 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(event) { - if (null === this.startX || null === this.startY) { - 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 (false === this.options.quickCropKey || 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 (false === this.options.quickCropKey || event.keyCode !== this.options.quickCropKey || !this.isKeyCroping) - return; - - // Unactive 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._renderCropZone(x, y, x+width, y+height); - } else { - this.cropZone.set({ - 'left': x, - 'top': y, - 'width': width, - 'height': 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.requireFocus(); - else - this.releaseFocus(); - }, - - 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 wether crop zone is set - hasFocus: function() { - return this.cropZone !== undefined; - }, - - // Create the crop zone - requireFocus: function() { - this.cropZone = new CropZone({ - fill: 'transparent', - hasBorders: false, - originX: 'left', - originY: 'top', - //stroke: '#444', - //strokeDashArray: [5, 5], - //borderColor: '#444', - cornerColor: '#444', - cornerSize: 8, - transparentCorners: false, - lockRotation: true, - hasRotatingPoint: false, - }); - - if (null !== this.options.ratio) { - 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 (undefined === this.cropZone) - 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(+this.options.minWidth, canvas.getWidth()); - var minHeight = Math.min(+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 && +this.options.ratio !== currentRatio) { - var ratio = +this.options.ratio; - - if(this.isKeyCroping) { - isLeft = this.isKeyLeft; - isUp = this.isKeyUp; - } - - if (currentRatio < ratio) { - var newWidth = height * ratio; - if (isLeft) { - leftX -= (newWidth - width); - } - width = newWidth; - } else if (currentRatio > ratio) { - var 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()) { - var newWidth = canvas.getWidth() - leftX; - height = newWidth * height / width; - width = newWidth; - if (isUp) { - topY = fromY - height; - } - } - if (topY + height > canvas.getHeight()) { - var 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'); - } -}); - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.history.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.history.js deleted file mode 100755 index 81bea34a..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.history.js +++ /dev/null @@ -1,66 +0,0 @@ -;(function(window, document, Darkroom, fabric) { - 'use strict'; - - 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: 'undo', - disabled: true - }); - - this.forwardButton = buttonGroup.createButton({ - image: 'redo', - disabled: 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(); - } - }); -})(window, document, Darkroom, fabric); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.rotate.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.rotate.js deleted file mode 100755 index 1f2fbe92..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.rotate.js +++ /dev/null @@ -1,57 +0,0 @@ -(function() { -'use strict'; - -var Rotation = Darkroom.Transformation.extend({ - applyTransformation: function(canvas, image, next) { - var angle = (image.getAngle() + this.options.angle) % 360; - image.rotate(angle); - - var width, height; - height = Math.abs(image.getWidth()*(Math.sin(angle*Math.PI/180)))+Math.abs(image.getHeight()*(Math.cos(angle*Math.PI/180))); - 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: 'rotate-left' - }); - - var rightButton = buttonGroup.createButton({ - image: 'rotate-right' - }); - - 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}) - ); - } - -}); - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.save.js b/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.save.js deleted file mode 100755 index d12eeadf..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/lib/js/plugins/darkroom.save.js +++ /dev/null @@ -1,23 +0,0 @@ -(function() { -'use strict'; - -Darkroom.plugins['save'] = Darkroom.Plugin.extend({ - - defaults: { - callback: function() { - this.darkroom.selfDestroy(); - } - }, - - initialize: function InitializeDarkroomSavePlugin() { - var buttonGroup = this.darkroom.toolbar.createButtonGroup(); - - this.destroyButton = buttonGroup.createButton({ - image: 'save' - }); - - this.destroyButton.addEventListener('click', this.options.callback.bind(this)); - }, -}); - -})(); diff --git a/web_widget_darkroom/static/lib/darkroomjs/package.json b/web_widget_darkroom/static/lib/darkroomjs/package.json deleted file mode 100755 index 4684f431..00000000 --- a/web_widget_darkroom/static/lib/darkroomjs/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "darkroom", - "description": "Extensible image editing tool via HTML canvas", - "version": "2.0.1", - "license": "MIT", - "homepage": "https://mattketmo.github.io/darkroomjs", - "repository": { - "type": "git", - "url": "https://github.com/mattketmo/darkroomjs.git" - }, - "author": "Matthieu Moquet (http://moquet.net/)", - "dependencies": {}, - "devDependencies": { - "cheerio": "^0.18.0", - "gulp": "^3.8.6", - "gulp-concat": "^2.3.4", - "gulp-connect": "^2.0.6", - "gulp-inject": "^1.2.0", - "gulp-plumber": "^0.6.4", - "gulp-sass": "^0.7.2", - "gulp-sourcemaps": "^1.1.0", - "gulp-svgmin": "^1.1.1", - "gulp-svgstore": "^5.0.0", - "gulp-uglify": "^0.3.1", - "gulp-util": "^3.0.0", - "js-string-escape": "^1.0.0", - "rimraf": "^2.2.8", - "streamqueue": "^0.1.1" - }, - "scripts": { - "start": "node_modules/.bin/gulp server build --prod", - "develop": "node_modules/.bin/gulp" - }, - "ignore": [ - "**/.*", - "node_modules", - "bower_components" - ] -} diff --git a/web_widget_darkroom/static/src/css/darkroom.css b/web_widget_darkroom/static/src/css/darkroom.css deleted file mode 100755 index 21d4668a..00000000 --- a/web_widget_darkroom/static/src/css/darkroom.css +++ /dev/null @@ -1,11 +0,0 @@ -/*.darkroom-container{ - padding-top: 50px; -} -.darkroom-toolbar{ - top: 5px; -} -*/ - -.darkroom-button-group{ - display: inline; -} diff --git a/web_widget_darkroom/static/src/js/darkroom_plugins.js b/web_widget_darkroom/static/src/js/darkroom_plugins.js deleted file mode 100644 index b7dd01c1..00000000 --- a/web_widget_darkroom/static/src/js/darkroom_plugins.js +++ /dev/null @@ -1,20 +0,0 @@ -/* Copyright 2016 LasLabs Inc. - * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). - */ - -odoo.define('web_widget_darkroom.darkroom_plugins', function(require){ - "use strict"; - - var DarkroomPlugins = Object; - DarkroomPlugins.extend = function(destination, source) { - for (var property in source) { - if (source.hasOwnProperty(property)) { - destination[property] = source[property]; - } - } - return destination; - }; - - return DarkroomPlugins - -}); diff --git a/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js b/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js index 8819a57c..0ae738f0 100755 --- a/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js +++ b/web_widget_darkroom/static/src/js/plugins/darkroom.crop.js @@ -1,683 +1,693 @@ -/* Adapted from https://github.com/MattKetmo/darkroomjs/tree/master/lib/js/plugins - * License https://github.com/MattKetmo/darkroomjs/blob/master/LICENSE - */ - -odoo.define('web_widget_darkroom.darkroom_crop', function(require){ - - '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() { - // 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 - }); - - var width = this.width; - var height = this.height; - - // 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 canvas = ctx.canvas; - 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 (ctx.setLineDash !== undefined) - ctx.setLineDash([dashWidth, dashWidth]); - else if (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(); - ctx.moveTo(-this.getWidth()/2, -this.getHeight()/2); // upper left - ctx.lineTo(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.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 dimension - 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 +/** +* 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, + }); + }, }); - - // Buttons click - 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 pointer = canvas.getPointer(event.e); - var x = pointer.x; - var y = pointer.y; - - var minX = currentObject.getLeft(); - var minY = currentObject.getTop(); - var maxX = currentObject.getLeft() + currentObject.getWidth(); - var maxY = currentObject.getTop() + currentObject.getHeight(); - - if (null !== this.options.ratio) { - 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 (null === this.startX || null === this.startY) { - 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(event) { - if (null === this.startX || null === this.startY) { - 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 (false === this.options.quickCropKey || 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 (false === this.options.quickCropKey || event.keyCode !== this.options.quickCropKey || !this.isKeyCroping) - return; - - // Unactive 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._renderCropZone(x, y, x+width, y+height); - } else { - this.cropZone.set({ - 'left': x, - 'top': y, - 'width': width, - 'height': 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.requireFocus(); - else - this.releaseFocus(); - }, - - 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 wether crop zone is set - hasFocus: function() { - return this.cropZone !== undefined; - }, - - // Create the crop zone - requireFocus: function() { - this.cropZone = new CropZone({ - fill: 'transparent', - hasBorders: false, - originX: 'left', - originY: 'top', - //stroke: '#444', - //strokeDashArray: [5, 5], - //borderColor: '#444', - cornerColor: '#444', - cornerSize: 8, - transparentCorners: false, - lockRotation: true, - hasRotatingPoint: false, + + 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(); + }, }); - - if (null !== this.options.ratio) { - 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 (undefined === this.cropZone) - 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(+this.options.minWidth, canvas.getWidth()); - var minHeight = Math.min(+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 && +this.options.ratio !== currentRatio) { - var ratio = +this.options.ratio; - - if(this.isKeyCroping) { - isLeft = this.isKeyLeft; - isUp = this.isKeyUp; - } - - if (currentRatio < ratio) { - var newWidth = height * ratio; - if (isLeft) { - leftX -= (newWidth - width); - } - width = newWidth; - } else if (currentRatio > ratio) { - var 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()) { - var newWidth = canvas.getWidth() - leftX; - height = newWidth * height / width; - width = newWidth; - if (isUp) { - topY = fromY - height; - } - } - if (topY + height > canvas.getHeight()) { - var newHeight = canvas.getHeight() - topY; - width = width * newHeight / height; - height = newHeight; - if (isLeft) { - leftX = fromX - width; + + 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'); } - } - } - - // 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}; + }); + }; + return {DarkroomPluginCrop: DarkroomPluginCrop}; }); diff --git a/web_widget_darkroom/static/src/js/plugins/darkroom.history.js b/web_widget_darkroom/static/src/js/plugins/darkroom.history.js index 99bc07f6..11b2569c 100755 --- a/web_widget_darkroom/static/src/js/plugins/darkroom.history.js +++ b/web_widget_darkroom/static/src/js/plugins/darkroom.history.js @@ -1,80 +1,76 @@ -/* Adapted from https://github.com/MattKetmo/darkroomjs/tree/master/lib/js/plugins - * License https://github.com/MattKetmo/darkroomjs/blob/master/LICENSE - */ - -odoo.define('web_widget_darkroom.darkroom_history', function(require){ - - '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, +/** +* 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(); + }, }); - - 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}; - + }; + + return {DarkroomPluginHistory: DarkroomPluginHistory}; }); diff --git a/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js b/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js index e50a0d5f..0a02629a 100755 --- a/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js +++ b/web_widget_darkroom/static/src/js/plugins/darkroom.rotate.js @@ -1,70 +1,64 @@ -/* Adapted from https://github.com/MattKetmo/darkroomjs/tree/master/lib/js/plugins - * License https://github.com/MattKetmo/darkroomjs/blob/master/LICENSE - */ +/** +* Copyright 2013 Matthieu Moquet +* Copyright 2016-2017 LasLabs Inc. +* License MIT (https://opensource.org/licenses/MIT) +**/ -odoo.define('web_widget_darkroom.darkroom_rotate', function(require){ - - 'use strict'; - - var DarkroomPluginRotate = function() { +odoo.define('web_widget_darkroom.darkroom_rotate', function() { + 'use strict'; - var Rotation = Darkroom.Transformation.extend({ - applyTransformation: function(canvas, image, next) { - var angle = (image.getAngle() + this.options.angle) % 360; - image.rotate(angle); - - var width, height; - height = Math.abs(image.getWidth()*(Math.sin(angle*Math.PI/180)))+Math.abs(image.getHeight()*(Math.cos(angle*Math.PI/180))); - 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 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(); + }, }); - - var rightButton = buttonGroup.createButton({ - image: 'fa fa-repeat oe_edit_only', - editOnly: true, + + 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}) + ); + } }); - - 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}; - + return {DarkroomPluginRotate: DarkroomPluginRotate}; }); diff --git a/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js b/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js index 65085aee..c4849196 100755 --- a/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js +++ b/web_widget_darkroom/static/src/js/plugins/darkroom.zoom.js @@ -1,198 +1,148 @@ -/* Copyright 2016 LasLabs Inc. - * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). - */ - -odoo.define('web_widget_darkroom.darkroom_zoom', function(require){ - - 'use strict'; - - var DarkroomPluginZoom = function(){ - - Darkroom.plugins['zoom'] = Darkroom.Plugin.extend({ - - inZoom: false, - zoomLevel: 0, - zoomFactor: .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.okButton = buttonGroup.createButton({ - image: 'fa fa-check', - type: 'success', - hide: true, - editOnly: true, - }); - this.cancelButton = buttonGroup.createButton({ - image: 'fa fa-times', - type: 'danger', - hide: true - }); - - // Buttons click - 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.okButton.addEventListener('click', this.saveZoom.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)); - $(this.darkroom.canvas.wrapperEl).on('mousewheel', function(event){ - self.onMouseWheel(event); +/** +* 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); + } + }, }); - - //fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this)); - //fabric.util.addListener(fabric.document, 'keyup', this.onKeyUp.bind(this)); - 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(activate) { - if (activate === 'undefined') { - activate = !this.hasFocus(); - } - this.zoomButton.active(!activate); - this.inZoom = activate; - this.zoomInButton.hide(!activate); - this.zoomOutButton.hide(!activate); - this.okButton.hide(!activate); - this.cancelButton.hide(!activate); - this.darkroom.canvas.default_cursor = activate ? "move" : "default"; - }, - - // 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; - console.log('Setting zoom factor'); - console.log(zoomLevel); - console.log(point); - if (point) { - var canvas = this.darkroom.canvas; - canvas.zoomToPoint(point, zoomLevel + 1); // Add one for zero index - this.zoomLevel = zoomLevel; - } - return true; - }, - - getObjectBounds: function() { - var canvas = this.darkroom.canvas; - var objects = canvas.getObjects(); - var top = 0, bottom = 0, left = 0, right = 0; - for (var idx in objects) { - var obj = objects[idx]; - var objRight = obj.left + obj.getWidth(); - var objBottom = obj.top + obj.getHeight(); - if (obj.left < left) left = obj.left; - if (objRight > right) right = objRight; - if (obj.top < top) top = obj.top; - if (objBottom > bottom) bottom = objBottom; - } - return { - top: top, - bottom: bottom, - left: left, - right: right, - height: (bottom - top), - width: (right - left), - } - }, - - zoomIn: function() { - return this.setZoomLevel(this.zoomFactor, this.getCenterPoint()); - }, - - zoomOut: function() { - return this.setZoomLevel(-this.zoomFactor, this.getCenterPoint()); - }, - - 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(event) { - if (this.hasFocus()) { - this.panning = true; - } - }, - - onMouseUp: function(event) { - this.panning = false; - }, - - onMouseMove: function(event) { - if (this.panning && event && event.e) { - var delta = new fabric.Point(event.e.movementX, - event.e.movementY); - var canvas = this.darkroom.canvas; - var objBounds = this.getObjectBounds(); - var newPoint = new fabric.Point( - -delta.x - canvas.viewportTransform[4], - -delta.y - canvas.viewportTransform[5] - ) - if (newPoint.x < objBounds.left || newPoint.x > objBounds.right) { - return; - } - if (newPoint.y < objBounds.top || newPoint.y > objBounds.bottom) { - return; - } - canvas.absolutePan(newPoint); - //canvas.setCoords(); - } - }, - - }); - - } - - return {DarkroomPluginZoom: DarkroomPluginZoom}; + }; + return {DarkroomPluginZoom: DarkroomPluginZoom}; }); diff --git a/web_widget_darkroom/static/src/js/widget_darkroom.js b/web_widget_darkroom/static/src/js/widget_darkroom.js index 582f8997..c9814b58 100644 --- a/web_widget_darkroom/static/src/js/widget_darkroom.js +++ b/web_widget_darkroom/static/src/js/widget_darkroom.js @@ -1,238 +1,224 @@ -/* Copyright 2016 LasLabs Inc. - * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). - */ - -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 framework = require('web.framework'); - var crash_manager = require('web.crash_manager'); - - var QWeb = core.qweb; - var _t = core._t; - - 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, - - // Plugins options - plugins: { - crop: { - minHeight: 50, - minWidth: 50, - ratio: 1 +/** +* 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 = ''; + 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 = ''; + 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, + }; }, - }, - - // Post initialization method - initialize: function() { - // Active crop selection - // this.plugins['crop'].requireFocus(); - // Add custom listener - // this.addEventListener('core:transformation', function() { /* ... */ }); - } - - }, - - init: function(field_manager, node) { - this._super(field_manager, node); - this.options = _.defaults(this.options, this.defaults); - }, - - _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 = ''; - 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: function() { - if (!this.darkroom) { - this._init_darkroom_icons(); - this._init_darkroom_ui(); - this._init_darkroom_plugins(); - } - }, - - _init_darkroom_ui: function() { - - Darkroom.UI = { - Toolbar: Toolbar, - ButtonGroup: ButtonGroup, - Button: Button, - }; - - // Toolbar object. - function Toolbar(element) { - this.element = element; - } - - Toolbar.prototype = { - createButtonGroup: function(options) { - var buttonGroup = document.createElement('div'); - buttonGroup.className = 'darkroom-button-group'; - this.element.appendChild(buttonGroup); - - return new ButtonGroup(buttonGroup); - } - }; - - // 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: '', - }; - - options = Darkroom.Utils.extend(options, defaults); - - var buttonElement = document.createElement('button'); - buttonElement.type = 'button'; - buttonElement.className = 'darkroom-button darkroom-button-' + options.type; - buttonElement.innerHTML = ''; - if (options.editOnly) { - buttonElement.classList.add('oe_edit_only'); - } - if (options.addClass) { - buttonElement.classList.add(options.addClass); - } - // buttonElement.innerHTML = ''; - this.element.appendChild(buttonElement); - - var button = new Button(buttonElement); - button.hide(options.hide); - button.disable(options.disabled); - - return button; - } - } - - // 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); - } + + destroy_content: function() { + if (this.darkroom && this.darkroom.containerElement) { + this.darkroom.containerElement.remove(); + this.darkroom = null; + } }, - removeEventListener: function(eventName, listener) { - if (this.element.removeEventListener){ - this.element.removeEventListener(eventName, listener); - } + + set_value: function(value) { + return this._super(value); }, - active: function(value) { - if (value){ - this.element.classList.add('darkroom-button-active'); - this.element.disabled = false; - } else { - this.element.classList.remove('darkroom-button-active'); - this.element.disabled = true; - } + + 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; }, - hide: function(value) { - if (value) - this.element.classList.add('hidden'); - else - this.element.classList.remove('hidden'); + + commit_value: function() { + if (this.darkroom.sourceImage) { + this.set_value(this.darkroom.sourceImage.toDataURL().split(',')[1]); + } }, - disable: function(value) { - this.element.disabled = (value) ? true : false; - } - }; - - }, - - destroy_content: function() { - if (this.darkroom && this.darkroom.containerElement) { - this.darkroom.containerElement.remove(); - this.darkroom = null; - } - }, - - set_value: function(value){ - this.destroy_content(); - return this._super(value); - }, - - render_value: function() { - this._init_darkroom(); - var url; - 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(callback) { - this.set_value( - this.darkroom.sourceImage.toDataURL().split(',')[1] - ); - }, - - }); - - core.form_widget_registry.add("darkroom", FieldDarkroomImage); - - return { - FieldDarkroomImage: FieldDarkroomImage, - } - + }); + + core.form_widget_registry.add("darkroom", FieldDarkroomImage); + + return {FieldDarkroomImage: FieldDarkroomImage}; }); diff --git a/web_widget_darkroom/static/src/js/widget_darkroom.js.orig b/web_widget_darkroom/static/src/js/widget_darkroom.js.orig deleted file mode 100644 index 4b0f04f1..00000000 --- a/web_widget_darkroom/static/src/js/widget_darkroom.js.orig +++ /dev/null @@ -1,246 +0,0 @@ -/* © 2016-TODAY LasLabs Inc. - * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - */ - -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 framework = require('web.framework'); - var crash_manager = require('web.crash_manager'); - - var QWeb = core.qweb; - var _t = core._t; - - var FieldDarkroomImage = common.AbstractField.extend(common.ReinitializeFieldMixin, { - className: 'darkroom-widget', - template: 'FieldDarkroomImage', - placeholder: "/web/static/src/img/placeholder.png", - darkroom: null, - no_rerender: false, - - _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 = ''; - 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(); - require('web_widget_darkroom.darkroom_save').DarkroomPluginSave(); - }, - - _init_darkroom_ui: function() { - - this._init_darkroom_icons(); - - Darkroom.UI = { - Toolbar: Toolbar, - ButtonGroup: ButtonGroup, - Button: Button, - }; - - // Toolbar object. - function Toolbar(element) { - this.element = element; - } - - Toolbar.prototype = { - createButtonGroup: function(options) { - var buttonGroup = document.createElement('div'); - buttonGroup.className = 'darkroom-button-group'; - this.element.appendChild(buttonGroup); - - return new ButtonGroup(buttonGroup); - } - }; - - // 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: '', - }; - - options = Darkroom.Utils.extend(options, defaults); - - var buttonElement = document.createElement('button'); - buttonElement.type = 'button'; - buttonElement.className = 'darkroom-button darkroom-button-' + options.type; - buttonElement.innerHTML = ''; - if (options.editOnly) { - buttonElement.classList.add('oe_edit_only'); - } -<<<<<<< Updated upstream - if (options.addClass) { - buttonElement.classList.add(options.addClass); - } - // buttonElement.innerHTML = ''; -======= ->>>>>>> Stashed changes - this.element.appendChild(buttonElement); - - var button = new Button(buttonElement); - button.hide(options.hide); - button.disable(options.disabled); - - return button; - } - } - - // 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); - } - }, - active: function(value) { - if (value){ - this.element.classList.add('darkroom-button-active'); - this.element.disabled = false; - } else { - this.element.classList.remove('darkroom-button-active'); - this.element.disabled = true; - } - }, - hide: function(value) { - if (value) - this.element.classList.add('hidden'); - else - this.element.classList.remove('hidden'); - }, - disable: function(value) { - this.element.disabled = (value) ? true : false; - } - }; - - }, - - destroy_content: function() { - console.log('Destroying Darkroom Obj'); - this.darkroom.selfDestroy(); - }, - - render_value: function() { - console.log('Rerendering'); - var url; - 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); - - if (!this.darkroom) { - this._init_darkroom_ui(); - this._init_darkroom_plugins(); - } - this.darkroom = new Darkroom($img.get(0)); - this.darkroom.widget = this; - }, - - on_save_as: function(e) { - - framework.blockUI(); - var value = this.darkroom.sourceImage.toDataURL(); - var c = crash_manager; - var filename_fieldname = this.node.attrs.filename; - var filename_field = this.view.fields && this.view.fields[filename_fieldname]; - - var filereader = new FileReader(); - filereader.onload = function(upload) { - var data = upload.target.result; - data = data.split(',')[1]; - $.post({ - url: '/web/binary/upload', - - }) - }; - filereader.readAsDataURL(new Blob(value)); - - this.$el.find('form.o_form_darkroom_form input[name=ufile]').val(value); - this.$el.find('form.o_form_darkroom_form input[name=session_id]').val(this.session.session_id); - this.$el.find('form.o_form_darkroom_form').submit(); - - var $form = $(parentEl).find('form'); - $form.find('input[name=ufile]').val(value); - - }, - - init: function(field_manager, node) { - var self = this; - this._super(field_manager, node); - this.binary_value = false; - this.useFileAPI = !!window.FileReader; - this.max_upload_size = 25 * 1024 * 1024; // 25Mo - if (!this.useFileAPI) { - this.fileupload_id = _.uniqueId('oe_fileupload'); - $(window).on(this.fileupload_id, function() { - var args = [].slice.call(arguments).slice(1); - self.on_file_uploaded.apply(self, args); - }); - } - }, - stop: function() { - if (!this.useFileAPI) { - $(window).off(this.fileupload_id); - } - this._super.apply(this, arguments); - }, - - }); - - core.form_widget_registry.add("darkroom", FieldDarkroomImage); - - return { - FieldDarkroomImage: FieldDarkroomImage, - } - -}); diff --git a/web_widget_darkroom/static/src/js/widget_darkroom_modal.js b/web_widget_darkroom/static/src/js/widget_darkroom_modal.js new file mode 100644 index 00000000..118fd102 --- /dev/null +++ b/web_widget_darkroom/static/src/js/widget_darkroom_modal.js @@ -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); + } + }, + }); +}); diff --git a/web_widget_darkroom/static/src/less/darkroom.less b/web_widget_darkroom/static/src/less/darkroom.less new file mode 100755 index 00000000..1c8a1b85 --- /dev/null +++ b/web_widget_darkroom/static/src/less/darkroom.less @@ -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%; +} diff --git a/web_widget_darkroom/static/src/xml/field_templates.xml b/web_widget_darkroom/static/src/xml/field_templates.xml index 746426be..2692cb7e 100644 --- a/web_widget_darkroom/static/src/xml/field_templates.xml +++ b/web_widget_darkroom/static/src/xml/field_templates.xml @@ -1,17 +1,30 @@ - + -
+
+ + + + + + + + + + + + + + diff --git a/web_widget_darkroom/tests/__init__.py b/web_widget_darkroom/tests/__init__.py new file mode 100644 index 00000000..3773eddd --- /dev/null +++ b/web_widget_darkroom/tests/__init__.py @@ -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 diff --git a/web_widget_darkroom/tests/test_darkroom_modal.py b/web_widget_darkroom/tests/test_darkroom_modal.py new file mode 100644 index 00000000..f299939e --- /dev/null +++ b/web_widget_darkroom/tests/test_darkroom_modal.py @@ -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'}) diff --git a/web_widget_darkroom/views/assets.xml b/web_widget_darkroom/views/assets.xml index 7b57c7a1..87e6a18a 100644 --- a/web_widget_darkroom/views/assets.xml +++ b/web_widget_darkroom/views/assets.xml @@ -1,31 +1,28 @@