24 |
25 |
26 | ```
27 |
28 | Finally you need to configure the plugin by updating your configuration object:
29 |
30 | ```js
31 | ...,
32 | {
33 | "class": "plugins/ImageLoader/ImageLoaderPlugin",
34 | "fileInputId": "my-image-loader-id"
35 | }
36 | ```
--------------------------------------------------------------------------------
/src/class/SvgColorator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SvgColorator module
3 | */
4 | define(function () {
5 |
6 | return class SvgColorator {
7 |
8 | /**
9 | * Color a canvas object (it should be a svg)
10 | */
11 | static color(canvasObject, color) {
12 |
13 | function isWhite(color) {
14 | return (
15 | color === 'rgb(255,255,255)' ||
16 | color === '#fff' ||
17 | color === '#ffffff' ||
18 | color === '#FFFFFF' ||
19 | color === '#FFF'
20 | );
21 | }
22 |
23 | if (canvasObject.isSameColor && canvasObject.isSameColor() || !canvasObject.paths) {
24 | canvasObject.setFill(color);
25 | } else if (canvasObject.paths) {
26 | for (var i = 0; i < canvasObject.paths.length; i++) {
27 | let path = canvasObject.paths[i];
28 | let filledColor = canvasObject.paths[i].fill;
29 | if (!isWhite(filledColor) || true === path.colored) {
30 | path.setFill(color);
31 | path.colored = true;
32 | }
33 | }
34 | }
35 | }
36 |
37 | }
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/src/plugins/OutputArea/README.md:
--------------------------------------------------------------------------------
1 | OutputArea Plugin
2 | =================
3 |
4 | This plugin allow you to output the canvas in svg in a textarea as soon as it is rendered
5 |
6 | First, add a textarea with the id of your choice.
7 |
8 | ```html
9 |
10 |
11 |
12 |
13 | Svg widget editor
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ```
27 |
28 | You just need to configure the plugin by updating your configuration object:
29 |
30 | ```js
31 | ...,
32 | 'output_area': {
33 | 'enable': true,
34 | 'texarea_id': 'my-output-area',
35 | 'enable_textarea_edition': false
36 | }
37 | ```
38 |
--------------------------------------------------------------------------------
/src/class/ImageReader/ImageReaderRegistry.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ImageReaderRegistry module
3 | */
4 | define(['./PlainImageReader', './SvgImageReader'], function (PlainImageReader, SvgImageReader) {
5 |
6 | return class ImageReaderRegistry {
7 |
8 | /**
9 | * Constructor
10 | */
11 | constructor() {
12 | this.readerMimeTypeMap = [
13 | {
14 | "mimeTypes": ["image/gif", "image/jpeg", "image/png"],
15 | "reader": new PlainImageReader()
16 | },
17 | {
18 | "mimeTypes": ["image/svg+xml"],
19 | "reader": new SvgImageReader()
20 | }
21 | ];
22 | }
23 |
24 | /**
25 | * Guess the image reader for the given mime type
26 | */
27 | guessImageReader(mimeType) {
28 | for (let i = 0, l = this.readerMimeTypeMap.length; i < l; i++) {
29 | let readerMimeType = this.readerMimeTypeMap[i];
30 | if (-1 !== readerMimeType.mimeTypes.indexOf(mimeType)) {
31 | return readerMimeType.reader;
32 | }
33 | }
34 |
35 | throw Error(`No reader found for mime type ${mimeType}`);
36 | }
37 | }
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/src/plugins/ColorPicker/README.md:
--------------------------------------------------------------------------------
1 | ColorPicker Plugin
2 | ==================
3 |
4 | This plugin allow to color the canvas svg images.
5 |
6 | First, add the jscolor library and an input with the class **jscolor** and the id of your choice. (Check http://jscolor.com/ for more information on this lib)
7 |
8 | ```html
9 |
10 |
11 |
12 |
13 | Svg widget editor
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ```
28 |
29 | Finally you need to configure the plugin by updating your configuration object:
30 |
31 | ```js
32 | ...,
33 | 'color_picker': {
34 | 'enable': true,
35 | 'input_id': 'my-color-picker-id'
36 | }
37 | ```
38 |
--------------------------------------------------------------------------------
/src/plugins/ImageDragAndDrop/README.md:
--------------------------------------------------------------------------------
1 | ImageDragAndDropper Plugin
2 | ==========================
3 |
4 | This plugin allow to drag and drop images to the canvas.
5 |
6 | First, add a container to the canvas and another one which will include all your images.
7 |
8 | ```html
9 |
10 |
11 |
12 |
13 | Svg widget editor
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | Finally you need to configure the plugin by updating your configuration object:
33 |
34 | ```js
35 | ...
36 | 'image_drag_and_drop': {
37 | 'enable': true,
38 | 'image_container_id': 'my-image-container-id'
39 | },
40 | ```
--------------------------------------------------------------------------------
/src/class/MimeTypeGuesser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Mime Type Guesser module
3 | */
4 | define(function () {
5 |
6 | return class MimeTypeGuesser {
7 |
8 | /**
9 | * Get the map of extension - mime type
10 | */
11 | static getExtensionMimeType() {
12 | return [
13 | {
14 | "extension": "svg",
15 | "mimeType": "image/svg+xml"
16 | },
17 | {
18 | "extension": "gif",
19 | "mimeType": "image/gif"
20 | },
21 | {
22 | "extension": "jpeg",
23 | "mimeType": "image/jpeg"
24 | },
25 | {
26 | "extension": "png",
27 | "mimeType": "image/png"
28 | },
29 | ]
30 | }
31 |
32 |
33 |
34 | /**
35 | * Guess the mime-type from the extension or filename
36 | */
37 | static guess(extension) {
38 | let extensionMimeTypeMap = MimeTypeGuesser.getExtensionMimeType();
39 | for (let i = 0, l = extensionMimeTypeMap.length; i < l; i++) {
40 | let extensionMimeType = extensionMimeTypeMap[i];
41 | if (extensionMimeType.extension === extension) {
42 | return extensionMimeType.mimeType;
43 | }
44 | }
45 |
46 | throw Error(`No mime type found for extension ${extension}`);
47 | }
48 | }
49 | });
50 |
--------------------------------------------------------------------------------
/assets/svg/vroum.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/plugins/ImageFlipper/README.md:
--------------------------------------------------------------------------------
1 | ImageFlipper Plugin
2 | ==================
3 |
4 | This plugin allow to flip the canvas svg images.
5 |
6 | First add inputs with the the ids of your choice.
7 |
8 | ```html
9 |
10 |
11 |
12 |
13 | Svg widget editor
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ```
30 |
31 | Finally you need to configure the plugin by updating your configuration object:
32 |
33 | ```js
34 | ...,
35 | 'image_flipper': {
36 | 'enable_horizontal_flip': true,
37 | 'enable_vertical_flip': true,
38 | 'horizontal_flip_input_id': 'horizontalflip-modal-button',
39 | 'vertical_flip_input_id': 'verticalflip-modal-button'
40 | },
41 | ```
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Get the filename
4 | */
5 | function getFilename(url) {
6 | return url.split('/').pop();
7 | }
8 |
9 | /**
10 | * Get the extension of the filename, path or url
11 | */
12 | function getExtension(string) {
13 | return string.split('.').pop();
14 | }
15 |
16 | /**
17 | * Get the current date
18 | */
19 | function getCurrentDate() {
20 | let currentDate = new Date();
21 | let day = ('0' + currentDate.getDate()).slice(-2);
22 | let month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
23 | let year = currentDate.getFullYear();
24 | let hours = currentDate.getHours();
25 | let minutes = ('0' + currentDate.getMinutes()).slice(-2);
26 |
27 | return day + "/" + month + "/" + year + ' ' + hours + 'h' + minutes;
28 | }
29 |
30 | /**
31 | * Merge a new object with a default one
32 | *
33 | * @param defaultObject
34 | * @param newObject
35 | * @returns {{}}
36 | */
37 | function mergeObjects(defaultObject, newObject) {
38 |
39 | for (var property in newObject) {
40 | try {
41 | // Property in destination object set; update its value.
42 | if (typeof newObject[property] == 'object' ) {
43 | defaultObject[property] = mergeObjects(defaultObject[property], newObject[property]);
44 | } else {
45 | defaultObject[property] = newObject[property];
46 | }
47 |
48 | } catch(e) {
49 | // Property in destination object not set; create it and set its value.
50 | defaultObject[property] = newObject[property];
51 | }
52 | }
53 |
54 | return defaultObject;
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/plugins/ObjectResizer/ObjectResizerPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ObjectResizerPlugin module
3 | */
4 | define(
5 | function () {
6 | return class ObjectResizerPlugin {
7 |
8 | /**
9 | * Constructor
10 | */
11 | constructor(canvas, config) {
12 | this.canvas = canvas;
13 | this.config = config;
14 | }
15 |
16 | /**
17 | * Check if the configuration is valid
18 | *
19 | * @return array
20 | */
21 | getConfigurationErrors() {
22 | let errors = [];
23 |
24 | if (typeof this.config.object_resizer === 'undefined') {
25 | errors.push('object_resizer must be defined');
26 | } else {
27 | if (typeof this.config.object_resizer.enable !== 'boolean') {
28 | errors.push('object_resizer.enable must be defined as a boolean');
29 | }
30 | }
31 |
32 | return errors;
33 | }
34 |
35 | /**
36 | * Start the plugin
37 | */
38 | start() {
39 | if (this.config.object_resizer.enable === true) {
40 | this.canvas.on('canvas:deserialized', (event) => {
41 | if (event.ratio) {
42 | let objects = this.canvas.getObjects();
43 | for (var i = 0; i < objects.length; i++) {
44 | let object = objects[i];
45 | object.scaleX = event.ratio * object.scaleX;
46 | object.scaleY = event.ratio * object.scaleY;
47 | object.top = event.ratio * object.top;
48 | object.left = event.ratio * object.left;
49 | object.setCoords();
50 | }
51 | }
52 | });
53 | }
54 | }
55 | }
56 | }
57 | );
58 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var gulp = require('gulp'),
4 | babel = require('gulp-babel'),
5 | optimize = require('gulp-requirejs-optimize'),
6 | chown = require('gulp-chown'),
7 | chmod = require('gulp-chmod'),
8 | concat = require('gulp-concat'),
9 | uglify = require('gulp-uglify')
10 | ;
11 |
12 | // Task to watch files
13 | gulp.task('watch', ['babel'], function() {
14 | gulp.watch('src/**/*.js', ['babel']);
15 | });
16 |
17 | // Task to watch files
18 |
19 | gulp.task('babel', function() {
20 | gulp
21 | .src('src/**/*.js')
22 | .pipe(babel({
23 | presets: ['es2015']
24 | }))
25 | .pipe(chown('www-data'))
26 | .pipe(chmod(750))
27 | .pipe(gulp.dest('lib'))
28 | ;
29 | });
30 |
31 | // task to build the project (one file output)
32 | gulp.task('build', ['babel'], function () {
33 | gulp
34 | .src('lib/init.js')
35 | .pipe(optimize({
36 | out:"svg-editor.min.js",
37 | optimize: 'uglify2',
38 | include: [
39 | "init.js",
40 | "utils.js",
41 | "plugins/ImageFlipper/ImageFlipperPlugin",
42 | "plugins/ImageLoader/ImageLoaderPlugin",
43 | "plugins/ManualSave/ManualSavePlugin",
44 | "plugins/AutoSave/AutoSavePlugin",
45 | "plugins/ObjectResizer/ObjectResizerPlugin",
46 | "plugins/ColorPicker/ColorPickerPlugin",
47 | "plugins/OutputArea/OutputAreaPlugin",
48 | "plugins/KeyboardListener/KeyboardListenerPlugin",
49 | "plugins/ImageDragAndDrop/ImageDragAndDropPlugin",
50 | "plugins/AutoImageResizer/AutoImageResizerPlugin",
51 | "plugins/RemoveObject/RemoveObjectPlugin"
52 | ]
53 | }))
54 | .pipe(chown('www-data'))
55 | .pipe(chmod(750))
56 | .pipe(gulp.dest('dist'))
57 | ;
58 | });
--------------------------------------------------------------------------------
/src/plugins/RemoveObject/RemoveObjectPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * OutputAreaPlugin module
3 | */
4 | define(
5 | function () {
6 |
7 | return class RemoveObjectPlugin {
8 |
9 | /**
10 | * Constructor
11 | */
12 | constructor(canvas, config) {
13 | this.canvas = canvas;
14 | this.config = config;
15 | }
16 |
17 | /**
18 | * Get the configuration errors
19 | *
20 | * @return array
21 | */
22 | getConfigurationErrors() {
23 | let errors = [];
24 |
25 | if (typeof this.config.remove_object === 'undefined') {
26 | errors.push('remove_object must be defined');
27 | } else {
28 | if (typeof this.config.remove_object.enable !== 'boolean') {
29 | errors.push('remove_object.enable must be defined as a boolean');
30 | } else {
31 | if (this.config.remove_object.enable === true) {
32 | if (typeof this.config.remove_object.input_id !== 'string') {
33 | errors.push('remove_object.input_id must be defined (as a string) because the plugin is enabled');
34 | } else {
35 | if (document.getElementById(this.config.remove_object.input_id) === null) {
36 | errors.push('No tag with id ' + this.config.remove_object.input_id + ' found');
37 | } else {
38 | this.removeObjectButton = document.getElementById(this.config.remove_object.input_id);
39 | }
40 | }
41 |
42 | }
43 | }
44 | }
45 |
46 | return errors;
47 | }
48 |
49 | /**
50 | * Start the plugin
51 | */
52 | start() {
53 | if (this.config.remove_object.enable === true) {
54 | this.removeObjectButton.onclick = () => {
55 | let element = this.canvas.getActiveObject();
56 | if (element) {
57 | element.remove();
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
64 | }
65 | );
66 |
--------------------------------------------------------------------------------
/assets/svg/dance.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/config/editor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Editor configuration
3 | */
4 | define(function () {
5 | return {
6 | 'persistence_manager': 'local_storage',
7 | 'local_storage_prefix': 'svg_editor_',
8 | 'serializer': 'json',
9 |
10 | // handles settings
11 | 'corner_shift': 'middle', // middle, in or out
12 | 'corner_size': 15,
13 | 'transparent_corners': true,
14 | 'corner_color': '#000000',
15 | 'border_color': '#000000',
16 |
17 | // cursor settings
18 | 'hoverCursor': 'move',
19 | 'moveCursor': 'move',
20 | 'defaultCursor': 'default',
21 | 'freeDrawingCursor': 'crosshair',
22 | 'rotationCursor': 'crosshair',
23 |
24 | // manual save default labels
25 | 'auto_save': {
26 | 'enable': false
27 | },
28 | 'auto_image_resizer': {
29 | 'enable': false
30 | },
31 | 'remove_object': {
32 | 'enable': false
33 | },
34 | 'color_picker': {
35 | 'enable': false
36 | },
37 | 'image_drag_and_drop': {
38 | 'enable': false
39 | },
40 | 'image_loader': {
41 | 'enable': false
42 | },
43 | 'image_flipper': {
44 | 'enable_horizontal_flip': false,
45 | 'enable_vertical_flip': false
46 | },
47 | 'keyboard_listener': {
48 | 'enable_delete_object': false,
49 | 'enable_move_object': false
50 | },
51 | 'output_area': {
52 | 'enable': false
53 | },
54 | 'object_resizer': {
55 | 'enable': true
56 | },
57 | 'manual_save': {
58 | 'enable': false,
59 | 'labels': {
60 | 'save': 'Save',
61 | 'save_this_project': 'Save this project',
62 | 'new_save': 'New save',
63 | 'override_save': 'Override',
64 | 'no_save_already': 'No save already',
65 | 'new_save_placeholder': 'Your title here...',
66 | 'load_project': 'Load a project',
67 | 'nothing_to_load': 'No projects to load',
68 | 'load': 'Load',
69 | 'close': 'Close',
70 | 'title_already_used': 'This title is already used',
71 | 'title_not_blank': 'The title cannot be blank',
72 | 'delete': 'Delete'
73 | }
74 | }
75 | };
76 | });
77 |
--------------------------------------------------------------------------------
/src/class/PersistenceManager/LocalStoragePersistenceManager.js:
--------------------------------------------------------------------------------
1 | /**
2 | * LocalStoragePersistanceManager module
3 | */
4 | define(['./AbstractPersistenceManager'], function (AbstractPersistenceManager) {
5 |
6 | return class LocalStoragePersistenceManager extends AbstractPersistenceManager {
7 |
8 | /**
9 | * Constructor
10 | */
11 | constructor(prefix) {
12 | super();
13 | this.prefix = prefix;
14 | }
15 |
16 | /**
17 | * Persist the canvas
18 | *
19 | * @param serializedCanvas: the canvas to be persisted
20 | * @param options
21 | */
22 | persist(serializedCanvas, options) {
23 | if (typeof serializedCanvas !== 'string') {
24 | console.error('Only strings should be stored in local storage')
25 | } else {
26 | localStorage.setItem(this.prefix + options.key, serializedCanvas);
27 | }
28 | }
29 |
30 | /**
31 | * Load the canvas
32 | *
33 | * @param options
34 | *
35 | * @return []: an array of the items which start by the key
36 | */
37 | load(options) {
38 | if (typeof options.key === 'undefined') {
39 | console.error('Load function missing argument: options.key');
40 | } else {
41 | // get all items with
42 | let items = [];
43 | for (let i = 0, len = localStorage.length; i < len; ++i) {
44 | if (localStorage.key(i).indexOf(this.prefix + options.key) === 0) {
45 | items.push(localStorage.getItem(localStorage.key(i)));
46 | }
47 | }
48 |
49 | return items;
50 | }
51 | }
52 |
53 | /**
54 | * Remove the item from local storage
55 | *
56 | * @param options
57 | */
58 | remove(options) {
59 | if (typeof options.key === 'undefined') {
60 | console.error('Remove function missing argument: options.key');
61 | } else {
62 | // get all items with
63 | let items = [];
64 | for (let i = 0, len = localStorage.length; i < len; ++i) {
65 | if (localStorage.key(i).indexOf(this.prefix + options.key) === 0) {
66 | localStorage.removeItem(localStorage.key(i));
67 | }
68 | }
69 |
70 | return items;
71 | }
72 | }
73 |
74 | }
75 |
76 | });
77 |
--------------------------------------------------------------------------------
/src/plugins/ManualSave/README.md:
--------------------------------------------------------------------------------
1 | ManualSave Plugin
2 | =================
3 |
4 | This plugin allow to manually save drawings. A save button trigger a modal which pops up and ask you for a title or to override an already save drawing. A load button allow you to choose a drawing to load among all saved drawings.
5 |
6 | This plugin uses the bootsrap modal. You first need to add the bootsrap and jquery libraries, as well as the 2 buttons with the id of your choice.
7 |
8 | ```html
9 |
10 |
11 |
12 | Svg widget editor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ```
30 |
31 | You just need to configure the plugin by updating your configuration object:
32 |
33 | ```js
34 | ...,
35 | 'manual_save': {
36 | 'enable': true,
37 | 'load_button_input_id': 'my-load-modal-button-id',
38 | 'save_button_input_d': 'my-save-modal-button-id',
39 | 'labels': {
40 | 'save': 'Save',
41 | 'save_this_project': 'Save this drawing',
42 | 'new_save': 'New save',
43 | 'override_save': 'Override',
44 | 'no_save_already': 'No save already',
45 | 'new_save_placeholder': 'Your title goes here...',
46 | 'load_project': 'Load a drawing',
47 | 'nothing_to_load': 'No drawings to load',
48 | 'load': 'Load',
49 | 'close': 'Close',
50 | 'title_already_used': 'This title is already used',
51 | 'title_not_blank': 'The title must be filled',
52 | 'delete': 'Delete'
53 | }
54 | },
55 | ```
56 |
--------------------------------------------------------------------------------
/src/plugins/AutoImageResizer/AutoImageResizerPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * AutoImageResizerPlugin module
3 | */
4 | define(
5 | function () {
6 |
7 | return class AutoImageResizerPlugin {
8 |
9 | /**
10 | * Constructor
11 | */
12 | constructor(canvas, config) {
13 | this.config = config;
14 | this.canvas = canvas;
15 | }
16 |
17 | /**
18 | * Get the configuration errors
19 | *
20 | * @return array
21 | */
22 | getConfigurationErrors() {
23 | let errors = [];
24 |
25 | if (typeof this.config.auto_image_resizer === 'undefined') {
26 | errors.push('auto_image_resizer must be defined');
27 | } else {
28 | if (typeof this.config.auto_image_resizer.enable !== 'boolean') {
29 | errors.push('auto_image_resizer.enable must be defined as a boolean');
30 | }
31 | }
32 |
33 | return errors;
34 | }
35 |
36 | /**
37 | * Start the plugin
38 | */
39 | start() {
40 | if (this.config.auto_image_resizer.enable === true) {
41 | this.canvas.on('object:added', (event) => {
42 | let object = event.target;
43 | let canvasWidth = parseInt(getComputedStyle(document.getElementById(this.config.canvas_id)).width);
44 | let canvasHeight = parseInt(getComputedStyle(document.getElementById(this.config.canvas_id)).height);
45 |
46 | if (object.width * object.scaleX > canvasWidth) { // the object is too large for the canvas so we resize it automatically
47 | let ratio = canvasWidth/object.width*0.90/object.scaleX;
48 | object.scaleX = ratio * object.scaleX;
49 | object.scaleY = ratio * object.scaleY;
50 | this.canvas.centerObject(object);
51 | object.setCoords();
52 | }
53 |
54 | if (object.height * object.scaleY > canvasHeight) { // the object is too large for the canvas so we resize it automatically
55 | let ratio = canvasHeight/object.height*0.90/object.scaleX;
56 | object.scaleX = ratio * object.scaleX;
57 | object.scaleY = ratio * object.scaleY;
58 | this.canvas.centerObject(object);
59 | object.setCoords();
60 | }
61 | });
62 | }
63 | }
64 | }
65 | }
66 | );
67 |
--------------------------------------------------------------------------------
/src/plugins/ImageLoader/ImageLoaderPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ImageLoaderPlugin module
3 | */
4 | define(
5 | [
6 | 'class/ImageReader/ImageReaderRegistry'
7 | ],
8 | function (ImageReaderRegistry) {
9 |
10 | return class ImageLoaderPlugin {
11 |
12 | /**
13 | * Constructor
14 | */
15 | constructor(canvas, config) {
16 | this.config = config;
17 | this.canvas = canvas;
18 | }
19 |
20 | /**
21 | * Get the configuration errors
22 | *
23 | * @return array
24 | */
25 | getConfigurationErrors() {
26 | let errors = [];
27 |
28 | if (typeof this.config.image_loader === 'undefined') {
29 | errors.push('image_loader must be defined');
30 | } else {
31 | if (typeof this.config.image_loader.enable !== 'boolean') {
32 | errors.push('image_loader.enable must be defined as a boolean');
33 | } else {
34 | if (this.config.image_loader.enable === true) {
35 | if (typeof this.config.image_loader.file_input_id !== 'string') {
36 | errors.push('image_loader.file_input_id must be defined (as a string) because the plugin is enabled');
37 | } else {
38 | if (document.getElementById(this.config.image_loader.file_input_id) === null) {
39 | errors.push('No tag with id ' + this.config.image_loader.file_input_id + ' found');
40 | } else {
41 | this.imageInput = document.getElementById(this.config.image_loader.file_input_id);
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
48 | return errors;
49 | }
50 |
51 | /**
52 | * Start the plugin
53 | */
54 | start() {
55 | if (this.config.image_loader.enable === true) {
56 | this.canvas.on('after:render', () => {
57 | this.imageInput.value = ""; // reset the file input to allow to add the same file several times
58 | });
59 |
60 | this.imageInput.onchange = (e) => {
61 | let file = e.target.files[0];
62 | let imageReaderRegistry = new ImageReaderRegistry();
63 | let imageReader = imageReaderRegistry.guessImageReader(file.type);
64 | imageReader.getCanvasImage(file, (item) => {
65 | this.canvas.centerObject(item);
66 | this.canvas.add(item);
67 | this.canvas.fire('object:newly-added', { target: item });
68 | });
69 | }
70 | }
71 | }
72 | }
73 |
74 | }
75 | );
76 |
--------------------------------------------------------------------------------
/docs/create_plugin.md:
--------------------------------------------------------------------------------
1 | Creating a plugin
2 | ------------------
3 |
4 | Here is the skeleton of a plugin. You need to wrap it by the requirejs define function so it can be loaded by the editor.
5 |
6 | ```js
7 | /**
8 | * MyAwesomePlugin module
9 | */
10 | define(
11 | function () {
12 |
13 | return class MyAwesomePlugin {
14 |
15 | /**
16 | * Constructor
17 | *
18 | * @param canvas : a fabric.Canvas() object
19 | * @param editorConfig : the configuration from the config/editor.js file
20 | */
21 | constructor(canvas, editorConfig) {
22 | this.canvas = canvas;
23 | this.config = editorConfig;
24 | }
25 |
26 | /**
27 | * Get the configuration errors
28 | * this function is used to check if the configuration is valid before the start() function is ran
29 | *
30 | * @return array
31 | */
32 | getConfigurationErrors() {
33 | let errors = [];
34 |
35 | if (typeof this.config.plugin_name === 'undefined') {
36 | errors.push('plugin_name must be defined');
37 | } else {
38 | if (this.config.plugin_name.enable !== 'boolean') {
39 | errors.push('plugin_name.enable must be defined as a boolean');
40 | } else {
41 | if (this.config.plugin_name.enable === true) {
42 | ... //additional configuration
43 | }
44 | }
45 | }
46 |
47 | return errors;
48 | }
49 |
50 | /**
51 | * Start the plugin
52 | */
53 | start() {
54 | if (this.config.plugin_name.enable === true) {
55 | // Your magic goes here.
56 | // With the configuration and the canvas, do whatever you want to add features on the canvas
57 | // canvas is an instance of fabric.Canvas. Check the fabricjs js docs at http://fabricjs.com/docs/
58 | }
59 | }
60 | }
61 | }
62 | );
63 | ```
64 |
65 | Finally you just need to register your plugin in the [config/plugin.js](src/config/plugin.js) file:
66 |
67 | ```js
68 | /**
69 | * Plugins configuration
70 | */
71 | define(function () {
72 | return [
73 | ... // other plugins
74 | ,{
75 | "class": "path/to/your/plugin/MyAwesomePlugin",
76 | "priority" : "3" // optional, it can be helpful when plugin depends on other plugins and must be loaded in a specific order. 1 is a higher priority than 2. No priority is equal to 9999.
77 | }
78 | ];
79 | });
80 | ```
81 |
82 | You can add **default configuration to your plugin** by editing the [config/editor.js](src/config/editor.js) file.
83 |
84 | Remember to add it also int the gulp file for the requirejs optimize task.
85 |
--------------------------------------------------------------------------------
/src/plugins/ColorPicker/ColorPickerPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ColorPickerPlugin module
3 | */
4 | define(
5 | [
6 | './SvgColorator'
7 | ],
8 | function (SvgColorator) {
9 |
10 | return class ColorPickerPlugin {
11 |
12 | /**
13 | * Constructor
14 | */
15 | constructor(canvas, config) {
16 |
17 | this.canvas = canvas;
18 | this.config = config;
19 | }
20 |
21 | /**
22 | * Get the configuration errors
23 | *
24 | * @return array
25 | */
26 | getConfigurationErrors() {
27 | let errors = [];
28 |
29 | if (typeof this.config.color_picker === 'undefined') {
30 | errors.push('color_picker must be defined');
31 | } else {
32 | if (typeof this.config.color_picker.enable !== 'boolean') {
33 | errors.push('color_picker.enable must be defined as a boolean');
34 | } else {
35 | if (this.config.color_picker.enable === true) {
36 | if (typeof this.config.color_picker.input_id !== 'string') {
37 | errors.push('color_picker.input_id must be defined (as a string) because the plugin is enabled');
38 | } else {
39 | if (document.getElementById(this.config.color_picker.input_id) === null) {
40 | errors.push('No tag with id ' + this.config.color_picker.input_id + ' found');
41 | } else {
42 |
43 | this.colorPicker = document.getElementById(this.config.color_picker.input_id);
44 | }
45 | }
46 |
47 | }
48 | }
49 | }
50 |
51 | return errors;
52 | }
53 |
54 | /**
55 | * Start the plugin
56 | */
57 | start() {
58 | if (this.config.color_picker.enable === true) {
59 | this.colorPicker.onchange = (e) => {
60 | let element = this.canvas.getActiveObject();
61 | if (element) {
62 | let color = '#' + e.target.value;
63 | SvgColorator.color(element, color);
64 | this.canvas.renderAll();
65 | }
66 | };
67 |
68 | this.canvas.on('object:selected', (event) => {
69 | let object = event.target;
70 | let color = SvgColorator.getColor(object);
71 | if (color !== null) {
72 | if (color.type === 'hexa') {
73 | this.colorPicker.jscolor.fromString(color.value);
74 | } else if (color.type === 'rgb') {
75 | this.colorPicker.jscolor.fromRGB(color.r, color.g, color.b);
76 | }
77 | }
78 | });
79 | }
80 | }
81 | }
82 | }
83 | );
84 |
--------------------------------------------------------------------------------
/src/plugins/OutputArea/OutputAreaPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * OutputAreaPlugin module
3 | */
4 | define(
5 | function () {
6 |
7 | return class OutputAreaPlugin {
8 |
9 | /**
10 | * Constructor
11 | */
12 | constructor(canvas, config) {
13 | this.canvas = canvas;
14 | this.config = config;
15 | }
16 |
17 | /**
18 | * Get the configuration errors
19 | *
20 | * @return array
21 | */
22 | getConfigurationErrors() {
23 | let errors = [];
24 |
25 | if (
26 | typeof this.config.output_area === 'undefined' ||
27 | typeof this.config.output_area.enable !== 'boolean'
28 | ) {
29 | errors.push('output_area.enable must be defined');
30 | }
31 |
32 | if (this.config.output_area.enable === true) {
33 | if (typeof this.config.output_area.texarea_id !== 'string') {
34 | errors.push('output_area.texarea_id must be defined because the plugin is enabled');
35 | } else {
36 | this.outputArea = document.getElementById(this.config.output_area.texarea_id);
37 | if (this.outputArea === null) {
38 | errors.push('No tag with id '+ this.config.output_area.texarea_id +' found');
39 | }
40 | }
41 | }
42 |
43 | return errors;
44 | }
45 |
46 | /**
47 | * Start the plugin
48 | */
49 | start() {
50 | if (this.config.output_area.enable === true) {
51 | this.canvas.on('after:render', () => {
52 | this.fillOutput();
53 | });
54 |
55 | this.fillOutput();
56 |
57 | if (true !== this.config.enable_textarea_edition) {
58 | this.outputArea.readOnly = true;
59 | }
60 |
61 | this.startOutputAreaListener();
62 | }
63 | }
64 |
65 | /**
66 | * Fill the output area with the canvas exported in svg
67 | */
68 | fillOutput() {
69 | this.outputArea.value = this.canvas.toSVG();
70 | }
71 |
72 | /**
73 | * Start a listener to fill the output area when the canvas is edited
74 | */
75 | startOutputAreaListener() {
76 | if (this.outputArea.addEventListener) {
77 | this.outputArea.addEventListener('input', (event) => {
78 | fabric.loadSVGFromString(event.target.value, (objects, options) => {
79 | this.canvas.off('after:render');
80 | this.canvas.clear();
81 | let object = fabric.util.groupSVGElements(objects, options);
82 | this.canvas.add(object);
83 | this.canvas.on('after:render', () => { this.fillOutput() });
84 | });
85 | }, false);
86 | } else if (this.outputArea.attachEvent) {
87 | this.outputArea.attachEvent('onpropertychange', () => {
88 | // IE-specific event handling code
89 | });
90 | }
91 | }
92 | }
93 |
94 | }
95 | );
96 |
--------------------------------------------------------------------------------
/src/plugins/ColorPicker/SvgColorator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SvgColorator module
3 | */
4 | define(function () {
5 |
6 | return class SvgColorator {
7 |
8 | /**
9 | * Check if a color is white
10 | *
11 | * @param color
12 | * @returns {boolean}
13 | */
14 | static isWhite(color) {
15 | return (
16 | color === 'rgb(255,255,255)' ||
17 | color === '#fff' ||
18 | color === '#ffffff' ||
19 | color === '#FFFFFF' ||
20 | color === '#FFF' ||
21 | color === null ||
22 | color === ''
23 | );
24 | }
25 |
26 | /**
27 | * Color a canvas object (it should be a svg)
28 | */
29 | static color(canvasObject, color) {
30 |
31 | if (!canvasObject.paths) {
32 | canvasObject.setFill(color);
33 | } else if (canvasObject.paths) {
34 | for (var i = 0; i < canvasObject.paths.length; i++) {
35 | let path = canvasObject.paths[i];
36 |
37 | if (!SvgColorator.isWhite(path.fill) || true === path.fillColored) {
38 | path.fill = color;
39 | path.fillColored = true;
40 | }
41 |
42 | if (!SvgColorator.isWhite(path.stroke) || true === path.strokeColored) {
43 | path.stroke = color;
44 | path.strokeColored = true;
45 | }
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * Format the color in an object with the type of color (ie: hexa or rgb)
52 | * To be used with jscolor
53 | *
54 | * rgb(100,25,33) -> {type: 'rgb', r: 100, g: 25, b:33}
55 | * #252525 -> {type: 'hexa', value: 252525}
56 | *
57 | * @param color
58 | * @return {}
59 | */
60 | static format(color) {
61 | let hexaRegex = /#([0-9a-fA-F]{6})/;
62 | let matches = hexaRegex.exec(color);
63 | if (matches !== null) {
64 | return {
65 | 'type': 'hexa',
66 | 'value': matches[1]
67 | }
68 | }
69 |
70 | let rgbRegex = /rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(,[0-9])?\)/;
71 | matches = rgbRegex.exec(color);
72 | if (matches !== null) {
73 | return {
74 | 'type': 'rgb',
75 | 'r': matches[1],
76 | 'g': matches[2],
77 | 'b': matches[3]
78 | }
79 | }
80 |
81 | console.error('Error formatting color ' + color);
82 |
83 | return null;
84 | }
85 |
86 | /**
87 | * Get the color of a canvas object (it should be a svg)
88 | */
89 | static getColor(canvasObject) {
90 |
91 | if (!canvasObject.paths) {
92 | return SvgColorator.format(canvasObject.getFill());
93 | } else if (canvasObject.paths) {
94 | for (var i = 0; i < canvasObject.paths.length; i++) {
95 | let path = canvasObject.paths[i];
96 |
97 | if (!SvgColorator.isWhite(path.fill) || path.fillColored) {
98 | return SvgColorator.format(path.fill);
99 | }
100 |
101 | if (!SvgColorator.isWhite(path.stroke) || true === path.strokeColored) {
102 | return SvgColorator.format(path.stroke);
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
109 | });
110 |
--------------------------------------------------------------------------------
/src/plugins/AutoSave/AutoSavePlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * AutoSavePlugin module
3 | */
4 | define(
5 | [
6 | 'class/PersistenceManager/PersistenceManagerRegistry',
7 | 'class/Serializer/SerializerRegistry'
8 | ],
9 | function (PersistenceManagerRegistry, SerializerRegistry) {
10 | return class AutoSavePlugin {
11 |
12 | /**
13 | * Constructor
14 | */
15 | constructor(canvas, config) {
16 | this.config = config;
17 | this.canvas = canvas;
18 | this.serializer = new SerializerRegistry().guessSerializer(config.serializer);
19 | this.persistenceManager = new PersistenceManagerRegistry(config).guessPersistenceManager(config.persistence_manager);
20 | }
21 |
22 | /**
23 | * Get the configuration errors
24 | *
25 | * @return array
26 | */
27 | getConfigurationErrors() {
28 | let errors = [];
29 |
30 | if (typeof this.config.auto_save === 'undefined') {
31 | errors.push('auto_save must be defined');
32 | } else {
33 | if (typeof this.config.auto_save.enable !== 'boolean') {
34 | errors.push('auto_save.enable must be defined as a boolean');
35 | }
36 | }
37 |
38 | return errors;
39 | }
40 |
41 | /**
42 | * Start the plugin
43 | */
44 | start() {
45 | if (this.config.auto_save.enable === true) {
46 | this.canvas.on('after:render', () => {
47 | this.saveProject();
48 | });
49 | this.loadProject();
50 | }
51 | }
52 |
53 | /**
54 | * Persist the project
55 | */
56 | saveProject() {
57 | // get the canvas container width and height for resizing on load
58 | let width = parseInt(getComputedStyle(document.getElementById(this.config.canvas_container_id)).width);
59 | let height = parseInt(getComputedStyle(document.getElementById(this.config.canvas_container_id)).height);
60 |
61 | let project = this.serializer.serialize({
62 | 'container-width': width,
63 | 'container-height': height,
64 | 'canvas': this.canvas
65 | });
66 |
67 | this.persistenceManager.persist(project, {key: 'autosave'});
68 | }
69 |
70 | /**
71 | * Load the project
72 | */
73 | loadProject() {
74 | let autosave = this.persistenceManager.load({key: 'autosave'});
75 | if (autosave.length > 0) {
76 | let project = JSON.parse(autosave);
77 | let serializedCanvas = this.serializer.serialize(project.canvas);
78 |
79 | // get the canvas container width and height for resizinganalytics
80 | let oldWidth = parseFloat(project["container-width"]);
81 | let newWidth = parseFloat(getComputedStyle(document.getElementById(this.config.canvas_container_id)).width);
82 |
83 | if (serializedCanvas) {
84 | this.serializer.deserialize(serializedCanvas, this.canvas, () => {
85 | let ratio = newWidth / oldWidth;
86 | this.canvas.trigger("canvas:deserialized", {"ratio": ratio}); // used by the ObjectResizer
87 | this.canvas.renderAll();
88 | });
89 | }
90 | }
91 | }
92 | }
93 | }
94 | );
95 |
--------------------------------------------------------------------------------
/src/plugins/KeyboardListener/KeyboardListenerPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * KeyboardListenerPlugin module
3 | */
4 | define(
5 | function () {
6 |
7 | return class KeyboardListenerPlugin {
8 |
9 | /**
10 | * Constructor
11 | */
12 | constructor(canvas, config) {
13 | this.canvas = canvas;
14 | this.config = config;
15 | }
16 |
17 |
18 | /**
19 | * Get the configuration errors
20 | *
21 | * @return array
22 | */
23 | getConfigurationErrors() {
24 | let errors = [];
25 | if (typeof this.config.keyboard_listener === 'undefined') {
26 | errors.push('keyboard_listener must be defined');
27 | } else {
28 | if (typeof this.config.keyboard_listener.enable_delete_object !== 'boolean') {
29 | errors.push('keyboard_listener.enable_delete_object must be defined as a boolean');
30 | }
31 |
32 | if (typeof this.config.keyboard_listener.enable_move_object !== 'boolean') {
33 | errors.push('keyboard_listener.enable_move_object must be defined as a boolean');
34 | }
35 | }
36 |
37 | return errors;
38 | }
39 |
40 | /**
41 | * Start the plugin
42 | */
43 | start() {
44 | document.addEventListener("keydown", (event) => {
45 |
46 | if (this.config.keyboard_listener.enable_delete_object === true) {
47 | let keyId = event.keyCode;
48 | // backspace -> 8
49 | // delete -> 46
50 | if (keyId === 46) {
51 | let element = this.canvas.getActiveObject();
52 | if (element) {
53 | element.remove();
54 | }
55 | }
56 | }
57 |
58 | if (this.config.keyboard_listener.enable_move_object === true) {
59 | let activeObject = this.canvas.getActiveObject();
60 | if (typeof activeObject !== 'undefined') {
61 | let arrowKeys = [37, 38, 39, 40];
62 | let keyId = event.keyCode;
63 | if (arrowKeys.indexOf(keyId) !== -1) {
64 | event.preventDefault();
65 | let newLeft = activeObject.left;
66 | let newTop = activeObject.top;
67 | switch(keyId) {
68 | case 37: // left arrow
69 | newLeft = newLeft - 5;
70 | activeObject.set({left: newLeft});
71 | this.canvas.renderAll();
72 | break;
73 | case 38: // up arrow
74 | newTop = newTop - 5;
75 | activeObject.set({top: newTop});
76 | this.canvas.renderAll();
77 | break;
78 | case 39: // right arrow
79 | newLeft = newLeft + 5;
80 | activeObject.set({left: newLeft});
81 | this.canvas.renderAll();
82 | break;
83 | case 40: // down arrow
84 | newTop = newTop + 5;
85 | activeObject.set({top: newTop});
86 | this.canvas.renderAll();
87 | }
88 | }
89 | }
90 | }
91 |
92 | });
93 | }
94 | }
95 | }
96 | );
97 |
--------------------------------------------------------------------------------
/src/class/FabricOverrider.js:
--------------------------------------------------------------------------------
1 | /**
2 | * FabricOverride module
3 | */
4 | define(function () {
5 |
6 | return class FabricOverrider {
7 |
8 | /**
9 | * Override the fabric object
10 | */
11 | static override(fabric, config) {
12 | fabric.Canvas.prototype.hoverCursor = config.hoverCursor;
13 | fabric.Canvas.prototype.moveCursor = config.moveCursor;
14 | fabric.Canvas.prototype.defaultCursor = config.defaultCursor;
15 | fabric.Canvas.prototype.freeDrawingCursor = config.freeDrawingCursor;
16 | fabric.Canvas.prototype.rotationCursor = config.rotationCursor;
17 |
18 | fabric.Object.prototype.set({
19 | transparentCorners: config.transparent_corners,
20 | borderColor: config.border_color,
21 | cornerColor: config.corner_color,
22 | cornerSize: config.corner_size,
23 |
24 | /**
25 | * Draws corners of an object's bounding box.
26 | * Requires public properties: width, height
27 | * Requires public options: cornerSize, padding
28 | * @param {CanvasRenderingContext2D} ctx Context to draw on
29 | * @return {fabric.Object} thisArg
30 | * @chainable
31 | */
32 | drawControls: function(ctx) {
33 | let shift;
34 | switch(config.corner_shift) {
35 | case 'out':
36 | shift = -config.corner_size/2;
37 | break;
38 | case 'in':
39 | shift = config.corner_size/2;
40 | break;
41 | default:
42 | shift = 0;
43 | }
44 |
45 | if (!this.hasControls) {
46 | return this;
47 | }
48 | var wh = this._calculateCurrentDimensions(),
49 | width = wh.x,
50 | height = wh.y,
51 | scaleOffset = this.cornerSize,
52 | left = -(width + scaleOffset) / 2,
53 | top = -(height + scaleOffset) / 2,
54 | methodName = this.transparentCorners ? 'stroke' : 'fill';
55 | ctx.save();
56 | ctx.strokeStyle = ctx.fillStyle = this.cornerColor;
57 | if (!this.transparentCorners) {
58 | ctx.strokeStyle = this.cornerStrokeColor;
59 | }
60 | this._setLineDash(ctx, this.cornerDashArray, null);
61 | // top-left
62 | this._drawControl('tl', ctx, methodName,
63 | left + shift,
64 | top + shift);
65 | // top-right
66 | this._drawControl('tr', ctx, methodName,
67 | left + width - shift,
68 | top + shift);
69 | // bottom-left
70 | this._drawControl('bl', ctx, methodName,
71 | left + shift,
72 | top + height - shift);
73 | // bottom-right
74 | this._drawControl('br', ctx, methodName,
75 | left + width - shift,
76 | top + height - shift);
77 | if (!this.get('lockUniScaling')) {
78 | // middle-top
79 | this._drawControl('mt', ctx, methodName,
80 | left + width/2,
81 | top + shift);
82 | // middle-bottom
83 | this._drawControl('mb', ctx, methodName,
84 | left + width/2,
85 | top + height - shift);
86 | // middle-right
87 | this._drawControl('mr', ctx, methodName,
88 | left + width - shift,
89 | top + height/2);
90 | // middle-left
91 | this._drawControl('ml', ctx, methodName,
92 | left + shift,
93 | top + height/2);
94 | }
95 | // middle-top-rotate
96 | if (this.hasRotatingPoint) {
97 | this._drawControl('mtr', ctx, methodName,
98 | left + width / 2,
99 | top - this.rotatingPointOffset);
100 | }
101 | ctx.restore();
102 | return this;
103 | }
104 | });
105 | }
106 | }
107 | });
108 |
--------------------------------------------------------------------------------
/src/plugins/ImageFlipper/ImageFlipperPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ImageFlipperPlugin module
3 | */
4 | define(
5 | function () {
6 | return class ImageFlipperPlugin {
7 |
8 | /**
9 | * Constructor
10 | *
11 | * @param canvas : a fabric.Canvas() object
12 | * @param config : a configuration object
13 | */
14 | constructor(canvas, config) {
15 | this.canvas = canvas;
16 | this.config = config;
17 | }
18 |
19 | /**
20 | * Get the configuration errors
21 | *
22 | * @return array
23 | */
24 | getConfigurationErrors() {
25 |
26 | let errors = [];
27 |
28 | if (
29 | typeof this.config.image_flipper === 'undefined'
30 | ) {
31 | errors.push('image_flipper must be defined');
32 | } else {
33 |
34 | if (typeof this.config.image_flipper.enable_horizontal_flip !== 'boolean') {
35 | errors.push('image_flipper.enable_horizontal_flip must be defined as a boolean');
36 | } else {
37 | if (this.config.image_flipper.enable_horizontal_flip === true) {
38 | if (typeof this.config.image_flipper.horizontal_flip_input_id !== 'string') {
39 | errors.push('image_flipper.horizontal_flip_input_id must be defined (as a string) because the enable_horizontal_flip parameter is set to true');
40 | } else {
41 | this.horizontalInput = document.getElementById(this.config.image_flipper.horizontal_flip_input_id);
42 | if (this.horizontalInput === null) {
43 | errors.push('No tag with id ' + this.config.image_flipper.horizontal_flip_input_id + ' found');
44 | }
45 | }
46 | }
47 | }
48 |
49 | if (typeof this.config.image_flipper.enable_vertical_flip !== 'boolean') {
50 | errors.push('image_flipper.enable_vertical_flip must be defined as a boolean');
51 | } else {
52 | if (this.config.image_flipper.enable_vertical_flip === true) {
53 | if (typeof this.config.image_flipper.vertical_flip_input_id !== 'string') {
54 | errors.push('image_flipper.vertical_flip_input_id must be defined (as a string) because the enable_vertical_flip parameter is set to true');
55 | } else {
56 | this.verticalInput = document.getElementById(this.config.image_flipper.vertical_flip_input_id);
57 | if (this.verticalInput === null) {
58 | errors.push('No tag with id '+ this.config.image_flipper.vertical_flip_input_id +' found');
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | return errors;
66 | }
67 |
68 | /**
69 | * Start the plugin
70 | */
71 | start() {
72 | if (this.config.image_flipper.enable_horizontal_flip === true) {
73 | this.horizontalInput.onclick = () => {
74 | if (null !== this.canvas.getActiveObject()) {
75 | if (this.canvas.getActiveObject().get('flipX')) {
76 | this.canvas.getActiveObject().set('flipX', false);
77 | } else {
78 | this.canvas.getActiveObject().set('flipX', true);
79 | }
80 |
81 | this.canvas.renderAll();
82 | }
83 | }
84 | }
85 |
86 | if (this.config.image_flipper.enable_vertical_flip === true) {
87 | this.verticalInput.onclick = () => {
88 | if (null !== this.canvas.getActiveObject()) {
89 | if (this.canvas.getActiveObject().get('flipY')) {
90 | this.canvas.getActiveObject().set('flipY', false);
91 | } else {
92 | this.canvas.getActiveObject().set('flipY', true);
93 | }
94 |
95 | this.canvas.renderAll();
96 | }
97 | }
98 | }
99 | }
100 | }
101 | }
102 | );
103 |
104 |
--------------------------------------------------------------------------------
/src/class/SvgEditor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SvgEditor module
3 | */
4 | define(['./FabricOverrider', '../config/plugins', '../config/editor'], function (FabricOverrider, plugins, editorDefaultConfiguration) {
5 |
6 | return class SvgEditor {
7 |
8 | /**
9 | * Constructor
10 | */
11 | constructor() {
12 | this.editorConfig = SvgEditor.getConfiguration(editorDefaultConfiguration);
13 | FabricOverrider.override(fabric, this.editorConfig);
14 | this.canvas = new fabric.Canvas(this.editorConfig.canvas_id);
15 | this.pluginsConfig = plugins;
16 | }
17 |
18 | /**
19 | * Start the svg editor
20 | */
21 | init() {
22 | this.canvas.on('object:moving', (e) => { e.target.bringToFront(); });
23 | this.loadPlugins();
24 | }
25 |
26 | /**
27 | * Trigger the ready function
28 | */
29 | triggerReadyFunction() {
30 | let script = document.querySelector('script[data-editor-ready-function]');
31 | if (script !== null) {
32 | let readyFunctionName = script.getAttribute('data-editor-ready-function');
33 |
34 | if (readyFunctionName !== null) {
35 | let readyFunction = window[readyFunctionName];
36 | if (typeof readyFunction === "function") {
37 | readyFunction(this.canvas);
38 | } else {
39 | throw new Error('The function ' + readyFunctionName + ' declared with the data-editor-ready-function attribute is not defined');
40 | }
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * Load the plugins from the configuration (check the parameters then start the plugins)
47 | */
48 | loadPlugins() {
49 |
50 | SvgEditor.sortPluginsByPriority(this.pluginsConfig);
51 |
52 | for (let i=0; i < this.pluginsConfig.length; i++) {
53 | let pluginConfig = this.pluginsConfig[i];
54 | if (typeof pluginConfig['class'] === 'undefined') {
55 | throw new Error('Could not load the plugin at position '+i+' in the plugins.js file. The \'class\' parameter must be defined');
56 | } else {
57 | require([pluginConfig['class']], (Plugin) => {
58 | let plugin = new Plugin(this.canvas, this.editorConfig);
59 | if (typeof plugin.start !== 'function' || typeof plugin.getConfigurationErrors !== 'function') {
60 | throw new Error('start() and getConfigurationErrors() functions must be implemented for the plugin ' + pluginConfig['class']);
61 | } else {
62 | let errors = plugin.getConfigurationErrors();
63 | if (errors.length === 0) {
64 | plugin.start();
65 | } else {
66 | let message = 'The plugin ' + pluginConfig['class'] +' does not have a valid configuration';
67 | for (let i = 0; i < errors.length; i++) {
68 | message += '\n - '+errors[i];
69 | }
70 |
71 | throw new Error(message);
72 | }
73 | }
74 | });
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * Get the user configuration
81 | *
82 | * @return {}
83 | */
84 | static getConfiguration(defaultConfiguration) {
85 | let script = document.querySelector('script[data-configuration-variable]');
86 | if (script === null) {
87 | throw new Error('The data-configuration-variable is missing on the require.js script tag');
88 | }
89 |
90 | let configurationVariableName = script.getAttribute('data-configuration-variable');
91 | let editorConfig = window[configurationVariableName];
92 | if (typeof editorConfig === 'undefined') {
93 | throw new Error('The variable ' + configurationVariableName + ' is not accessible');
94 | }
95 |
96 | if (typeof editorConfig.canvas_id === 'undefined') {
97 | throw new Error('The canvasId must be present in the configuration');
98 | } else {
99 | if (document.getElementById(editorConfig.canvas_id) === null) {
100 | throw new Error('No canvas with id '+ editorConfig.canvas_id +' found');
101 | }
102 | }
103 |
104 | if (typeof editorConfig.canvas_container_id === 'undefined') {
105 | throw new Error('The canvas_container_id must be present in the configuration (the canvas must be wrapped in a div with an id)');
106 | } else {
107 | if (document.getElementById(editorConfig.canvas_container_id) === null) {
108 | throw new Error('No canvas container with id '+ editorConfig.canvas_container_id +' found');
109 | }
110 | }
111 |
112 | return mergeObjects(defaultConfiguration, editorConfig);
113 | }
114 |
115 | /**
116 | * Sort the plugins in the config by priority
117 | * The plugins wil be loaded according to their priority
118 | *
119 | * @param config
120 | * @return config
121 | */
122 | static sortPluginsByPriority(config) {
123 | config.sort(function(a, b){
124 | if (typeof a.priority !== 'number') {
125 | a.priority = 9999;
126 | }
127 |
128 | if (typeof b.priority !== 'number') {
129 | b.priority = 9999;
130 | }
131 |
132 | if(a.priority > b.priority) return 1;
133 | if(a.priority < b.priority) return -1;
134 |
135 | return 0;
136 | });
137 | }
138 | }
139 | });
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Svg editor
2 | ==========
3 |
4 | A simple editor widget to create svg composed images. [Check out the live demo](https://idci-consulting.github.io/SvgEditor/).
5 | This editor is based on [fabricjs](https://github.com/kangax/fabric.js/). It is merely composed of a canvas with plugins adding new features on it.
6 |
7 | Getting started
8 | ---------------
9 |
10 | This svg editor must run in a browser that support es6. If not, use [gulp](http://gulpjs.com/) to build the javascript file with babel (gulp build task).
11 | You'll need either **docker** and **docker-compose**, or **node** and **npm** along with **gulp-cli**.
12 | The gulp build command create a **lib/** directory with built scripts in it. You must update the data-main attribute of the requirejs script with the right path of your script.
13 |
14 | ### With docker
15 |
16 | Run the following command:
17 |
18 | ```
19 | docker-compose up -d && docker exec -it svgeditor_app_1 npm install
20 | ```
21 |
22 | Then browse [http://localhost:8030](http://localhost:8030).
23 |
24 | ### On your own setup
25 |
26 | ```
27 | npm install
28 | npm install --global gulp-cli
29 | ```
30 |
31 | ### Build
32 |
33 | If you want a single minified file, run **gulp build**. It will create the dist/svg-editor.min.js file.
34 | If you want to add new plugins, edit the gulpfile at line 49 accordingly to tell the requirejs optimizer to include them.
35 |
36 | Usage
37 | -----
38 |
39 | ### Minimal setup
40 |
41 | Here is the minimal html you need to get the editor working.
42 | You **must** create a configuration object whose name is defined with the **data-configuration-variable** attribute:
43 |
44 | ```html
45 |
46 |
47 |
48 |
49 | Svg widget editor
50 |
51 |
52 |
53 |
54 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | ```
69 |
70 | Without plugins, this editor is not really useful... Check any of the plugins in the `plugins\` directory.
71 | Read plugins documentation for more details on how to install and configure them.
72 | If you want all plugins available and installed right away, just clone the project and start it as it is.
73 |
74 | ### Use fabricjs on editor ready
75 |
76 | You can use the fabricjs canvas object once the editor is loaded by adding a function whose name is specified with the **data-editor-ready-function** attribute, on the script that load requirejs. Let's say you want to hide a button when no object is selected:
77 |
78 | ```html
79 |
85 |
96 | ```
97 |
98 | Plugin reference
99 | ----------------
100 |
101 | **All plugins are disabled by default**
102 |
103 | * [ImageFlipperPlugin](src/plugins/ImageFlipper/README.md): Flip images vertically or horizontally
104 | * [ImageLoaderPlugin](src/plugins/ImageLoader/README.md): Load images to the canvas thanks to a file input
105 | * [ObjectResizerPlugin](src/plugins/ObjectResizer/README.md): Resize the canvas objects. Used only to add 'responsivness' to the canvas
106 | * [ColorPickerPlugin](src/plugins/ColorPicker/README.md)ColorPickerPlugin: Color a selected svg in the canvas thank to the [jscolor picker](http://jscolor.com/)
107 | * [OutputAreaPlugin](src/plugins/OutputArea/README.md): Output the final svg in a textarea
108 | * [KeyboardListenerPlugin](src/plugins/KeyboardListener/README.md): Add features to the canvas to move/delete selected objects with the keyboard
109 | * [ImageDragAndDropPlugin](src/plugins/ImageDragAndDrop/README.md): Drag and drop images to the canvas
110 | * [AutoImageResizerPlugin](src/plugins/AutoImageResizer/README.md): Automatically resize images when they are bigger than the canvas
111 | * [RemoveObjectPlugin](src/plugins/RemoveObject/README.md): Delete selected object thank to a button
112 | * [ManualSavePlugin](src/plugins/ManualSave/README.md): Display a modal that will be used to load / save the canvas in local storage
113 | * [AutoSavePlugin](src/plugins/AutoSave/README.md): Automatically save the plugin each time the canvas is rendered in local storage
114 |
115 | Improve the editor
116 | ------------------
117 |
118 | See how to:
119 |
120 | * [Create a plugin](docs/create_plugin.md) to add features to the canvas
121 | * Add a new persistence manager to save data wherever you want (TODO)
--------------------------------------------------------------------------------
/assets/svg/miaou.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Svg editor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
90 |
96 |
108 |
109 |
110 | Select an svg object and delete it
111 |
112 |
113 | Drag and drop the images below
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | Select an svg object and color it
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/src/plugins/ImageDragAndDrop/ImageDragAndDropPlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ImageDragAndDropperPlugin module
3 | *
4 | * See http://jsfiddle.net/Ahammadalipk/w8kkc/185/
5 | */
6 | define(
7 | [
8 | 'class/ImageReader/ImageReaderRegistry',
9 | 'class/FileDownloader/FileDownloaderRegistry',
10 | 'class/MimeTypeGuesser'
11 | ],
12 | function (ImageReaderRegistry, FileDownloaderRegistry, MimeTypeGuesser) {
13 |
14 | return class ImageDragAndDropperPlugin {
15 |
16 | /**
17 | * Constructor
18 | */
19 | constructor(canvas, config) {
20 | this.imageReaderRegistry = new ImageReaderRegistry();
21 | this.fileDownloaderRegistry = new FileDownloaderRegistry();
22 | this.canvas = canvas;
23 | this.config = config;
24 | }
25 |
26 | /**
27 | * Get the configuration errors
28 | *
29 | * @return array
30 | */
31 | getConfigurationErrors() {
32 | let errors = [];
33 |
34 | if (typeof this.config.image_drag_and_drop === 'undefined') {
35 | errors.push('image_drag_and_drop must be defined');
36 | } else {
37 | if (typeof this.config.image_drag_and_drop.enable !== 'boolean') {
38 | errors.push('image_drag_and_drop.enable must be defined as a boolean');
39 | } else {
40 | if (this.config.image_drag_and_drop.enable === true) {
41 | if (typeof this.config.image_drag_and_drop.image_container_id !== 'string') {
42 | errors.push('image_drag_and_drop.image_container_id must be defined (as a string) because the plugin is enabled');
43 | } else {
44 | if (document.getElementById(this.config.image_drag_and_drop.image_container_id) === null) {
45 | errors.push('No tag with id ' + this.config.image_drag_and_drop.image_container_id + ' found');
46 | } else {
47 | this.images = document.querySelectorAll('#'+this.config.image_drag_and_drop.image_container_id+' img');
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 | return errors;
55 | }
56 |
57 | /**
58 | * Start the plugin
59 | */
60 | start() {
61 | if (this.config.image_drag_and_drop.enable === true) {
62 | // drag and drop html5 detection
63 | if (!('draggable' in document.createElement('span'))) {
64 | console.error('HTML5 Drag and drop is not supported by your browser');
65 | return;
66 | }
67 |
68 | for (let i = 0, len = this.images.length; i < len; i++) {
69 | // drag and drop for desktop
70 | this.images[i].addEventListener('dragstart', event => this.handleDragStart(event), false);
71 | this.images[i].addEventListener('dragend', event => this.handleDragEnd(event), false);
72 |
73 | // mobile touch support
74 | this.images[i].addEventListener('touchstart', event => this.handleTouchStart(event), false);
75 | }
76 |
77 | let canvasContainer = document.getElementById(this.config.canvas_container_id);
78 | canvasContainer.addEventListener('dragenter', event => this.handleDragEnter(event), false);
79 | canvasContainer.addEventListener('dragover', event => this.handleDragOver(event), false);
80 | canvasContainer.addEventListener('dragleave', event => this.handleDragLeave(event), false);
81 | canvasContainer.addEventListener('drop', event => this.handleDrop(event), false);
82 | }
83 | }
84 |
85 | /**
86 | * Add an image to the canvas
87 | *
88 | * @param imageUrl
89 | * @param callback
90 | */
91 | downloadImage(imageUrl, callback) {
92 | this.fileDownloaderRegistry.guessFileDownloader('blob').downloadFile(imageUrl, (blob) => {
93 | let filename = getFilename(imageUrl);
94 | let file = null;
95 |
96 | try {
97 | file = new File([blob], filename);
98 | } catch (e) {
99 | // IE does not support the File constructor
100 | blob.name = filename;
101 | file = blob;
102 | }
103 |
104 | return callback(file);
105 | });
106 | }
107 |
108 | /**
109 | * Function triggered on touch start
110 | *
111 | * @param event
112 | */
113 | handleTouchStart(event) {
114 | let imageUrl = event.target.currentSrc;
115 | let fileMimeType = MimeTypeGuesser.guess(getExtension(imageUrl));
116 | let imageReader = this.imageReaderRegistry.guessImageReader(fileMimeType);
117 |
118 | // We can't read the file if it's not on the computer of the client
119 | // We need to download it before so we can use our imageReader
120 | this.downloadImage(imageUrl, (file) => {
121 | imageReader.getCanvasImage(file, (item) => {
122 | this.canvas.centerObject(item);
123 | this.canvas.add(item);
124 | this.canvas.fire('object:newly-added', { target: item });
125 | });
126 | });
127 | }
128 |
129 | /**
130 | * Function triggered on drag over
131 | */
132 | handleDragOver(event) {
133 | if (event.preventDefault) {
134 | event.preventDefault(); // Necessary. Allows us to drop.
135 | }
136 |
137 | event.dataTransfer.dropEffect = 'copy'; // See the section on the DataTransfer object.
138 |
139 | return false;
140 | }
141 |
142 | /**
143 | * Function triggered on drop
144 | */
145 | handleDrop(event) {
146 | if(event.preventDefault) {
147 | event.preventDefault();
148 | }
149 | if (event.stopPropagation) {
150 | event.stopPropagation(); // stops the browser from redirecting.
151 | }
152 |
153 | let imageUrl = document.querySelector('#'+this.config.image_drag_and_drop.image_container_id+' img.img_dragging').src;
154 | let fileMimeType = MimeTypeGuesser.guess(getExtension(imageUrl));
155 | let imageReader = this.imageReaderRegistry.guessImageReader(fileMimeType);
156 |
157 | // We can't read the file if it's not on the computer of the client
158 | // We need to download it before so we can use our imageReader
159 | this.downloadImage(imageUrl, (file) => {
160 | imageReader.getCanvasImage(file, (item) => {
161 | item.left = event.layerX;
162 | item.top = event.layerY;
163 | this.canvas.add(item);
164 | this.canvas.fire('object:newly-added', { target: item });
165 | });
166 | });
167 |
168 | return false;
169 | }
170 |
171 | /**
172 | * Function triggered on drag enter
173 | */
174 | handleDragEnter(event) {
175 | event.target.classList.add('over');
176 | }
177 |
178 | /**
179 | * Function triggered on drag leave
180 | */
181 | handleDragLeave(event) {
182 | event.target.classList.remove('over');
183 | }
184 |
185 | /**
186 | * Function triggered on drag start
187 | */
188 | handleDragStart(event) {
189 | for (let i = 0, len = this.images.length; i < len; i++) {
190 | this.images[i].classList.remove('img_dragging');
191 | }
192 |
193 | event.target.classList.add('img_dragging');
194 | }
195 |
196 | /**
197 | * Function triggered on drag end
198 | */
199 | handleDragEnd() {
200 | for (let i = 0, len = this.images.length; i < len; i++) {
201 | this.images[i].classList.remove('img_dragging');
202 | }
203 | }
204 | }
205 |
206 | }
207 | );
208 |
--------------------------------------------------------------------------------
/assets/svg/panda.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/bootstrap-modal/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v3.3.7 (http://getbootstrap.com)
3 | * Copyright 2011-2016 Twitter, Inc.
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 |
7 | /*!
8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=fa8721f4276e60c899507a3ed4b05d9b)
9 | * Config saved to config.json and https://gist.github.com/fa8721f4276e60c899507a3ed4b05d9b
10 | */
11 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(t){"use strict";var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(t){"use strict";function e(e,s){return this.each(function(){var n=t(this),o=n.data("bs.modal"),a=t.extend({},i.DEFAULTS,n.data(),"object"==typeof e&&e);o||n.data("bs.modal",o=new i(this,a)),"string"==typeof e?o[e](s):a.show&&o.show(s)})}var i=function(e,i){this.options=i,this.$body=t(document.body),this.$element=t(e),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,t.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};i.VERSION="3.3.7",i.TRANSITION_DURATION=300,i.BACKDROP_TRANSITION_DURATION=150,i.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},i.prototype.toggle=function(t){return this.isShown?this.hide():this.show(t)},i.prototype.show=function(e){var s=this,n=t.Event("show.bs.modal",{relatedTarget:e});this.$element.trigger(n),this.isShown||n.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',t.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){s.$element.one("mouseup.dismiss.bs.modal",function(e){t(e.target).is(s.$element)&&(s.ignoreBackdropClick=!0)})}),this.backdrop(function(){var n=t.support.transition&&s.$element.hasClass("fade");s.$element.parent().length||s.$element.appendTo(s.$body),s.$element.show().scrollTop(0),s.adjustDialog(),n&&s.$element[0].offsetWidth,s.$element.addClass("in"),s.enforceFocus();var o=t.Event("shown.bs.modal",{relatedTarget:e});n?s.$dialog.one("bsTransitionEnd",function(){s.$element.trigger("focus").trigger(o)}).emulateTransitionEnd(i.TRANSITION_DURATION):s.$element.trigger("focus").trigger(o)}))},i.prototype.hide=function(e){e&&e.preventDefault(),e=t.Event("hide.bs.modal"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),t(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),t.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",t.proxy(this.hideModal,this)).emulateTransitionEnd(i.TRANSITION_DURATION):this.hideModal())},i.prototype.enforceFocus=function(){t(document).off("focusin.bs.modal").on("focusin.bs.modal",t.proxy(function(t){document===t.target||this.$element[0]===t.target||this.$element.has(t.target).length||this.$element.trigger("focus")},this))},i.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",t.proxy(function(t){27==t.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},i.prototype.resize=function(){this.isShown?t(window).on("resize.bs.modal",t.proxy(this.handleUpdate,this)):t(window).off("resize.bs.modal")},i.prototype.hideModal=function(){var t=this;this.$element.hide(),this.backdrop(function(){t.$body.removeClass("modal-open"),t.resetAdjustments(),t.resetScrollbar(),t.$element.trigger("hidden.bs.modal")})},i.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},i.prototype.backdrop=function(e){var s=this,n=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var o=t.support.transition&&n;if(this.$backdrop=t(document.createElement("div")).addClass("modal-backdrop "+n).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",t.proxy(function(t){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(t.target===t.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),o&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!e)return;o?this.$backdrop.one("bsTransitionEnd",e).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):e()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var a=function(){s.removeBackdrop(),e&&e()};t.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",a).emulateTransitionEnd(i.BACKDROP_TRANSITION_DURATION):a()}else e&&e()},i.prototype.handleUpdate=function(){this.adjustDialog()},i.prototype.adjustDialog=function(){var t=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},i.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},i.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth
2 |
3 |
4 |
63 |
--------------------------------------------------------------------------------
/src/plugins/ManualSave/ManualSavePlugin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ManualSavePlugin module
3 | */
4 | define(
5 | [
6 | 'class/PersistenceManager/PersistenceManagerRegistry',
7 | 'class/Serializer/SerializerRegistry'
8 | ],
9 | function (PersistenceManagerRegistry, SerializerRegistry) {
10 | return class ManualSavePlugin {
11 |
12 | /**
13 | * Constructor
14 | */
15 | constructor(canvas, config) {
16 | this.prefix = 'manual_save_';
17 | this.canvas = canvas;
18 | this.config = config;
19 | this.serializer = new SerializerRegistry().guessSerializer(config.serializer);
20 | this.persistenceManager = new PersistenceManagerRegistry(config).guessPersistenceManager(config.persistence_manager);
21 | }
22 |
23 | /**
24 | * Get the configuration errors
25 | *
26 | * @return array
27 | */
28 | getConfigurationErrors() {
29 | let errors = [];
30 |
31 | if (typeof this.config.manual_save === 'undefined') {
32 | errors.push('manual_save must be defined');
33 | } else {
34 | if (typeof this.config.manual_save.enable !== 'boolean') {
35 | errors.push('manual_save.enable must be defined as a boolean');
36 | } else {
37 | if (this.config.manual_save.enable === true) {
38 |
39 | if (typeof this.config.manual_save.load_button_input_id !== 'string') {
40 | errors.push('manual_save.load_button_input_id must be defined (as a string) because the plugin is enabled');
41 | } else {
42 | if (document.getElementById(this.config.manual_save.load_button_input_id) === null) {
43 | errors.push('No tag with id ' + this.config.manual_save.load_button_input_id + ' found');
44 | }
45 | }
46 |
47 | if (typeof this.config.manual_save.save_button_input_d !== 'string') {
48 | errors.push('manual_save.save_button_input_d must be defined (as a string) because the plugin is enabled');
49 | } else {
50 | if (document.getElementById(this.config.manual_save.save_button_input_d) === null) {
51 | errors.push('No tag with id ' + this.config.manual_save.save_button_input_d + ' found');
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | return errors;
59 | }
60 |
61 | /**
62 | * Start the plugin
63 | */
64 | start() {
65 | if (this.config.manual_save.enable === true) {
66 | // add the modal to the dom on page ready
67 | $(document).ready(() => {
68 | $('body').append(this.getLoadModalHtmlContent());
69 | $('body').append(this.getSaveModalHtmlContent());
70 | });
71 |
72 | // open the load modal
73 | document.getElementById(this.config.manual_save.load_button_input_id).onclick = (e) => {
74 | $('#load-modal').replaceWith(this.getLoadModalHtmlContent());
75 | $('#load-modal').modal('show');
76 | };
77 |
78 | // open the save modal
79 | document.getElementById(this.config.manual_save.save_button_input_d).onclick = (e) => {
80 | $('#save-modal').replaceWith(this.getSaveModalHtmlContent());
81 | $('#save-modal').modal('show');
82 | };
83 |
84 | // save as a new project
85 | $(document).on('click', '#new-save-button', event => {
86 | let title = document.getElementById('new-project-title').value;
87 | let error = this.getError(title);
88 | if (!error) {
89 | this.saveProject(title);
90 | $('#save-modal').modal('hide');
91 | } else {
92 | this.printError(error);
93 | }
94 | });
95 |
96 | // override a saved project
97 | $(document).on('click', '.override-save-button', event => {
98 | let title = $(event.target).data('project');
99 | this.saveProject(title);
100 | $('#save-modal').modal('hide');
101 | });
102 |
103 | // load a project
104 | $(document).on('click', '.load-button', event => {
105 | let title = $(event.target).data('project');
106 | this.loadProject(title);
107 | });
108 |
109 | // delete a project
110 | $(document).on('click', '.delete-button', event => {
111 | let title = $(event.target).data('project');
112 | this.removeProject(title);
113 | });
114 | }
115 | }
116 |
117 | /**
118 | * Check errors on the title before a new save
119 | */
120 | getError(title) {
121 | title = title.trim();
122 | if (title.length === 0) {
123 | return this.config.manual_save.labels.title_not_blank;
124 | }
125 |
126 | let projects = this.persistenceManager.load({'key': this.prefix});
127 | for (let i = 0, len = projects.length; i < len; i++) {
128 | let project = JSON.parse(projects[i]);
129 | if (project.title === title) {
130 | return this.config.manual_save.labels.title_already_used;
131 | }
132 | }
133 |
134 | return null;
135 | }
136 |
137 | /**
138 | * Print errors on the modal
139 | */
140 | printError(error) {
141 | $('span.error').replaceWith('' + error + '');
142 | }
143 |
144 | /**
145 | * Load a project from his title
146 | */
147 | loadProject(title) {
148 | let project = JSON.parse(this.persistenceManager.load({key: this.prefix + title})[0]);
149 | let serializedCanvas = this.serializer.serialize(project.canvas);
150 | let oldWidth = parseFloat(project["container-width"]);
151 | let newWidth = parseFloat(getComputedStyle(document.getElementById(this.config.canvas_container_id)).width);
152 |
153 | this.serializer.deserialize(serializedCanvas, this.canvas, () => {
154 | let ratio = newWidth / oldWidth;
155 | this.canvas.trigger("canvas:deserialized", {"ratio": ratio}); // used by the ObjectResizer
156 | this.canvas.renderAll();
157 | $('#load-modal').modal('hide');
158 | });
159 | }
160 |
161 | /**
162 | * Remove a project from his title
163 | */
164 | removeProject(title) {
165 | this.persistenceManager.remove({key: this.prefix + title});
166 | $('#load-modal').modal('hide');
167 | }
168 |
169 | /**
170 | * Save a project by title
171 | */
172 | saveProject(title) {
173 | // get the canvas container width and height for resizing on load
174 | let width = parseInt(getComputedStyle(document.getElementById(this.config.canvas_container_id)).width);
175 | let height = parseInt(getComputedStyle(document.getElementById(this.config.canvas_container_id)).height);
176 |
177 | let project = this.serializer.serialize({
178 | 'title': title,
179 | 'canvas': this.canvas,
180 | 'container-width': width,
181 | 'container-height': height,
182 | 'date': getCurrentDate()
183 | });
184 |
185 | this.persistenceManager.persist(project, {key: this.prefix + title});
186 | }
187 |
188 | /**
189 | * Get the html content of the save modal
190 | */
191 | getSaveModalHtmlContent() {
192 | let config = this.config;
193 | // get an array with all stringified projects
194 | let projects = this.persistenceManager.load({'key': this.prefix});
195 | /**
196 | * Template string for the save modal (ES6 feature)
197 | *
198 | * This function add the list of projects to override a save
199 | */
200 | function saveModalHTML(templateData) {
201 | let string = templateData[0];
202 | let labels = config.manual_save.labels;
203 | let len = projects.length;
204 | if (len > 0) {
205 | let html = '
';
206 | for (let i = 0; i < len; i++) {
207 | let project = JSON.parse(projects[i]);
208 | html +=
209 | '
258 | `;
259 | }
260 |
261 | /**
262 | * Get the html content of the load modal
263 | */
264 | getLoadModalHtmlContent() {
265 | let config = this.config;
266 | // get an array with all stringified projects
267 | let projects = this.persistenceManager.load({'key': this.prefix});
268 | let labels = config.manual_save.labels;
269 |
270 | /**
271 | * Template string for the load modal (ES6 feature)
272 | *
273 | * This function replace the "no projects to load" text
274 | * by the list of projects if there are any already saved
275 | */
276 | function loadModalHTML(templateData) {
277 | let string = templateData[0];
278 | let len = projects.length;
279 | if (len > 0) {
280 | let html = '
';
281 | for (let i = 0; i < len; i++) {
282 | let project = JSON.parse(projects[i]);
283 | html +=
284 | '