tui-image-editor
Advanced tools
Comparing version 3.2.2 to 3.3.0
@@ -20,2 +20,3 @@ module.exports = { | ||
'rules': { | ||
indent: [2, 4, {SwitchCase: 1, ignoreComments: false, ImportDeclaration: 1, flatTernaryExpressions: false}], | ||
'prefer-destructuring': ['error', { | ||
@@ -22,0 +23,0 @@ VariableDeclarator: {array: true, object: true}, |
@@ -40,5 +40,5 @@ { | ||
"resolutions": { | ||
"tui-code-snippet": "^1.4.0", | ||
"tui-code-snippet": "^1.5.0", | ||
"tui-color-picker": "^2.2.0" | ||
} | ||
} |
{ | ||
"name": "tui-image-editor", | ||
"author": "NHNEnt FE Development Lab <dl_javascript@nhnent.com>", | ||
"version": "3.2.2", | ||
"version": "3.3.0", | ||
"license": "MIT", | ||
@@ -70,5 +70,5 @@ "repository": { | ||
"core-js": "2.4.1", | ||
"tui-code-snippet": "^1.4.0", | ||
"tui-code-snippet": "^1.5.0", | ||
"tui-color-picker": "^2.2.0" | ||
} | ||
} |
@@ -7,2 +7,5 @@ # ![Toast UI ImageEditor](https://user-images.githubusercontent.com/35218826/40895380-0b9f4cd6-67ea-11e8-982f-18121daa3a04.png) | ||
## Wrappers | ||
- [toast-ui.vue-image-editor](https://github.com/nhnent/toast-ui.vue-image-editor): Vue wrapper component is powered by [NHN Entertainment](https://github.com/nhnent). | ||
![6 -20-2018 17-45-54](https://user-images.githubusercontent.com/35218826/41647896-7b218ae0-74b2-11e8-90db-d7805cc23e8c.gif) | ||
@@ -36,2 +39,3 @@ | ||
* [TOAST UI Family](#-toast-ui-family) | ||
* [Used By](#-used-by) | ||
* [License](#-license) | ||
@@ -333,3 +337,6 @@ | ||
## 🚀 Used By | ||
* [TOAST Dooray! - Collaboration Service (Project, Messenger, Mail, Calendar, Drive, Wiki, Contacts)](https://dooray.com/home/) | ||
## 📜 License | ||
[MIT LICENSE](https://github.com/nhnent/tui.image-editor/blob/master/LICENSE) |
@@ -323,2 +323,28 @@ import {extend} from 'tui-code-snippet'; | ||
this.ui.changeMenu('crop'); | ||
}, | ||
preset: presetType => { | ||
switch (presetType) { | ||
case 'preset-square': | ||
this.setCropzoneRect(1 / 1); | ||
break; | ||
case 'preset-3-2': | ||
this.setCropzoneRect(3 / 2); | ||
break; | ||
case 'preset-4-3': | ||
this.setCropzoneRect(4 / 3); | ||
break; | ||
case 'preset-5-4': | ||
this.setCropzoneRect(5 / 4); | ||
break; | ||
case 'preset-7-5': | ||
this.setCropzoneRect(7 / 5); | ||
break; | ||
case 'preset-16-9': | ||
this.setCropzoneRect(16 / 9); | ||
break; | ||
default: | ||
this.setCropzoneRect(); | ||
this.ui.crop.changeApplyButtonStatus(false); | ||
break; | ||
} | ||
} | ||
@@ -325,0 +351,0 @@ }, this._commonAction()); |
@@ -26,7 +26,12 @@ /** | ||
const prevImageHeight = prevImage ? prevImage.height : 0; | ||
const objects = graphics.removeAll(true).filter(objectItem => objectItem.type !== 'cropzone'); | ||
objects.forEach(objectItem => { | ||
objectItem.evented = true; | ||
}); | ||
this.undoData = { | ||
name: loader.getImageName(), | ||
image: prevImage, | ||
objects: graphics.removeAll(true) | ||
objects | ||
}; | ||
@@ -41,2 +46,3 @@ | ||
}, | ||
/** | ||
@@ -43,0 +49,0 @@ * @param {Graphics} graphics - Graphics instance |
@@ -5,2 +5,3 @@ /** | ||
*/ | ||
import snippet from 'tui-code-snippet'; | ||
import fabric from 'fabric/dist/fabric.require'; | ||
@@ -13,2 +14,8 @@ import Component from '../interface/component'; | ||
const MOUSE_MOVE_THRESHOLD = 10; | ||
const DEFAULT_OPTION = { | ||
top: -10, | ||
left: -10, | ||
height: 1, | ||
width: 1 | ||
}; | ||
@@ -283,2 +290,55 @@ /** | ||
/** | ||
* Set a cropzone square | ||
* @param {number} [presetRatio] - preset ratio | ||
*/ | ||
setCropzoneRect(presetRatio) { | ||
const canvas = this.getCanvas(); | ||
const cropzone = this._cropzone; | ||
canvas.deactivateAll(); | ||
canvas.selection = false; | ||
cropzone.remove(); | ||
cropzone.set(presetRatio ? this._getPresetCropSizePosition(presetRatio) : DEFAULT_OPTION); | ||
canvas.add(cropzone); | ||
canvas.selection = true; | ||
if (presetRatio) { | ||
canvas.setActiveObject(cropzone); | ||
} | ||
} | ||
/** | ||
* Set a cropzone square | ||
* @param {number} presetRatio - preset ratio | ||
* @returns {{left: number, top: number, width: number, height: number}} | ||
* @private | ||
*/ | ||
_getPresetCropSizePosition(presetRatio) { | ||
const canvas = this.getCanvas(); | ||
const originalWidth = canvas.getWidth(); | ||
const originalHeight = canvas.getHeight(); | ||
const standardSize = (originalWidth >= originalHeight) ? originalWidth : originalHeight; | ||
const getScale = (value, orignalValue) => (value > orignalValue) ? orignalValue / value : 1; | ||
let width = standardSize * presetRatio; | ||
let height = standardSize; | ||
const scaleWidth = getScale(width, originalWidth); | ||
[width, height] = snippet.map([width, height], sizeValue => sizeValue * scaleWidth); | ||
const scaleHeight = getScale(height, originalHeight); | ||
[width, height] = snippet.map([width, height], sizeValue => sizeValue * scaleHeight); | ||
return { | ||
top: (originalHeight - height) / 2, | ||
left: (originalWidth - width) / 2, | ||
width, | ||
height | ||
}; | ||
} | ||
/** | ||
* Keydown event handler | ||
@@ -285,0 +345,0 @@ * @param {KeyboardEvent} e - Event object |
@@ -556,2 +556,11 @@ /** | ||
/** | ||
* Get cropped rect | ||
* @param {Object} mode cropzone rect mode | ||
* @returns {Object} rect | ||
*/ | ||
setCropzoneRect(mode) { | ||
return this.getComponent(components.CROPPER).setCropzoneRect(mode); | ||
} | ||
/** | ||
* Get cropped image data | ||
@@ -558,0 +567,0 @@ * @param {Object} cropRect cropzone rect |
@@ -670,2 +670,11 @@ /** | ||
/** | ||
* Set the cropping rect | ||
* @param {Object} mode crop rect mode [1, 1.5, 1.3333333333333333, 1.25, 1.7777777777777777] | ||
* @returns {Object} {{left: number, top: number, width: number, height: number}} rect | ||
*/ | ||
setCropzoneRect(mode) { | ||
return this._graphics.setCropzoneRect(mode); | ||
} | ||
/** | ||
* Flip | ||
@@ -672,0 +681,0 @@ * @returns {Promise} |
@@ -0,1 +1,2 @@ | ||
import snippet from 'tui-code-snippet'; | ||
import Submenu from './submenuBase'; | ||
@@ -19,6 +20,10 @@ import templateHtml from './template/submenu/crop'; | ||
this.status = 'active'; | ||
this._els = { | ||
apply: this.selector('#tie-crop-button .apply'), | ||
cancel: this.selector('#tie-crop-button .cancel') | ||
cancel: this.selector('#tie-crop-button .cancel'), | ||
preset: this.selector('#tie-crop-preset-button') | ||
}; | ||
this.defaultPresetButton = this._els.preset.querySelector('.preset-none'); | ||
} | ||
@@ -31,2 +36,3 @@ | ||
* @param {Function} actions.cancel - cancel action | ||
* @param {Function} actions.preset - draw rectzone at a predefined ratio | ||
*/ | ||
@@ -44,2 +50,12 @@ addEvent(actions) { | ||
}); | ||
this._els.preset.addEventListener('click', event => { | ||
const button = event.target.closest('.tui-image-editor-button.preset'); | ||
if (button) { | ||
const [presetType] = button.className.match(/preset-[^\s]+/); | ||
this._setPresetButtonActive(button); | ||
this.actions.preset(presetType); | ||
} | ||
}); | ||
} | ||
@@ -59,2 +75,3 @@ | ||
this.actions.stopDrawingMode(); | ||
this._setPresetButtonActive(); | ||
} | ||
@@ -73,4 +90,19 @@ | ||
} | ||
/** | ||
* Set preset button to active status | ||
* @param {HTMLElement} button - event target element | ||
* @private | ||
*/ | ||
_setPresetButtonActive(button = this.defaultPresetButton) { | ||
snippet.forEach([].slice.call(this._els.preset.querySelectorAll('.preset')), presetButton => { | ||
presetButton.classList.remove('active'); | ||
}); | ||
if (button) { | ||
button.classList.add('active'); | ||
} | ||
} | ||
} | ||
export default Crop; |
@@ -36,2 +36,3 @@ export default ({ | ||
#tie-crop-button .tui-image-editor-button.apply.active label, | ||
#tie-crop-preset-button .tui-image-editor-button.preset.active label, | ||
#tie-shape-button.rect .tui-image-editor-button.rect label, | ||
@@ -38,0 +39,0 @@ #tie-shape-button.circle .tui-image-editor-button.circle label, |
@@ -0,4 +1,89 @@ | ||
export default ({iconStyle: {normal, active}}) => (` | ||
<ul class="tui-image-editor-submenu-item"> | ||
<li id="tie-crop-button" class="apply"> | ||
<li id="tie-crop-preset-button"> | ||
<div class="tui-image-editor-button preset preset-none active"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-shape-rectangle" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-shape-rectangle" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> Custom </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-square"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> Square </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-3-2"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> 3:2 </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-4-3"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> 4:3 </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-5-4"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> 5:4 </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-7-5"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> 7:5 </label> | ||
</div> | ||
<div class="tui-image-editor-button preset preset-16-9"> | ||
<div> | ||
<svg class="svg_ic-submenu"> | ||
<use xlink:href="${normal.path}#${normal.name}-ic-crop" | ||
class="normal"/> | ||
<use xlink:href="${active.path}#${active.name}-ic-crop" | ||
class="active"/> | ||
</svg> | ||
</div> | ||
<label> 16:9 </label> | ||
</div> | ||
</li> | ||
<li class="tui-image-editor-partition tui-image-editor-newline"> | ||
</li> | ||
<li class="tui-image-editor-partition only-left-right"> | ||
<div></div> | ||
</li> | ||
<li id="tie-crop-button" class="action"> | ||
<div class="tui-image-editor-button apply"> | ||
@@ -5,0 +90,0 @@ <svg class="svg_ic-menu"> |
@@ -132,3 +132,3 @@ /** | ||
sendHostname('image-editor'); | ||
sendHostname('image-editor', 'UA-129999381-1'); | ||
}, | ||
@@ -135,0 +135,0 @@ |
@@ -91,5 +91,5 @@ /** | ||
invoker.execute('testCommand', graphics, 1, 2, 3).then(() => | ||
invoker.execute('testCommand', graphics, 1, 2, 3).then(() => ( | ||
invoker.undo() | ||
).then(() => done() | ||
)).then(() => done() | ||
)['catch'](message => { | ||
@@ -157,6 +157,37 @@ fail(message); | ||
it('After running the LOAD_IMAGE command, existing objects should not include cropzone.', done => { | ||
const objCropzone = new fabric.Object({type: 'cropzone'}); | ||
invoker.execute(commands.ADD_OBJECT, graphics, objCropzone).then(() => { | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => { | ||
const lastUndoIndex = invoker._undoStack.length - 1; | ||
const savedObjects = invoker._undoStack[lastUndoIndex].undoData.objects; | ||
expect(savedObjects.length).toBe(0); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('`evented` attribute of the saved object must be true after LOAD_IMAGE.', done => { | ||
const objCircle = new fabric.Object({ | ||
type: 'circle', | ||
evented: false | ||
}); | ||
invoker.execute(commands.ADD_OBJECT, graphics, objCircle).then(() => { | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => { | ||
const lastUndoIndex = invoker._undoStack.length - 1; | ||
const [savedObject] = invoker._undoStack[lastUndoIndex].undoData.objects; | ||
expect(savedObject.evented).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
it('"undo()" should clear image if not exists prev image', done => { | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(graphics.getCanvasImage()).toBe(null); | ||
@@ -171,5 +202,5 @@ expect(graphics.getImageName()).toBe(''); | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'image', imageURL).then(() => ( | ||
invoker.execute(commands.LOAD_IMAGE, graphics, 'newImage', newImageURL) | ||
).then(() => { | ||
)).then(() => { | ||
expect(graphics.getImageName()).toBe('newImage'); | ||
@@ -217,5 +248,5 @@ expect(graphics.getCanvasImage().getSrc()).toContain(newImageURL); | ||
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX').then(() => | ||
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipX').then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(mockImage.flipX).toBe(originFlipX); | ||
@@ -229,5 +260,5 @@ done(); | ||
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY').then(() => | ||
invoker.execute(commands.FLIP_IMAGE, graphics, 'flipY').then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(mockImage.flipY).toBe(originFlipY); | ||
@@ -258,5 +289,5 @@ done(); | ||
invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 100).then(() => | ||
invoker.execute(commands.ROTATE_IMAGE, graphics, 'setAngle', 100).then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(mockImage.angle).toBe(originalAngle); | ||
@@ -296,5 +327,5 @@ done(); | ||
canvas.add.apply(canvasContext, objects); | ||
invoker.execute(commands.CLEAR_OBJECTS, graphics).then(() => | ||
invoker.execute(commands.CLEAR_OBJECTS, graphics).then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(canvas.contains(objects[0])).toBe(true); | ||
@@ -340,5 +371,5 @@ expect(canvas.contains(objects[1])).toBe(true); | ||
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(object)).then(() => | ||
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(object)).then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(canvas.contains(object)).toBe(true); | ||
@@ -351,5 +382,5 @@ done(); | ||
canvas.setActiveObject(group); | ||
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(group)).then(() => | ||
invoker.execute(commands.REMOVE_OBJECT, graphics, snippet.stamp(group)).then(() => ( | ||
invoker.undo() | ||
).then(() => { | ||
)).then(() => { | ||
expect(canvas.contains(object)).toBe(true); | ||
@@ -356,0 +387,0 @@ expect(canvas.contains(object2)).toBe(true); |
@@ -243,2 +243,70 @@ /** | ||
describe('"presets - setCropzoneRect()"', () => { | ||
beforeEach(() => { | ||
cropper.start(); | ||
}); | ||
afterEach(() => { | ||
cropper.end(); | ||
}); | ||
it('should return cropzone rect as a square', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(1 / 1); | ||
expect(cropper.getCropzoneRect().width).toBe(cropper.getCropzoneRect().height); | ||
}); | ||
it('should return cropzone rect as a 3:2 aspect box', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(3 / 2); | ||
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)) | ||
.toBe((3 / 2).toFixed(1)); | ||
}); | ||
it('should return cropzone rect as a 4:3 aspect box', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(4 / 3); | ||
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)) | ||
.toBe((4 / 3).toFixed(1)); | ||
}); | ||
it('should return cropzone rect as a 5:4 aspect box', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(5 / 4); | ||
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)) | ||
.toBe((5 / 4).toFixed(1)); | ||
}); | ||
it('should return cropzone rect as a 7:5 aspect box', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(7 / 5); | ||
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)) | ||
.toBe((7 / 5).toFixed(1)); | ||
}); | ||
it('should return cropzone rect as a 16:9 aspect box', () => { | ||
spyOn(cropper._cropzone, 'isValid').and.returnValue(true); | ||
cropper.setCropzoneRect(16 / 9); | ||
expect((cropper.getCropzoneRect().width / cropper.getCropzoneRect().height).toFixed(1)) | ||
.toBe((16 / 9).toFixed(1)); | ||
}); | ||
it('should remove cropzone of cropper when falsy is passed', () => { | ||
cropper.setCropzoneRect(); | ||
expect(cropper.getCropzoneRect()).toBeFalsy(); | ||
cropper.setCropzoneRect(0); | ||
expect(cropper.getCropzoneRect()).toBeFalsy(); | ||
cropper.setCropzoneRect(null); | ||
expect(cropper.getCropzoneRect()).toBeFalsy(); | ||
}); | ||
}); | ||
describe('"end()"', () => { | ||
@@ -245,0 +313,0 @@ it('should set cropzone of cropper to null', () => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
4716222
37084
340
Updatedtui-code-snippet@^1.5.0