├── .babelrc ├── style.css ├── .gitignore ├── docker-compose.yml ├── src ├── init.js ├── plugins │ ├── AutoSave │ │ ├── README.md │ │ └── AutoSavePlugin.js │ ├── AutoImageResizer │ │ ├── README.md │ │ └── AutoImageResizerPlugin.js │ ├── KeyboardListener │ │ ├── README.md │ │ └── KeyboardListenerPlugin.js │ ├── ObjectResizer │ │ ├── README.md │ │ └── ObjectResizerPlugin.js │ ├── RemoveObject │ │ ├── README.md │ │ └── RemoveObjectPlugin.js │ ├── ImageLoader │ │ ├── README.md │ │ └── ImageLoaderPlugin.js │ ├── OutputArea │ │ ├── README.md │ │ └── OutputAreaPlugin.js │ ├── ColorPicker │ │ ├── README.md │ │ ├── ColorPickerPlugin.js │ │ └── SvgColorator.js │ ├── ImageDragAndDrop │ │ ├── README.md │ │ └── ImageDragAndDropPlugin.js │ ├── ImageFlipper │ │ ├── README.md │ │ └── ImageFlipperPlugin.js │ └── ManualSave │ │ ├── README.md │ │ └── ManualSavePlugin.js ├── class │ ├── ImageReader │ │ ├── AbstractImageReader.js │ │ ├── SvgImageReader.js │ │ ├── PlainImageReader.js │ │ └── ImageReaderRegistry.js │ ├── FileDownloader │ │ ├── AbstractFileDownloader.js │ │ ├── BlobFileDownloader.js │ │ └── FileDownloaderRegistry.js │ ├── Serializer │ │ ├── AbstractSerializer.js │ │ ├── JsonSerializer.js │ │ ├── SvgSerializer.js │ │ └── SerializerRegistry.js │ ├── PersistenceManager │ │ ├── AbstractPersistenceManager.js │ │ ├── PersistenceManagerRegistry.js │ │ └── LocalStoragePersistenceManager.js │ ├── SvgColorator.js │ ├── MimeTypeGuesser.js │ ├── FabricOverrider.js │ └── SvgEditor.js ├── config │ ├── plugins.js │ └── editor.js └── utils.js ├── package.json ├── assets ├── svg │ ├── vroum.svg │ ├── dance.svg │ ├── miaou.svg │ ├── panda.svg │ ├── tux.svg │ └── bonjour.svg └── bootstrap-modal │ ├── js │ ├── bootstrap.min.js │ └── bootstrap.js │ ├── css │ └── bootstrap.min.css │ └── config.json ├── gulpfile.js ├── docs └── create_plugin.md ├── README.md └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | border: 1px solid black; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | lib/ 4 | dist/ 5 | .idea/ 6 | *~ 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | app: 2 | image: ovski/frontend-dev 3 | volumes: 4 | - .:/var/www 5 | ports: 6 | - 8030:80 7 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | requirejs(['./class/SvgEditor'], function(SvgEditor) { 2 | try { 3 | let editor = new SvgEditor(); 4 | editor.init(); 5 | editor.triggerReadyFunction(); 6 | } catch(e) { 7 | console.error('The editor failed to start: ' + e.message); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/plugins/AutoSave/README.md: -------------------------------------------------------------------------------- 1 | AutoSave Plugin 2 | =============== 3 | 4 | This plugin save the canvas in the local storage (a stringified json) after each render. 5 | 6 | You just need to enable the plugin by updating your configuration object: 7 | 8 | ```js 9 | ..., 10 | 'auto_save': { 11 | 'enable': true 12 | }, 13 | ``` 14 | -------------------------------------------------------------------------------- /src/plugins/AutoImageResizer/README.md: -------------------------------------------------------------------------------- 1 | AutoImageResizer Plugin 2 | ======================= 3 | 4 | This plugin automatically resize images when they are bigger than the canvas by updating their scale. 5 | 6 | You just need to enable the plugin by updating your configuration object: 7 | 8 | ```js 9 | ..., 10 | 'auto_image_resizer': { 11 | 'enable': true 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /src/plugins/KeyboardListener/README.md: -------------------------------------------------------------------------------- 1 | KeyboardListener Plugin 2 | ======================= 3 | 4 | This plugin only add the possibility to delete a selected object by pressing the `del` key, and to move a selected object by pressing the arrow keys. 5 | It may be a good start point to add keyboard related features. 6 | 7 | You just need to configure the plugin by updating your configuration object: 8 | 9 | ```js 10 | ..., 11 | 'keyboard_listener': { 12 | 'enable_delete_object': true, 13 | 'enable_move_object': true 14 | }, 15 | ``` 16 | -------------------------------------------------------------------------------- /src/class/ImageReader/AbstractImageReader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract Image Reader module 3 | */ 4 | define(function () { 5 | 6 | return class AbstractImageReader { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | 13 | if (this.constructor === AbstractImageReader) { 14 | throw new TypeError("Cannot construct AbstractImageReader instances directly"); 15 | } 16 | 17 | if (this.getCanvasImage === undefined) { 18 | throw new TypeError("getCanvasImage must be implemented"); 19 | } 20 | } 21 | } 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /src/class/FileDownloader/AbstractFileDownloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract File Downloader module 3 | */ 4 | define(function () { 5 | 6 | return class AbstractFileDownloader { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | 13 | if (this.constructor === AbstractFileDownloader) { 14 | throw new TypeError("Cannot construct AbstractFileDownloader instances directly"); 15 | } 16 | 17 | if (this.downloadFile === undefined) { 18 | throw new TypeError("downloadFile must be implemented"); 19 | } 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /src/plugins/ObjectResizer/README.md: -------------------------------------------------------------------------------- 1 | ManualSave Plugin 2 | ================= 3 | 4 | This plugin is used to scale the canvas after loading it from the persitence layer. On save, the width of the canvas container is saved. 5 | On load, the ratio between the last save and the current windows is used to transform (translate and resize) the canvas objects. 6 | 7 | This plugin listen to the canvas:deserialized fabricjs event. The event contain the ratio. 8 | 9 | You just need to configure the plugin by updating your configuration object: 10 | 11 | ```js 12 | ..., 13 | 'object_resizer': { 14 | 'enable': true 15 | } 16 | ``` 17 | -------------------------------------------------------------------------------- /src/class/Serializer/AbstractSerializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract serializer module 3 | */ 4 | define(function () { 5 | 6 | return class AbstractSerializer { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | 13 | if (this.constructor === AbstractSerializer) { 14 | throw new TypeError("Cannot construct AbstractSerializer instances directly"); 15 | } 16 | 17 | if (this.serialize === undefined) { 18 | throw new TypeError("serialize must be implemented"); 19 | } 20 | 21 | if (this.deserialize === undefined) { 22 | throw new TypeError("deserialize must be implemented"); 23 | } 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabricjs", 3 | "version": "1.0.0", 4 | "description": "A fabricjs prototype", 5 | "main": "index.html", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "babel src -d lib" 9 | }, 10 | "keywords": [ 11 | "fabricjs", 12 | "svg", 13 | "babel" 14 | ], 15 | "author": "baptiste.bouchereau@idci-consulting.fr", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "babel-cli": "^6.10.1", 19 | "babel-preset-es2015": "^6.9.0", 20 | "gulp": "^3.9.1", 21 | "gulp-babel": "^6.1.2", 22 | "gulp-chmod": "^1.3.0", 23 | "gulp-chown": "^1.1.0", 24 | "gulp-concat": "^2.6.0", 25 | "gulp-livereload": "^3.8.1", 26 | "gulp-requirejs-optimize": "^1.2.0", 27 | "gulp-uglify": "^2.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/class/PersistenceManager/AbstractPersistenceManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract persistence manager module 3 | */ 4 | define(function () { 5 | 6 | return class AbstractPersistenceManager { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | 13 | if (this.constructor === AbstractPersistenceManager) { 14 | throw new TypeError("Cannot construct AbstractPersistenceManager instances directly"); 15 | } 16 | 17 | if (this.persist === undefined) { 18 | throw new TypeError("persist must be implemented"); 19 | } 20 | 21 | if (this.load === undefined) { 22 | throw new TypeError("load must be implemented"); 23 | } 24 | 25 | if (this.remove === undefined) { 26 | throw new TypeError("remove must be implemented"); 27 | } 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/class/Serializer/JsonSerializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JsonSerializer module 3 | */ 4 | define(['./AbstractSerializer'], function (AbstractSerializer) { 5 | 6 | return class JsonSerializer extends AbstractSerializer { 7 | 8 | /** 9 | * Serialize the canvas 10 | * 11 | * @param canvas: the canvas to be serialized 12 | * @return string: the serialize canvas 13 | */ 14 | serialize(canvas) { 15 | return JSON.stringify(canvas); 16 | } 17 | 18 | /** 19 | * Deserialize 20 | * 21 | * @param serializedCanvas: the serialized canvas 22 | * @param canvas: the canvas object 23 | * @param callback : a callback function 24 | */ 25 | deserialize(serializedCanvas, canvas, callback) { 26 | canvas.loadFromJSON(serializedCanvas); 27 | callback(); 28 | } 29 | 30 | } 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /src/class/ImageReader/SvgImageReader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SvgImageReader module 3 | */ 4 | define(['./AbstractImageReader'], function (AbstractImageReader) { 5 | 6 | return class SvgImageReader extends AbstractImageReader { 7 | 8 | /** 9 | * Get the canevas image object from the svg file 10 | * 11 | * @param file: the svg from the input 12 | * @param callback: the function with the retrieved image 13 | */ 14 | getCanvasImage(file, callback) { 15 | let reader = new FileReader(); 16 | reader.onload = (event) => { 17 | let content = event.target.result; 18 | fabric.loadSVGFromString(content, function(objects, options) { 19 | let obj = fabric.util.groupSVGElements(objects, options); 20 | 21 | return callback(obj); 22 | }); 23 | }; 24 | 25 | reader.readAsText(file); 26 | } 27 | } 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /src/class/Serializer/SvgSerializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SvgSerializer module 3 | */ 4 | define(['./AbstractSerializer'], function (AbstractSerializer) { 5 | 6 | return class SvgSerializer extends AbstractSerializer { 7 | 8 | /** 9 | * Serialize the canvas 10 | * 11 | * @param canvas: the canvas to be serialized 12 | */ 13 | serialize(canvas) { 14 | return canvas.toSVG(); 15 | } 16 | 17 | /** 18 | * Deserialize 19 | * 20 | * @param serializedCanvas: the serialized canvas 21 | * @param canvas: the canvas object 22 | * @param callback : a callback function 23 | */ 24 | deserialize(serializedCanvas, canvas, callback) { 25 | fabric.loadSVGFromString(serializedCanvas, function(objects, options) { 26 | var obj = fabric.util.groupSVGElements(objects, options); 27 | canvas.add(obj); 28 | callback(); 29 | }); 30 | } 31 | 32 | } 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /src/class/FileDownloader/BlobFileDownloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * BlobFileDownloader module 3 | */ 4 | define(['./AbstractFileDownloader', 'class/MimeTypeGuesser'], function (AbstractFileDownloader, MimeTypeGuesser) { 5 | 6 | return class BlobFileDownloader extends AbstractFileDownloader{ 7 | 8 | /** 9 | * download a file 10 | * 11 | * @param imageUrl 12 | * 13 | * @return callback 14 | */ 15 | downloadFile(imageUrl, callback) { 16 | let extension = getExtension(imageUrl); 17 | var xhr = new XMLHttpRequest(); 18 | xhr.open('GET', imageUrl, true); 19 | xhr.responseType = 'blob'; 20 | 21 | xhr.onload = (event) => { 22 | if (event.target.status == 200) { 23 | var blob = new Blob( 24 | [ event.target.response ], 25 | { type: MimeTypeGuesser.guess(extension) } 26 | ); 27 | 28 | return callback(blob); 29 | } 30 | }; 31 | 32 | xhr.send(); 33 | } 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/class/FileDownloader/FileDownloaderRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FileDownloaderRegistry module 3 | */ 4 | define(['./BlobFileDownloader'], function (BlobFileDownloader) { 5 | 6 | return class FileDownloaderRegistry { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | this.downloaderFileTypeMap = [ 13 | { 14 | "fileType": "blob", 15 | "downloader": new BlobFileDownloader() 16 | } 17 | ]; 18 | } 19 | 20 | /** 21 | * Guess the file downloader for the given mime type 22 | */ 23 | guessFileDownloader(fileType) { 24 | for (let i = 0, l = this.downloaderFileTypeMap.length; i < l; i++) { 25 | let downloaderFileType = this.downloaderFileTypeMap[i]; 26 | if (downloaderFileType.fileType === fileType) { 27 | return downloaderFileType.downloader; 28 | } 29 | } 30 | 31 | throw Error(`No file downloader found for type ${fileType}`); 32 | } 33 | } 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/plugins/RemoveObject/README.md: -------------------------------------------------------------------------------- 1 | RemoveObject Plugin 2 | =================== 3 | 4 | This plugin allow to delete a selected object thanks to a button. 5 | 6 | ```html 7 | 8 | 9 | 10 | 11 | Svg widget editor 12 | 13 | 14 | 15 | 16 | 17 | 18 |

19 |
20 | 21 |
22 | 23 | 24 | ``` 25 | 26 | Finally you need to configure the plugin by updating your configuration object: 27 | 28 | ```js 29 | ..., 30 | 'remove_object': { 31 | 'enable': true, 32 | 'input_id': 'remove-object' 33 | }, 34 | ``` 35 | -------------------------------------------------------------------------------- /src/class/Serializer/SerializerRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ImageReader module 3 | */ 4 | define(['./JsonSerializer', './SvgSerializer'], function (JsonSerializer, SvgSerializer) { 5 | 6 | return class SerializerRegistry { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor() { 12 | this.keySerializerMap = [ 13 | { 14 | "key": "json", 15 | "serializer": new JsonSerializer() 16 | }, 17 | { 18 | "key": "svg", 19 | "serializer": new SvgSerializer() 20 | } 21 | ]; 22 | } 23 | 24 | /** 25 | * Guess the serializer for the given key 26 | */ 27 | guessSerializer(key) { 28 | for (let i = 0, l = this.keySerializerMap.length; i < l; i++) { 29 | let keySerializer = this.keySerializerMap[i]; 30 | if (key === keySerializer.key) { 31 | return keySerializer.serializer; 32 | } 33 | } 34 | 35 | throw Error(`No serializer found for key ${key}`); 36 | } 37 | 38 | } 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /src/config/plugins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugins configuration 3 | */ 4 | define(function () { 5 | return [ 6 | { 7 | "class": "plugins/ImageLoader/ImageLoaderPlugin" 8 | }, 9 | { 10 | "class": "plugins/OutputArea/OutputAreaPlugin" 11 | }, 12 | { 13 | "class": "plugins/ManualSave/ManualSavePlugin" 14 | }, 15 | { 16 | "class": "plugins/ImageFlipper/ImageFlipperPlugin" 17 | }, 18 | { 19 | "class": "plugins/AutoImageResizer/AutoImageResizerPlugin" 20 | }, 21 | { 22 | "class": "plugins/RemoveObject/RemoveObjectPlugin" 23 | }, 24 | { 25 | "class": "plugins/AutoSave/AutoSavePlugin" 26 | }, 27 | { 28 | "class": "plugins/ObjectResizer/ObjectResizerPlugin", 29 | "priority": 1 30 | }, 31 | { 32 | "class": "plugins/ColorPicker/ColorPickerPlugin" 33 | }, 34 | { 35 | "class": "plugins/KeyboardListener/KeyboardListenerPlugin" 36 | }, 37 | { 38 | "class": "plugins/ImageDragAndDrop/ImageDragAndDropPlugin" 39 | } 40 | ]; 41 | }); 42 | -------------------------------------------------------------------------------- /src/class/ImageReader/PlainImageReader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PlainImageReader module 3 | */ 4 | define(['./AbstractImageReader'], function (AbstractImageReader) { 5 | 6 | return class PlainImageReader extends AbstractImageReader { 7 | 8 | /** 9 | * Get the canevas image object from the image file 10 | * 11 | * @param file: the file from the input 12 | * @param callback: the function with the retrieved image 13 | */ 14 | getCanvasImage(file, callback) { 15 | var reader = new FileReader(); 16 | reader.onload = (event) => { 17 | var imgObj = new Image(); 18 | imgObj.src = event.target.result; 19 | imgObj.onload = () => { 20 | let image = new fabric.Image(imgObj); 21 | image.set({ 22 | angle: 0, 23 | padding: 10, 24 | cornersize:10, 25 | height:110, 26 | width:110, 27 | }); 28 | 29 | return callback(image); 30 | } 31 | } 32 | 33 | reader.readAsDataURL(file); 34 | } 35 | 36 | } 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /src/class/PersistenceManager/PersistenceManagerRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ImageReader module 3 | */ 4 | define(['./LocalStoragePersistenceManager'], function (LocalStoragePersistenceManager) { 5 | 6 | return class PersistenceManagerRegistry { 7 | 8 | /** 9 | * Constructor 10 | */ 11 | constructor(config) { 12 | this.keyPersistenceManagerMap = [ 13 | { 14 | "key": "local_storage", 15 | "persistenceManager": new LocalStoragePersistenceManager(config.local_storage_prefix) 16 | } 17 | ]; 18 | } 19 | 20 | /** 21 | * Guess the persistence manager for the given key 22 | */ 23 | guessPersistenceManager(key) { 24 | for (let i = 0, l = this.keyPersistenceManagerMap.length; i < l; i++) { 25 | let keyPersistenceManager = this.keyPersistenceManagerMap[i]; 26 | if (key === keyPersistenceManager.key) { 27 | return keyPersistenceManager.persistenceManager; 28 | } 29 | } 30 | 31 | throw Error(`No persistenceManager found for key ${key}`); 32 | } 33 | 34 | } 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /src/plugins/ImageLoader/README.md: -------------------------------------------------------------------------------- 1 | ImageLoader Plugin 2 | ================== 3 | 4 | This plugin allow you to load images to the canvas thanks to a file input. 5 | 6 | First, add a file input 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 | 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 | vroum -------------------------------------------------------------------------------- /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 | dance -------------------------------------------------------------------------------- /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 | miaou -------------------------------------------------------------------------------- /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 | 35 -------------------------------------------------------------------------------- /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 | 18 | 20 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 61 | 62 | 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 | '' + 210 | '' + 211 | '' + 212 | '' + 215 | '' 216 | ; 217 | } 218 | html += '
' + project.title + '' + project.date + '
'; 219 | string = string.replace('{{ no_save_already }}', html); 220 | } 221 | 222 | for (let label in labels) { 223 | if (labels.hasOwnProperty(label)) { 224 | string = string.replace('{{ ' + label + ' }}', labels[label]); 225 | } 226 | } 227 | 228 | return string; 229 | } 230 | 231 | return saveModalHTML` 232 | 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 | '' + 285 | '' + 286 | '' + 287 | '' + 295 | '' 296 | ; 297 | } 298 | html += '
' + project.title + '' + project.date + '' + 288 | '' + 291 | '' + 294 | '
'; 299 | string = string.replace('{{ nothing_to_load }}', html); 300 | } 301 | 302 | for (let label in labels) { 303 | if (labels.hasOwnProperty(label)) { 304 | string = string.replace('{{ ' + label + ' }}', labels[label]); 305 | } 306 | } 307 | 308 | return string; 309 | } 310 | 311 | return loadModalHTML` 312 | 328 | `; 329 | } 330 | } 331 | } 332 | ); 333 | -------------------------------------------------------------------------------- /assets/bootstrap-modal/css/bootstrap.min.css: -------------------------------------------------------------------------------- 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 | * Bootstrap v3.3.7 (http://getbootstrap.com) 12 | * Copyright 2011-2016 Twitter, Inc. 13 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 14 | *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:normal;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height, visibility;-o-transition-property:height, visibility;transition-property:height, visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:hidden;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);-o-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform 0.3s ease-out;-o-transition:-o-transform 0.3s ease-out;transition:transform 0.3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);-webkit-background-clip:padding-box;background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.clearfix:before,.clearfix:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important}.affix{position:fixed} -------------------------------------------------------------------------------- /assets/bootstrap-modal/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "vars": { 3 | "@gray-base": "#000", 4 | "@gray-darker": "lighten(@gray-base, 13.5%)", 5 | "@gray-dark": "lighten(@gray-base, 20%)", 6 | "@gray": "lighten(@gray-base, 33.5%)", 7 | "@gray-light": "lighten(@gray-base, 46.7%)", 8 | "@gray-lighter": "lighten(@gray-base, 93.5%)", 9 | "@brand-primary": "darken(#428bca, 6.5%)", 10 | "@brand-success": "#5cb85c", 11 | "@brand-info": "#5bc0de", 12 | "@brand-warning": "#f0ad4e", 13 | "@brand-danger": "#d9534f", 14 | "@body-bg": "#fff", 15 | "@text-color": "@gray-dark", 16 | "@link-color": "@brand-primary", 17 | "@link-hover-color": "darken(@link-color, 15%)", 18 | "@link-hover-decoration": "underline", 19 | "@font-family-sans-serif": "\"Helvetica Neue\", Helvetica, Arial, sans-serif", 20 | "@font-family-serif": "Georgia, \"Times New Roman\", Times, serif", 21 | "@font-family-monospace": "Menlo, Monaco, Consolas, \"Courier New\", monospace", 22 | "@font-family-base": "@font-family-sans-serif", 23 | "@font-size-base": "14px", 24 | "@font-size-large": "ceil((@font-size-base * 1.25))", 25 | "@font-size-small": "ceil((@font-size-base * 0.85))", 26 | "@font-size-h1": "floor((@font-size-base * 2.6))", 27 | "@font-size-h2": "floor((@font-size-base * 2.15))", 28 | "@font-size-h3": "ceil((@font-size-base * 1.7))", 29 | "@font-size-h4": "ceil((@font-size-base * 1.25))", 30 | "@font-size-h5": "@font-size-base", 31 | "@font-size-h6": "ceil((@font-size-base * 0.85))", 32 | "@line-height-base": "1.428571429", 33 | "@line-height-computed": "floor((@font-size-base * @line-height-base))", 34 | "@headings-font-family": "inherit", 35 | "@headings-font-weight": "500", 36 | "@headings-line-height": "1.1", 37 | "@headings-color": "inherit", 38 | "@icon-font-path": "\"../fonts/\"", 39 | "@icon-font-name": "\"glyphicons-halflings-regular\"", 40 | "@icon-font-svg-id": "\"glyphicons_halflingsregular\"", 41 | "@padding-base-vertical": "6px", 42 | "@padding-base-horizontal": "12px", 43 | "@padding-large-vertical": "10px", 44 | "@padding-large-horizontal": "16px", 45 | "@padding-small-vertical": "5px", 46 | "@padding-small-horizontal": "10px", 47 | "@padding-xs-vertical": "1px", 48 | "@padding-xs-horizontal": "5px", 49 | "@line-height-large": "1.3333333", 50 | "@line-height-small": "1.5", 51 | "@border-radius-base": "4px", 52 | "@border-radius-large": "6px", 53 | "@border-radius-small": "3px", 54 | "@component-active-color": "#fff", 55 | "@component-active-bg": "@brand-primary", 56 | "@caret-width-base": "4px", 57 | "@caret-width-large": "5px", 58 | "@table-cell-padding": "8px", 59 | "@table-condensed-cell-padding": "5px", 60 | "@table-bg": "transparent", 61 | "@table-bg-accent": "#f9f9f9", 62 | "@table-bg-hover": "#f5f5f5", 63 | "@table-bg-active": "@table-bg-hover", 64 | "@table-border-color": "#ddd", 65 | "@btn-font-weight": "normal", 66 | "@btn-default-color": "#333", 67 | "@btn-default-bg": "#fff", 68 | "@btn-default-border": "#ccc", 69 | "@btn-primary-color": "#fff", 70 | "@btn-primary-bg": "@brand-primary", 71 | "@btn-primary-border": "darken(@btn-primary-bg, 5%)", 72 | "@btn-success-color": "#fff", 73 | "@btn-success-bg": "@brand-success", 74 | "@btn-success-border": "darken(@btn-success-bg, 5%)", 75 | "@btn-info-color": "#fff", 76 | "@btn-info-bg": "@brand-info", 77 | "@btn-info-border": "darken(@btn-info-bg, 5%)", 78 | "@btn-warning-color": "#fff", 79 | "@btn-warning-bg": "@brand-warning", 80 | "@btn-warning-border": "darken(@btn-warning-bg, 5%)", 81 | "@btn-danger-color": "#fff", 82 | "@btn-danger-bg": "@brand-danger", 83 | "@btn-danger-border": "darken(@btn-danger-bg, 5%)", 84 | "@btn-link-disabled-color": "@gray-light", 85 | "@btn-border-radius-base": "@border-radius-base", 86 | "@btn-border-radius-large": "@border-radius-large", 87 | "@btn-border-radius-small": "@border-radius-small", 88 | "@input-bg": "#fff", 89 | "@input-bg-disabled": "@gray-lighter", 90 | "@input-color": "@gray", 91 | "@input-border": "#ccc", 92 | "@input-border-radius": "@border-radius-base", 93 | "@input-border-radius-large": "@border-radius-large", 94 | "@input-border-radius-small": "@border-radius-small", 95 | "@input-border-focus": "#66afe9", 96 | "@input-color-placeholder": "#999", 97 | "@input-height-base": "(@line-height-computed + (@padding-base-vertical * 2) + 2)", 98 | "@input-height-large": "(ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2)", 99 | "@input-height-small": "(floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2)", 100 | "@form-group-margin-bottom": "15px", 101 | "@legend-color": "@gray-dark", 102 | "@legend-border-color": "#e5e5e5", 103 | "@input-group-addon-bg": "@gray-lighter", 104 | "@input-group-addon-border-color": "@input-border", 105 | "@cursor-disabled": "not-allowed", 106 | "@dropdown-bg": "#fff", 107 | "@dropdown-border": "rgba(0,0,0,.15)", 108 | "@dropdown-fallback-border": "#ccc", 109 | "@dropdown-divider-bg": "#e5e5e5", 110 | "@dropdown-link-color": "@gray-dark", 111 | "@dropdown-link-hover-color": "darken(@gray-dark, 5%)", 112 | "@dropdown-link-hover-bg": "#f5f5f5", 113 | "@dropdown-link-active-color": "@component-active-color", 114 | "@dropdown-link-active-bg": "@component-active-bg", 115 | "@dropdown-link-disabled-color": "@gray-light", 116 | "@dropdown-header-color": "@gray-light", 117 | "@dropdown-caret-color": "#000", 118 | "@screen-xs": "480px", 119 | "@screen-xs-min": "@screen-xs", 120 | "@screen-phone": "@screen-xs-min", 121 | "@screen-sm": "768px", 122 | "@screen-sm-min": "@screen-sm", 123 | "@screen-tablet": "@screen-sm-min", 124 | "@screen-md": "992px", 125 | "@screen-md-min": "@screen-md", 126 | "@screen-desktop": "@screen-md-min", 127 | "@screen-lg": "1200px", 128 | "@screen-lg-min": "@screen-lg", 129 | "@screen-lg-desktop": "@screen-lg-min", 130 | "@screen-xs-max": "(@screen-sm-min - 1)", 131 | "@screen-sm-max": "(@screen-md-min - 1)", 132 | "@screen-md-max": "(@screen-lg-min - 1)", 133 | "@grid-columns": "12", 134 | "@grid-gutter-width": "30px", 135 | "@grid-float-breakpoint": "@screen-sm-min", 136 | "@grid-float-breakpoint-max": "(@grid-float-breakpoint - 1)", 137 | "@container-tablet": "(720px + @grid-gutter-width)", 138 | "@container-sm": "@container-tablet", 139 | "@container-desktop": "(940px + @grid-gutter-width)", 140 | "@container-md": "@container-desktop", 141 | "@container-large-desktop": "(1140px + @grid-gutter-width)", 142 | "@container-lg": "@container-large-desktop", 143 | "@navbar-height": "50px", 144 | "@navbar-margin-bottom": "@line-height-computed", 145 | "@navbar-border-radius": "@border-radius-base", 146 | "@navbar-padding-horizontal": "floor((@grid-gutter-width / 2))", 147 | "@navbar-padding-vertical": "((@navbar-height - @line-height-computed) / 2)", 148 | "@navbar-collapse-max-height": "340px", 149 | "@navbar-default-color": "#777", 150 | "@navbar-default-bg": "#f8f8f8", 151 | "@navbar-default-border": "darken(@navbar-default-bg, 6.5%)", 152 | "@navbar-default-link-color": "#777", 153 | "@navbar-default-link-hover-color": "#333", 154 | "@navbar-default-link-hover-bg": "transparent", 155 | "@navbar-default-link-active-color": "#555", 156 | "@navbar-default-link-active-bg": "darken(@navbar-default-bg, 6.5%)", 157 | "@navbar-default-link-disabled-color": "#ccc", 158 | "@navbar-default-link-disabled-bg": "transparent", 159 | "@navbar-default-brand-color": "@navbar-default-link-color", 160 | "@navbar-default-brand-hover-color": "darken(@navbar-default-brand-color, 10%)", 161 | "@navbar-default-brand-hover-bg": "transparent", 162 | "@navbar-default-toggle-hover-bg": "#ddd", 163 | "@navbar-default-toggle-icon-bar-bg": "#888", 164 | "@navbar-default-toggle-border-color": "#ddd", 165 | "@navbar-inverse-color": "lighten(@gray-light, 15%)", 166 | "@navbar-inverse-bg": "#222", 167 | "@navbar-inverse-border": "darken(@navbar-inverse-bg, 10%)", 168 | "@navbar-inverse-link-color": "lighten(@gray-light, 15%)", 169 | "@navbar-inverse-link-hover-color": "#fff", 170 | "@navbar-inverse-link-hover-bg": "transparent", 171 | "@navbar-inverse-link-active-color": "@navbar-inverse-link-hover-color", 172 | "@navbar-inverse-link-active-bg": "darken(@navbar-inverse-bg, 10%)", 173 | "@navbar-inverse-link-disabled-color": "#444", 174 | "@navbar-inverse-link-disabled-bg": "transparent", 175 | "@navbar-inverse-brand-color": "@navbar-inverse-link-color", 176 | "@navbar-inverse-brand-hover-color": "#fff", 177 | "@navbar-inverse-brand-hover-bg": "transparent", 178 | "@navbar-inverse-toggle-hover-bg": "#333", 179 | "@navbar-inverse-toggle-icon-bar-bg": "#fff", 180 | "@navbar-inverse-toggle-border-color": "#333", 181 | "@nav-link-padding": "10px 15px", 182 | "@nav-link-hover-bg": "@gray-lighter", 183 | "@nav-disabled-link-color": "@gray-light", 184 | "@nav-disabled-link-hover-color": "@gray-light", 185 | "@nav-tabs-border-color": "#ddd", 186 | "@nav-tabs-link-hover-border-color": "@gray-lighter", 187 | "@nav-tabs-active-link-hover-bg": "@body-bg", 188 | "@nav-tabs-active-link-hover-color": "@gray", 189 | "@nav-tabs-active-link-hover-border-color": "#ddd", 190 | "@nav-tabs-justified-link-border-color": "#ddd", 191 | "@nav-tabs-justified-active-link-border-color": "@body-bg", 192 | "@nav-pills-border-radius": "@border-radius-base", 193 | "@nav-pills-active-link-hover-bg": "@component-active-bg", 194 | "@nav-pills-active-link-hover-color": "@component-active-color", 195 | "@pagination-color": "@link-color", 196 | "@pagination-bg": "#fff", 197 | "@pagination-border": "#ddd", 198 | "@pagination-hover-color": "@link-hover-color", 199 | "@pagination-hover-bg": "@gray-lighter", 200 | "@pagination-hover-border": "#ddd", 201 | "@pagination-active-color": "#fff", 202 | "@pagination-active-bg": "@brand-primary", 203 | "@pagination-active-border": "@brand-primary", 204 | "@pagination-disabled-color": "@gray-light", 205 | "@pagination-disabled-bg": "#fff", 206 | "@pagination-disabled-border": "#ddd", 207 | "@pager-bg": "@pagination-bg", 208 | "@pager-border": "@pagination-border", 209 | "@pager-border-radius": "15px", 210 | "@pager-hover-bg": "@pagination-hover-bg", 211 | "@pager-active-bg": "@pagination-active-bg", 212 | "@pager-active-color": "@pagination-active-color", 213 | "@pager-disabled-color": "@pagination-disabled-color", 214 | "@jumbotron-padding": "30px", 215 | "@jumbotron-color": "inherit", 216 | "@jumbotron-bg": "@gray-lighter", 217 | "@jumbotron-heading-color": "inherit", 218 | "@jumbotron-font-size": "ceil((@font-size-base * 1.5))", 219 | "@jumbotron-heading-font-size": "ceil((@font-size-base * 4.5))", 220 | "@state-success-text": "#3c763d", 221 | "@state-success-bg": "#dff0d8", 222 | "@state-success-border": "darken(spin(@state-success-bg, -10), 5%)", 223 | "@state-info-text": "#31708f", 224 | "@state-info-bg": "#d9edf7", 225 | "@state-info-border": "darken(spin(@state-info-bg, -10), 7%)", 226 | "@state-warning-text": "#8a6d3b", 227 | "@state-warning-bg": "#fcf8e3", 228 | "@state-warning-border": "darken(spin(@state-warning-bg, -10), 5%)", 229 | "@state-danger-text": "#a94442", 230 | "@state-danger-bg": "#f2dede", 231 | "@state-danger-border": "darken(spin(@state-danger-bg, -10), 5%)", 232 | "@tooltip-max-width": "200px", 233 | "@tooltip-color": "#fff", 234 | "@tooltip-bg": "#000", 235 | "@tooltip-opacity": ".9", 236 | "@tooltip-arrow-width": "5px", 237 | "@tooltip-arrow-color": "@tooltip-bg", 238 | "@popover-bg": "#fff", 239 | "@popover-max-width": "276px", 240 | "@popover-border-color": "rgba(0,0,0,.2)", 241 | "@popover-fallback-border-color": "#ccc", 242 | "@popover-title-bg": "darken(@popover-bg, 3%)", 243 | "@popover-arrow-width": "10px", 244 | "@popover-arrow-color": "@popover-bg", 245 | "@popover-arrow-outer-width": "(@popover-arrow-width + 1)", 246 | "@popover-arrow-outer-color": "fadein(@popover-border-color, 5%)", 247 | "@popover-arrow-outer-fallback-color": "darken(@popover-fallback-border-color, 20%)", 248 | "@label-default-bg": "@gray-light", 249 | "@label-primary-bg": "@brand-primary", 250 | "@label-success-bg": "@brand-success", 251 | "@label-info-bg": "@brand-info", 252 | "@label-warning-bg": "@brand-warning", 253 | "@label-danger-bg": "@brand-danger", 254 | "@label-color": "#fff", 255 | "@label-link-hover-color": "#fff", 256 | "@modal-inner-padding": "15px", 257 | "@modal-title-padding": "15px", 258 | "@modal-title-line-height": "@line-height-base", 259 | "@modal-content-bg": "#fff", 260 | "@modal-content-border-color": "rgba(0,0,0,.2)", 261 | "@modal-content-fallback-border-color": "#999", 262 | "@modal-backdrop-bg": "#000", 263 | "@modal-backdrop-opacity": ".5", 264 | "@modal-header-border-color": "#e5e5e5", 265 | "@modal-footer-border-color": "@modal-header-border-color", 266 | "@modal-lg": "900px", 267 | "@modal-md": "600px", 268 | "@modal-sm": "300px", 269 | "@alert-padding": "15px", 270 | "@alert-border-radius": "@border-radius-base", 271 | "@alert-link-font-weight": "bold", 272 | "@alert-success-bg": "@state-success-bg", 273 | "@alert-success-text": "@state-success-text", 274 | "@alert-success-border": "@state-success-border", 275 | "@alert-info-bg": "@state-info-bg", 276 | "@alert-info-text": "@state-info-text", 277 | "@alert-info-border": "@state-info-border", 278 | "@alert-warning-bg": "@state-warning-bg", 279 | "@alert-warning-text": "@state-warning-text", 280 | "@alert-warning-border": "@state-warning-border", 281 | "@alert-danger-bg": "@state-danger-bg", 282 | "@alert-danger-text": "@state-danger-text", 283 | "@alert-danger-border": "@state-danger-border", 284 | "@progress-bg": "#f5f5f5", 285 | "@progress-bar-color": "#fff", 286 | "@progress-border-radius": "@border-radius-base", 287 | "@progress-bar-bg": "@brand-primary", 288 | "@progress-bar-success-bg": "@brand-success", 289 | "@progress-bar-warning-bg": "@brand-warning", 290 | "@progress-bar-danger-bg": "@brand-danger", 291 | "@progress-bar-info-bg": "@brand-info", 292 | "@list-group-bg": "#fff", 293 | "@list-group-border": "#ddd", 294 | "@list-group-border-radius": "@border-radius-base", 295 | "@list-group-hover-bg": "#f5f5f5", 296 | "@list-group-active-color": "@component-active-color", 297 | "@list-group-active-bg": "@component-active-bg", 298 | "@list-group-active-border": "@list-group-active-bg", 299 | "@list-group-active-text-color": "lighten(@list-group-active-bg, 40%)", 300 | "@list-group-disabled-color": "@gray-light", 301 | "@list-group-disabled-bg": "@gray-lighter", 302 | "@list-group-disabled-text-color": "@list-group-disabled-color", 303 | "@list-group-link-color": "#555", 304 | "@list-group-link-hover-color": "@list-group-link-color", 305 | "@list-group-link-heading-color": "#333", 306 | "@panel-bg": "#fff", 307 | "@panel-body-padding": "15px", 308 | "@panel-heading-padding": "10px 15px", 309 | "@panel-footer-padding": "@panel-heading-padding", 310 | "@panel-border-radius": "@border-radius-base", 311 | "@panel-inner-border": "#ddd", 312 | "@panel-footer-bg": "#f5f5f5", 313 | "@panel-default-text": "@gray-dark", 314 | "@panel-default-border": "#ddd", 315 | "@panel-default-heading-bg": "#f5f5f5", 316 | "@panel-primary-text": "#fff", 317 | "@panel-primary-border": "@brand-primary", 318 | "@panel-primary-heading-bg": "@brand-primary", 319 | "@panel-success-text": "@state-success-text", 320 | "@panel-success-border": "@state-success-border", 321 | "@panel-success-heading-bg": "@state-success-bg", 322 | "@panel-info-text": "@state-info-text", 323 | "@panel-info-border": "@state-info-border", 324 | "@panel-info-heading-bg": "@state-info-bg", 325 | "@panel-warning-text": "@state-warning-text", 326 | "@panel-warning-border": "@state-warning-border", 327 | "@panel-warning-heading-bg": "@state-warning-bg", 328 | "@panel-danger-text": "@state-danger-text", 329 | "@panel-danger-border": "@state-danger-border", 330 | "@panel-danger-heading-bg": "@state-danger-bg", 331 | "@thumbnail-padding": "4px", 332 | "@thumbnail-bg": "@body-bg", 333 | "@thumbnail-border": "#ddd", 334 | "@thumbnail-border-radius": "@border-radius-base", 335 | "@thumbnail-caption-color": "@text-color", 336 | "@thumbnail-caption-padding": "9px", 337 | "@well-bg": "#f5f5f5", 338 | "@well-border": "darken(@well-bg, 7%)", 339 | "@badge-color": "#fff", 340 | "@badge-link-hover-color": "#fff", 341 | "@badge-bg": "@gray-light", 342 | "@badge-active-color": "@link-color", 343 | "@badge-active-bg": "#fff", 344 | "@badge-font-weight": "bold", 345 | "@badge-line-height": "1", 346 | "@badge-border-radius": "10px", 347 | "@breadcrumb-padding-vertical": "8px", 348 | "@breadcrumb-padding-horizontal": "15px", 349 | "@breadcrumb-bg": "#f5f5f5", 350 | "@breadcrumb-color": "#ccc", 351 | "@breadcrumb-active-color": "@gray-light", 352 | "@breadcrumb-separator": "\"/\"", 353 | "@carousel-text-shadow": "0 1px 2px rgba(0,0,0,.6)", 354 | "@carousel-control-color": "#fff", 355 | "@carousel-control-width": "15%", 356 | "@carousel-control-opacity": ".5", 357 | "@carousel-control-font-size": "20px", 358 | "@carousel-indicator-active-bg": "#fff", 359 | "@carousel-indicator-border-color": "#fff", 360 | "@carousel-caption-color": "#fff", 361 | "@close-font-weight": "bold", 362 | "@close-color": "#000", 363 | "@close-text-shadow": "0 1px 0 #fff", 364 | "@code-color": "#c7254e", 365 | "@code-bg": "#f9f2f4", 366 | "@kbd-color": "#fff", 367 | "@kbd-bg": "#333", 368 | "@pre-bg": "#f5f5f5", 369 | "@pre-color": "@gray-dark", 370 | "@pre-border-color": "#ccc", 371 | "@pre-scrollable-max-height": "340px", 372 | "@component-offset-horizontal": "180px", 373 | "@text-muted": "@gray-light", 374 | "@abbr-border-color": "@gray-light", 375 | "@headings-small-color": "@gray-light", 376 | "@blockquote-small-color": "@gray-light", 377 | "@blockquote-font-size": "(@font-size-base * 1.25)", 378 | "@blockquote-border-color": "@gray-lighter", 379 | "@page-header-border-color": "@gray-lighter", 380 | "@dl-horizontal-offset": "@component-offset-horizontal", 381 | "@dl-horizontal-breakpoint": "@grid-float-breakpoint", 382 | "@hr-border": "@gray-lighter" 383 | }, 384 | "css": [ 385 | "buttons.less", 386 | "close.less", 387 | "component-animations.less", 388 | "modals.less" 389 | ], 390 | "js": [ 391 | "modal.js", 392 | "collapse.js", 393 | "transition.js" 394 | ], 395 | "customizerUrl": "http://getbootstrap.com/customize/?id=fa8721f4276e60c899507a3ed4b05d9b" 396 | } -------------------------------------------------------------------------------- /assets/svg/bonjour.svg: -------------------------------------------------------------------------------- 1 | 11 -------------------------------------------------------------------------------- /assets/bootstrap-modal/js/bootstrap.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 (typeof jQuery === 'undefined') { 12 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 13 | } 14 | +function ($) { 15 | 'use strict'; 16 | var version = $.fn.jquery.split(' ')[0].split('.') 17 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { 18 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') 19 | } 20 | }(jQuery); 21 | 22 | /* ======================================================================== 23 | * Bootstrap: modal.js v3.3.7 24 | * http://getbootstrap.com/javascript/#modals 25 | * ======================================================================== 26 | * Copyright 2011-2016 Twitter, Inc. 27 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 28 | * ======================================================================== */ 29 | 30 | 31 | +function ($) { 32 | 'use strict'; 33 | 34 | // MODAL CLASS DEFINITION 35 | // ====================== 36 | 37 | var Modal = function (element, options) { 38 | this.options = options 39 | this.$body = $(document.body) 40 | this.$element = $(element) 41 | this.$dialog = this.$element.find('.modal-dialog') 42 | this.$backdrop = null 43 | this.isShown = null 44 | this.originalBodyPad = null 45 | this.scrollbarWidth = 0 46 | this.ignoreBackdropClick = false 47 | 48 | if (this.options.remote) { 49 | this.$element 50 | .find('.modal-content') 51 | .load(this.options.remote, $.proxy(function () { 52 | this.$element.trigger('loaded.bs.modal') 53 | }, this)) 54 | } 55 | } 56 | 57 | Modal.VERSION = '3.3.7' 58 | 59 | Modal.TRANSITION_DURATION = 300 60 | Modal.BACKDROP_TRANSITION_DURATION = 150 61 | 62 | Modal.DEFAULTS = { 63 | backdrop: true, 64 | keyboard: true, 65 | show: true 66 | } 67 | 68 | Modal.prototype.toggle = function (_relatedTarget) { 69 | return this.isShown ? this.hide() : this.show(_relatedTarget) 70 | } 71 | 72 | Modal.prototype.show = function (_relatedTarget) { 73 | var that = this 74 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 75 | 76 | this.$element.trigger(e) 77 | 78 | if (this.isShown || e.isDefaultPrevented()) return 79 | 80 | this.isShown = true 81 | 82 | this.checkScrollbar() 83 | this.setScrollbar() 84 | this.$body.addClass('modal-open') 85 | 86 | this.escape() 87 | this.resize() 88 | 89 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 90 | 91 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 92 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 93 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 94 | }) 95 | }) 96 | 97 | this.backdrop(function () { 98 | var transition = $.support.transition && that.$element.hasClass('fade') 99 | 100 | if (!that.$element.parent().length) { 101 | that.$element.appendTo(that.$body) // don't move modals dom position 102 | } 103 | 104 | that.$element 105 | .show() 106 | .scrollTop(0) 107 | 108 | that.adjustDialog() 109 | 110 | if (transition) { 111 | that.$element[0].offsetWidth // force reflow 112 | } 113 | 114 | that.$element.addClass('in') 115 | 116 | that.enforceFocus() 117 | 118 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 119 | 120 | transition ? 121 | that.$dialog // wait for modal to slide in 122 | .one('bsTransitionEnd', function () { 123 | that.$element.trigger('focus').trigger(e) 124 | }) 125 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 126 | that.$element.trigger('focus').trigger(e) 127 | }) 128 | } 129 | 130 | Modal.prototype.hide = function (e) { 131 | if (e) e.preventDefault() 132 | 133 | e = $.Event('hide.bs.modal') 134 | 135 | this.$element.trigger(e) 136 | 137 | if (!this.isShown || e.isDefaultPrevented()) return 138 | 139 | this.isShown = false 140 | 141 | this.escape() 142 | this.resize() 143 | 144 | $(document).off('focusin.bs.modal') 145 | 146 | this.$element 147 | .removeClass('in') 148 | .off('click.dismiss.bs.modal') 149 | .off('mouseup.dismiss.bs.modal') 150 | 151 | this.$dialog.off('mousedown.dismiss.bs.modal') 152 | 153 | $.support.transition && this.$element.hasClass('fade') ? 154 | this.$element 155 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 156 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 157 | this.hideModal() 158 | } 159 | 160 | Modal.prototype.enforceFocus = function () { 161 | $(document) 162 | .off('focusin.bs.modal') // guard against infinite focus loop 163 | .on('focusin.bs.modal', $.proxy(function (e) { 164 | if (document !== e.target && 165 | this.$element[0] !== e.target && 166 | !this.$element.has(e.target).length) { 167 | this.$element.trigger('focus') 168 | } 169 | }, this)) 170 | } 171 | 172 | Modal.prototype.escape = function () { 173 | if (this.isShown && this.options.keyboard) { 174 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 175 | e.which == 27 && this.hide() 176 | }, this)) 177 | } else if (!this.isShown) { 178 | this.$element.off('keydown.dismiss.bs.modal') 179 | } 180 | } 181 | 182 | Modal.prototype.resize = function () { 183 | if (this.isShown) { 184 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 185 | } else { 186 | $(window).off('resize.bs.modal') 187 | } 188 | } 189 | 190 | Modal.prototype.hideModal = function () { 191 | var that = this 192 | this.$element.hide() 193 | this.backdrop(function () { 194 | that.$body.removeClass('modal-open') 195 | that.resetAdjustments() 196 | that.resetScrollbar() 197 | that.$element.trigger('hidden.bs.modal') 198 | }) 199 | } 200 | 201 | Modal.prototype.removeBackdrop = function () { 202 | this.$backdrop && this.$backdrop.remove() 203 | this.$backdrop = null 204 | } 205 | 206 | Modal.prototype.backdrop = function (callback) { 207 | var that = this 208 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 209 | 210 | if (this.isShown && this.options.backdrop) { 211 | var doAnimate = $.support.transition && animate 212 | 213 | this.$backdrop = $(document.createElement('div')) 214 | .addClass('modal-backdrop ' + animate) 215 | .appendTo(this.$body) 216 | 217 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 218 | if (this.ignoreBackdropClick) { 219 | this.ignoreBackdropClick = false 220 | return 221 | } 222 | if (e.target !== e.currentTarget) return 223 | this.options.backdrop == 'static' 224 | ? this.$element[0].focus() 225 | : this.hide() 226 | }, this)) 227 | 228 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 229 | 230 | this.$backdrop.addClass('in') 231 | 232 | if (!callback) return 233 | 234 | doAnimate ? 235 | this.$backdrop 236 | .one('bsTransitionEnd', callback) 237 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 238 | callback() 239 | 240 | } else if (!this.isShown && this.$backdrop) { 241 | this.$backdrop.removeClass('in') 242 | 243 | var callbackRemove = function () { 244 | that.removeBackdrop() 245 | callback && callback() 246 | } 247 | $.support.transition && this.$element.hasClass('fade') ? 248 | this.$backdrop 249 | .one('bsTransitionEnd', callbackRemove) 250 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 251 | callbackRemove() 252 | 253 | } else if (callback) { 254 | callback() 255 | } 256 | } 257 | 258 | // these following methods are used to handle overflowing modals 259 | 260 | Modal.prototype.handleUpdate = function () { 261 | this.adjustDialog() 262 | } 263 | 264 | Modal.prototype.adjustDialog = function () { 265 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 266 | 267 | this.$element.css({ 268 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 269 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 270 | }) 271 | } 272 | 273 | Modal.prototype.resetAdjustments = function () { 274 | this.$element.css({ 275 | paddingLeft: '', 276 | paddingRight: '' 277 | }) 278 | } 279 | 280 | Modal.prototype.checkScrollbar = function () { 281 | var fullWindowWidth = window.innerWidth 282 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 283 | var documentElementRect = document.documentElement.getBoundingClientRect() 284 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 285 | } 286 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 287 | this.scrollbarWidth = this.measureScrollbar() 288 | } 289 | 290 | Modal.prototype.setScrollbar = function () { 291 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 292 | this.originalBodyPad = document.body.style.paddingRight || '' 293 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 294 | } 295 | 296 | Modal.prototype.resetScrollbar = function () { 297 | this.$body.css('padding-right', this.originalBodyPad) 298 | } 299 | 300 | Modal.prototype.measureScrollbar = function () { // thx walsh 301 | var scrollDiv = document.createElement('div') 302 | scrollDiv.className = 'modal-scrollbar-measure' 303 | this.$body.append(scrollDiv) 304 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 305 | this.$body[0].removeChild(scrollDiv) 306 | return scrollbarWidth 307 | } 308 | 309 | 310 | // MODAL PLUGIN DEFINITION 311 | // ======================= 312 | 313 | function Plugin(option, _relatedTarget) { 314 | return this.each(function () { 315 | var $this = $(this) 316 | var data = $this.data('bs.modal') 317 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 318 | 319 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 320 | if (typeof option == 'string') data[option](_relatedTarget) 321 | else if (options.show) data.show(_relatedTarget) 322 | }) 323 | } 324 | 325 | var old = $.fn.modal 326 | 327 | $.fn.modal = Plugin 328 | $.fn.modal.Constructor = Modal 329 | 330 | 331 | // MODAL NO CONFLICT 332 | // ================= 333 | 334 | $.fn.modal.noConflict = function () { 335 | $.fn.modal = old 336 | return this 337 | } 338 | 339 | 340 | // MODAL DATA-API 341 | // ============== 342 | 343 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 344 | var $this = $(this) 345 | var href = $this.attr('href') 346 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 347 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 348 | 349 | if ($this.is('a')) e.preventDefault() 350 | 351 | $target.one('show.bs.modal', function (showEvent) { 352 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 353 | $target.one('hidden.bs.modal', function () { 354 | $this.is(':visible') && $this.trigger('focus') 355 | }) 356 | }) 357 | Plugin.call($target, option, this) 358 | }) 359 | 360 | }(jQuery); 361 | 362 | /* ======================================================================== 363 | * Bootstrap: collapse.js v3.3.7 364 | * http://getbootstrap.com/javascript/#collapse 365 | * ======================================================================== 366 | * Copyright 2011-2016 Twitter, Inc. 367 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 368 | * ======================================================================== */ 369 | 370 | /* jshint latedef: false */ 371 | 372 | +function ($) { 373 | 'use strict'; 374 | 375 | // COLLAPSE PUBLIC CLASS DEFINITION 376 | // ================================ 377 | 378 | var Collapse = function (element, options) { 379 | this.$element = $(element) 380 | this.options = $.extend({}, Collapse.DEFAULTS, options) 381 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 382 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 383 | this.transitioning = null 384 | 385 | if (this.options.parent) { 386 | this.$parent = this.getParent() 387 | } else { 388 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 389 | } 390 | 391 | if (this.options.toggle) this.toggle() 392 | } 393 | 394 | Collapse.VERSION = '3.3.7' 395 | 396 | Collapse.TRANSITION_DURATION = 350 397 | 398 | Collapse.DEFAULTS = { 399 | toggle: true 400 | } 401 | 402 | Collapse.prototype.dimension = function () { 403 | var hasWidth = this.$element.hasClass('width') 404 | return hasWidth ? 'width' : 'height' 405 | } 406 | 407 | Collapse.prototype.show = function () { 408 | if (this.transitioning || this.$element.hasClass('in')) return 409 | 410 | var activesData 411 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 412 | 413 | if (actives && actives.length) { 414 | activesData = actives.data('bs.collapse') 415 | if (activesData && activesData.transitioning) return 416 | } 417 | 418 | var startEvent = $.Event('show.bs.collapse') 419 | this.$element.trigger(startEvent) 420 | if (startEvent.isDefaultPrevented()) return 421 | 422 | if (actives && actives.length) { 423 | Plugin.call(actives, 'hide') 424 | activesData || actives.data('bs.collapse', null) 425 | } 426 | 427 | var dimension = this.dimension() 428 | 429 | this.$element 430 | .removeClass('collapse') 431 | .addClass('collapsing')[dimension](0) 432 | .attr('aria-expanded', true) 433 | 434 | this.$trigger 435 | .removeClass('collapsed') 436 | .attr('aria-expanded', true) 437 | 438 | this.transitioning = 1 439 | 440 | var complete = function () { 441 | this.$element 442 | .removeClass('collapsing') 443 | .addClass('collapse in')[dimension]('') 444 | this.transitioning = 0 445 | this.$element 446 | .trigger('shown.bs.collapse') 447 | } 448 | 449 | if (!$.support.transition) return complete.call(this) 450 | 451 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 452 | 453 | this.$element 454 | .one('bsTransitionEnd', $.proxy(complete, this)) 455 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 456 | } 457 | 458 | Collapse.prototype.hide = function () { 459 | if (this.transitioning || !this.$element.hasClass('in')) return 460 | 461 | var startEvent = $.Event('hide.bs.collapse') 462 | this.$element.trigger(startEvent) 463 | if (startEvent.isDefaultPrevented()) return 464 | 465 | var dimension = this.dimension() 466 | 467 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 468 | 469 | this.$element 470 | .addClass('collapsing') 471 | .removeClass('collapse in') 472 | .attr('aria-expanded', false) 473 | 474 | this.$trigger 475 | .addClass('collapsed') 476 | .attr('aria-expanded', false) 477 | 478 | this.transitioning = 1 479 | 480 | var complete = function () { 481 | this.transitioning = 0 482 | this.$element 483 | .removeClass('collapsing') 484 | .addClass('collapse') 485 | .trigger('hidden.bs.collapse') 486 | } 487 | 488 | if (!$.support.transition) return complete.call(this) 489 | 490 | this.$element 491 | [dimension](0) 492 | .one('bsTransitionEnd', $.proxy(complete, this)) 493 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 494 | } 495 | 496 | Collapse.prototype.toggle = function () { 497 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 498 | } 499 | 500 | Collapse.prototype.getParent = function () { 501 | return $(this.options.parent) 502 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 503 | .each($.proxy(function (i, element) { 504 | var $element = $(element) 505 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 506 | }, this)) 507 | .end() 508 | } 509 | 510 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 511 | var isOpen = $element.hasClass('in') 512 | 513 | $element.attr('aria-expanded', isOpen) 514 | $trigger 515 | .toggleClass('collapsed', !isOpen) 516 | .attr('aria-expanded', isOpen) 517 | } 518 | 519 | function getTargetFromTrigger($trigger) { 520 | var href 521 | var target = $trigger.attr('data-target') 522 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 523 | 524 | return $(target) 525 | } 526 | 527 | 528 | // COLLAPSE PLUGIN DEFINITION 529 | // ========================== 530 | 531 | function Plugin(option) { 532 | return this.each(function () { 533 | var $this = $(this) 534 | var data = $this.data('bs.collapse') 535 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 536 | 537 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 538 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 539 | if (typeof option == 'string') data[option]() 540 | }) 541 | } 542 | 543 | var old = $.fn.collapse 544 | 545 | $.fn.collapse = Plugin 546 | $.fn.collapse.Constructor = Collapse 547 | 548 | 549 | // COLLAPSE NO CONFLICT 550 | // ==================== 551 | 552 | $.fn.collapse.noConflict = function () { 553 | $.fn.collapse = old 554 | return this 555 | } 556 | 557 | 558 | // COLLAPSE DATA-API 559 | // ================= 560 | 561 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 562 | var $this = $(this) 563 | 564 | if (!$this.attr('data-target')) e.preventDefault() 565 | 566 | var $target = getTargetFromTrigger($this) 567 | var data = $target.data('bs.collapse') 568 | var option = data ? 'toggle' : $this.data() 569 | 570 | Plugin.call($target, option) 571 | }) 572 | 573 | }(jQuery); 574 | 575 | /* ======================================================================== 576 | * Bootstrap: transition.js v3.3.7 577 | * http://getbootstrap.com/javascript/#transitions 578 | * ======================================================================== 579 | * Copyright 2011-2016 Twitter, Inc. 580 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 581 | * ======================================================================== */ 582 | 583 | 584 | +function ($) { 585 | 'use strict'; 586 | 587 | // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) 588 | // ============================================================ 589 | 590 | function transitionEnd() { 591 | var el = document.createElement('bootstrap') 592 | 593 | var transEndEventNames = { 594 | WebkitTransition : 'webkitTransitionEnd', 595 | MozTransition : 'transitionend', 596 | OTransition : 'oTransitionEnd otransitionend', 597 | transition : 'transitionend' 598 | } 599 | 600 | for (var name in transEndEventNames) { 601 | if (el.style[name] !== undefined) { 602 | return { end: transEndEventNames[name] } 603 | } 604 | } 605 | 606 | return false // explicit for ie8 ( ._.) 607 | } 608 | 609 | // http://blog.alexmaccaw.com/css-transitions 610 | $.fn.emulateTransitionEnd = function (duration) { 611 | var called = false 612 | var $el = this 613 | $(this).one('bsTransitionEnd', function () { called = true }) 614 | var callback = function () { if (!called) $($el).trigger($.support.transition.end) } 615 | setTimeout(callback, duration) 616 | return this 617 | } 618 | 619 | $(function () { 620 | $.support.transition = transitionEnd() 621 | 622 | if (!$.support.transition) return 623 | 624 | $.event.special.bsTransitionEnd = { 625 | bindType: $.support.transition.end, 626 | delegateType: $.support.transition.end, 627 | handle: function (e) { 628 | if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) 629 | } 630 | } 631 | }) 632 | 633 | }(jQuery); 634 | --------------------------------------------------------------------------------