├── pi.ico ├── filedLinker.jpg ├── package.json ├── composer.json ├── LICENCE ├── fieldsLinker.css ├── README.md ├── index.html └── fieldsLinker.js /pi.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippeMarcMeyer/FieldsLinker/HEAD/pi.ico -------------------------------------------------------------------------------- /filedLinker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhilippeMarcMeyer/FieldsLinker/HEAD/filedLinker.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fieldslinker", 3 | "version": "1.0.4", 4 | "description": "jquery plugin to match file columns with DB columns by drawing arrows", 5 | "homepage": "https://philippemarcmeyer.github.io/#FieldsLinker", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/PhilippeMarcMeyer/FieldsLinker.git" 9 | }, 10 | "funding": "", 11 | "author": "Philippemarcmeyer", 12 | "license": "MIT", 13 | "scripts": {}, 14 | "keywords": ["jquery", "plugins", "canvas", "list", "lines", "jquery-plugin"], 15 | "main": "fieldsLinker.js", 16 | "files": [ 17 | "fieldsLinker.css", 18 | "fieldsLinker.js" 19 | ], 20 | "devDependencies": {}, 21 | "semistandard": { 22 | "ignore": [], 23 | "globals": [] 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/PhilippeMarcMeyer/FieldsLinker/issues" 27 | } 28 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "philippemarcmeyer/fieldslinker", 3 | "description": "Designed for matching files headers to database fields during the process importing. Allows drawing links between elements of 2 lists.", 4 | "keywords": ["jquery", "plugins", "canvas", "list", "lines", "jquery-plugin"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Phillipe Marc Mayer", 10 | "email": "pmg.meyer@gmail.com", 11 | "homepage": "https://philippemarcmeyer.github.io", 12 | "role": "Maintainer" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/PhilippeMarcMeyer/FieldsLinker/issues", 17 | "source": "https://github.com/PhilippeMarcMeyer/FieldsLinker", 18 | "docs": "https://github.com/PhilippeMarcMeyer/FieldsLinker" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 philippe MEYER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | © 2020 GitHub, Inc. 23 | -------------------------------------------------------------------------------- /fieldsLinker.css: -------------------------------------------------------------------------------- 1 | .fieldsLinker div { 2 | font-weight: bold; 3 | } 4 | 5 | .fieldsLinker ul { 6 | list-style: none; 7 | margin-left: 0px; 8 | padding-left: 0px; 9 | font-weight: normal; 10 | } 11 | 12 | tr td { 13 | padding-left: 10px; 14 | } 15 | 16 | tr td:not(:last-child) { 17 | border-right: 1px solid grey; 18 | } 19 | 20 | .fieldsLinker li { 21 | border: solid 1px grey; 22 | padding: 5px 2px 5px 8px; 23 | margin-top: 7px; 24 | margin-bottom: 8px; 25 | user-select: none; 26 | -moz-user-select: none; 27 | -webkit-user-select: none; 28 | -ms-user-select: none; 29 | cursor: pointer; 30 | background-color: #fff; 31 | background: linear-gradient(#ffffff, #e9e9e9); 32 | color: black; 33 | border-radius: 0px; 34 | text-overflow: ellipsis; 35 | overflow: hidden; 36 | } 37 | 38 | .fieldsLinker li.selected { 39 | -webkit-filter: invert(100%); 40 | filter: invert(100%); 41 | } 42 | 43 | .fieldsLinker li[data-mandatory='true'] { 44 | background-color: #D7504C; 45 | background: linear-gradient(#D7504C, #C12F2B); 46 | color: white; 47 | } 48 | 49 | .fieldsLinker li.inactive { 50 | opacity: 0.5; 51 | } 52 | 53 | .fieldsLinker li.linkOver { 54 | border: solid 2px transparent; /*plus 1px*/ 55 | padding: 4px 1px 4px 7px; /* minus 1px*/ 56 | } 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FieldsLinker 2 | 3 | Designed for matching files headers to database fields during the process importing. 4 | Allows drawing links between elements of 2 lists (headers of the file on the left, column names on the right) 5 | and getting back the result in a js object 6 | 7 | Given 2 lists : for instance one from a text import, the second listing the fields a db table 8 | the jquery plugin allows you to draw and save links between the 2 lists 9 | 10 | You can link on a one to one basis or on a one to many basis. Fields can be declared as mandatory the result reporting an error in case there are not filled. 11 | 12 | Store previous links and headers to automaticaly recognize files 13 | reload links if you need it with the existingLinks property : 14 | 15 | example : 16 | "existingLinks": [{ "from": "lastName", "to": "last_name" }, { "from": "firstName", "to": "first_name" }, { "from": "role", "to": "jobTitle" }] 17 | 18 | Side note : If FieldsLinker interest you, but you need it for other use case than file importation, check LinksMaker plugin which works the same but can allow more than 2 lists (you then compare lists two by two, by selecting them). this plugin works much the same as Fieldslinker and this one could evolve in any direction that meets your needs. 19 | 20 | ## Usage 21 | 22 | * param 1 : action 23 | * param 2 : input 24 | 25 | ### Available actions : 26 | 27 | * init : 28 |
 29 |  fieldLinks=$("#bonds").fieldsLinker("init",input);
 30 | 
31 | 32 | Example of input : 33 | 34 |
 35 | 
 36 | var input =	{
 37 | 				"options": {
 38 | 					"lineStyle": "square-ends",
 39 | 					"buttonErase": "Erase Links"
 40 | 				},
 41 | 				"listA": {
 42 | 					"name": "columns in files",
 43 | 					"list": [
 44 | 						"firstName",
 45 | 						"lastName",
 46 | 						"phone",
 47 | 						"email",
 48 | 						"role",
 49 | 						"Birthday",
 50 | 						"Adress",
 51 | 						"Sales"
 52 | 					]
 53 | 				},
 54 | 				"listB": {
 55 | 					"name": "Fields available",
 56 | 					"list": [
 57 | 						"Id",
 58 | 						"Company",
 59 | 						"jobTitle",
 60 | 						"adress 1",
 61 | 						"adress 2",
 62 | 						"first_name",
 63 | 						"last_name",
 64 | 						"email_adress",
 65 | 						"Phone number"
 66 | 					]
 67 | 				},
 68 | 				"existingLinks": [{
 69 | 					"from": "lastName",
 70 | 					"to": "last_name"
 71 | 				}, {
 72 | 					"from": "firstName",
 73 | 					"to": "first_name"
 74 | 				}, {
 75 | 					"from": "role",
 76 | 					"to": "jobTitle"
 77 | 				}]
 78 | 
 79 | 			}
 80 | 
81 | 82 | Available input.options for "init" action : 83 | * options.className : you may change the default className ('fieldsLinker') by another class you provide 84 | * options.whiteSpace : default is 'nowrap' => you can change it to 'normal' to allow lists element to go to the next line (usefull if you have long names separated by spaces or ][) 85 | * lineStyle : 'straight' or 'square-ends' : will change the way the links are drawn between the items of the 2 lists, default is 'straight' 86 | * associationMode : 'oneToOne' or 'manyToMany' : allow links to be set on a one to one relationship or many to many 87 | * handleColor : your own colors default is '#CF0000,#00AD00,#0000AD,#FF4500,#00ADAD,#AD00AD,#582900,#FFCC00,#000000,#33FFCC' 88 | * mobileClickIt : true or false : simpler mode on mobile device where you click on item A and then click on item B to set the link (mobileClickIt option is automatic on touch devices) 89 | 90 | 91 | * changeParameters : 92 | example : 93 |
 94 | fieldLinks.fieldsLinker("changeParameters",{"whiteSpace":"normal"});
 95 | 
96 | where the second parameter holds an options object to modify the original input.options attribute 97 | 98 | * eraseLinks : drop all links 99 | 100 | * getLinks : returns all the links 101 | 102 | * disable : disables the whole stuff 103 | 104 | * enable : enables it again 105 | 106 | 107 | https://philippemarcmeyer.github.io/FieldsLinker/index.html 108 | 109 | Also available on https://packagist.org/packages/philippemarcmeyer/fieldslinker ++++ 110 | 111 | ### v1.04 : Just a wording fix for npm 112 | 113 | ### v1.03 : bug fix : no drag and drop when FieldsLinker is disabled 114 | 115 | ### v1.02 : Improvement : adding an id to the root dom element if not provided 116 | 117 | ### v1.01 : Bug correction : the manual sorting by drap and drop now works 118 | 119 | ### v1.00 : Jquery 3.5 Compatibility 120 | 121 | ### v0.96 : Removing filter option and alternateview 122 | Sorry for the contributors : Fieldlinkers was diverging too much from the original intent and use case : to be a filter between a file to import and a database 123 | 124 | I made another plugin LinksMaker, which works the same but can allow more than 2 lists (you then compare lists two by two, by selecting them) 125 | 126 | this plugin works much the same as Fieldslinker 127 | 128 | this one could evolve in any direction ! 129 | 130 | ### v0.95 : rewritten for multiples instances in mind 131 | 132 | ### v0.92 133 | 134 | Introducing new option : whiteSpace 135 | normal,nowrap,pre,pre-wrap,pre-line,break-spaces default => nowrap 136 | 137 | ### v0.91 138 | 139 | Fix `mobileClickIt` option: 140 | 141 | - if set to `true`, will use mobile mode, regardless of touch screen mode. 142 | - fix crash modeB `null` value 143 | - if selected (clicked), use CSS to invert element as visual feedback. 144 | - mobileClickIt option is automatic on touch devices 145 | 146 | ### v0.90 147 | Canvas calculations fixes and other various fixes. 148 | Filter mode (removed in v 0.96) 149 | 150 | ### v0.89 151 | 152 | ![screen shot](https://raw.githubusercontent.com/PhilippeMarcMeyer/FieldsLinker/master/filedLinker.jpg) 153 | 154 | v 0.89 : Corrected a bug that corrupted the links array of objects detected by flartet on github 155 | 156 | v 0.88 : New display mode : idea by Naveen nsirangu => show links between two "tables" linked by ids like a join in sql. instead of headers names, objects are provided (removed in v 0.96) 157 | 158 | v 0.87 : New option for touch devices {"mobileClickIt":true} : idea by Norman Tomlins => make links more easily on touch devices just by clicking 159 | 160 | v 0.86 : Modifications if Bootstrap is not available : tooltip are basic html titles and the links are horizontally centered 161 | 162 | v 0.85 : Mobile friendly (requested by a user) 163 | 164 | v 0.80 : I've been cleaning code in order to get something much simpler (nearly the same code than LinksMaker) it allowed me to implement drag and drop to reorder the items in the lists, which is convenient expecially if you've got long lists. 165 | options dropped in this version : autoDetect and byName : no more autodetect and the links are given back by name only not by order. 166 | the link over effect has also been dropped. Important : oneToMany:"on|off" becomes associationMode: "oneToOne|manyToMany" 167 | So you don't have one to many relation + one to one, but instead many to many and one to one relations. 168 | 169 | v 0.72 : [Cancelled] New lineStyle : square-ends-dotted : white dots at the beginings and ends of lines on hover 170 | 171 | v 0.60 : 172 | Mandatory fields show a tooltip (mandatoryErrorMessage) 173 | disable/enable : disable/enable everything, the global opacity is set to 0.5 174 | 175 | v 0.45 : Mandatory fields. the css is now in his own file. 176 | 177 | v 0.41 : the colors don't change when links are deleted and the color of the link while drawing is consistent with the final result 178 | 179 | v 0.4 : new option allowing to link one to many 180 | 181 | v 0.3 : 182 | FieldsLinker becomes responsive ! 183 | 184 | v 0.2 : 185 | You can choose beetween an output with positions or names 186 | Colours can be re-defined and lines come in 2 flavours 187 | You may input somme links from a previous session 188 | Auto-detect feature helps you find part of the links (optional) 189 | Optional "Erase Links" button 190 | Works in Chrome, Chromium, Opera, Firefox and IE (9+) 191 | 192 | v 0.1 : the lines are drawn while dragging over the canvas and the horizontal middle points of the cells are calculated 193 | no parameters anymore 194 | 195 | Tested on : Chrome, Firefox, Chromium, IE, Opéra : OK for Chrome, Firefox and Opera 196 | todo : horizontal middle points of the cells are wrong on IE 197 | and on chromium the canvas zone (between the 2 lists) is selected during the dragging process which is not aesthetic. 198 | 199 | v0.01 : first commit : todo => parameters should not be necessary cellHeight,List1Width,canvasWidth,List2Width and should be calculated 200 | 201 | https://philippemarcmeyer.github.io/FieldsLinker/index.html 202 | 203 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FieldsLinker 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 187 | 188 | 189 | 206 |
207 | 208 | 209 |
210 |
FieldsLinker
 v 1.04 211 |
New option for touch devices {"mobileClickIt":true} : idea by Norman Tomlins , make links more easily on touch devices just by clicking 212 |

Allows to link elements of list A to elements of list B and returns the links in a js object

213 | 226 |
227 |
228 |
229 |
230 | Change options : 231 |
232 | 233 | 234 |
235 | 236 |
237 | 238 | 239 |
240 |
241 | 242 | 243 |
244 |
245 | 246 |
247 |
248 | 249 | 250 | 251 |
252 | 253 |
254 |
255 |
256 |
257 |
258 |
259 | 260 |
261 | 262 |
263 | 264 |   265 |

266 |
267 | 268 |
269 | 270 | 271 | 272 | 273 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 385 | 386 | -------------------------------------------------------------------------------- /fieldsLinker.js: -------------------------------------------------------------------------------- 1 | /* 2 | https://github.com/PhilippeMarcMeyer/FieldsLinker v 1.04 3 | 4 | v 1.04 : Just a wording fix for npm 5 | v 1.03 : bug fix : no drag and drop when FieldsLinker is disabled 6 | v 1.02 : Improvement : adding an id to the root dom element if not provided 7 | v 1.01 : Bug correction : the manual sorting by drap and drop now works 8 | v 1.00 : jquery 3.5 compatibility 9 | v 0.96 : Remove filter option and alternateview : theses modes have nothing to do with the original concept 10 | v 0.95 : rewritten for multiples instances in mind 11 | v 0.92 : introducing new option : whiteSpace 12 | v 0.91 : fix mobileClickIt if set, add selected css classes, automatic mobileClickIt on touch devices 13 | v 0.90 : Code beautified by flartet 14 | v 0.89 : Corrected a bug that corrupted the links array of objects detected by flartet on github 15 | v 0.88 : New display mode : idea by Naveen nsirangu => show links between two "tables" linked by ids like a join in sql. instead of headers names, objects ar provided 16 | v 0.87 : New option for touch devices {"mobileClickIt":true} : idea by Norman Tomlins => make links more easily on touch devices just by clicking 17 | */ 18 | let fieldsLinkerMemory = []; 19 | 20 | function getRandomInt(max) { 21 | return Math.floor(Math.random() * Math.floor(max)); 22 | } 23 | 24 | ;(function($) { 25 | $.fn.fieldsLinker = function (action, input) { 26 | factory = this; 27 | if (action == 'init') { 28 | factory.selector = factory[0]; 29 | 30 | if(!factory.selector.id){ 31 | factory.selector.id = "FLinkerId_" + getRandomInt(1024000000); 32 | } 33 | 34 | factory.work = new FieldsLinker(factory.selector); 35 | if(fieldsLinkerMemory.length == 0){ 36 | fieldsLinkerMemory.push({"selector":factory.selector,"factory":factory}); 37 | }else{ 38 | let found = false; 39 | fieldsLinkerMemory.forEach(function(x,i){ 40 | if(x.selector == factory.selector){ 41 | found = true; 42 | fieldsLinkerMemory[i].factory = factory; 43 | } 44 | }); 45 | if(!found){ 46 | fieldsLinkerMemory.push({"selector":factory.selector,"factory":factory}); 47 | } 48 | } 49 | factory.work.init(input); 50 | factory.work.deduplicate(); 51 | factory.work.setGlobalRedraw(); 52 | factory.work.readUserPreferences(); 53 | factory.work.fillChosenLists(); 54 | factory.work.makeDropDownForLists(); 55 | factory.work.drawColumnsAtLoadTime(); 56 | factory.work.drawColumnsContentA(); 57 | factory.work.drawColumnsContentB(); 58 | factory.work.createCanvas(); 59 | factory.work.setListeners(); 60 | factory.work.changeSelects(); 61 | factory.work.manageExistingLinks(); 62 | factory.work.manageResize(); 63 | factory.work.draw(); 64 | 65 | return factory; 66 | 67 | } else if (action == 'eraseLinks') { 68 | factory.work.eraseLinks(); 69 | factory.work.draw(); 70 | return factory; 71 | 72 | } else if (action === 'getLinks') { 73 | return factory.work.getLinks(); 74 | 75 | } else if (action === 'changeParameters') { 76 | factory.work.changeParameters(input); 77 | 78 | } else if (action == 'disable') { 79 | factory.work.enable(false); 80 | return (factory); 81 | 82 | } else if (action == 'enable') { 83 | factory.work.enable(true); 84 | return (factory); 85 | } else { 86 | console.log(factory.work.errMsg + 'no action parameter provided (param 1)'); 87 | } 88 | }; 89 | }(jQuery)); 90 | 91 | 92 | function FieldsLinker(selector){ 93 | this.selector = selector; 94 | this.$root = $(this.selector); 95 | this.FL_Factory_Lists = null; 96 | this.FL_Original_Factory_Lists = null; 97 | this.bootstrap_enabled = (typeof $().modal == 'function'); 98 | this.errMsg = 'fieldsLinker error : '; 99 | this.data = {}; 100 | this.listsNr = 0; 101 | this.listNames = []; 102 | this.listA = []; 103 | this.listB = []; 104 | this.chosenListA = ''; 105 | this.chosenListB = ''; 106 | this.keyNameA = ''; 107 | this.keyNameB = ''; 108 | this.dropDownForLists = null; 109 | this.$leftDiv; 110 | this.$midDiv; 111 | this.$rightDiv; 112 | this.$canvas; 113 | this.$main; 114 | this.$btn; 115 | this.$ulLeft; 116 | this.$ulRight; 117 | this.$filterDiv1; 118 | this.$filterDiv2; 119 | this.canvasId = ''; 120 | this.canvasCtx = null; 121 | this.canvasWidth = 0; 122 | this.canvasHeight = 0; 123 | this.canvasPtr = null; 124 | this.mandatoryErrorMessage = 'This field is mandatory'; 125 | this.mandatoryTooltips = true; 126 | this.onError = false; 127 | this.className = 'fieldsLinker'; 128 | this.linksByName = []; 129 | this.ListHeights1 = []; 130 | this.ListHeights2 = []; 131 | this.move = null; 132 | this.that = null; 133 | this.lineStyle = 'straight'; // straight or square-ends 134 | this.handleColor = '#CF0000,#00AD00,#0000AD,#FF4500,#00ADAD,#AD00AD,#582900,#FFCC00,#000000,#33FFCC'.split(','); 135 | this.lineColor = 'black'; 136 | this.associationMode = 'oneToOne'; 137 | this.isDisabled = false; 138 | this.globalAlpha = 1; 139 | this.mandatories = []; 140 | this.whiteSpace ="nowrap"; 141 | this.hideLink = false; 142 | this.isTouchScreen = is_touch_device(); 143 | this.mobileClickIt = false; 144 | 145 | this.draw = function () { 146 | var self = this; 147 | var tablesAB = self.chosenListA + '|' + self.chosenListB; // existingLinks 148 | self.canvasCtx.globalAlpha = self.globalAlpha; 149 | self.canvasCtx.beginPath(); 150 | self.canvasCtx.fillStyle = 'white'; 151 | self.canvasCtx.strokeStyle = self.lineColor; 152 | self.canvasCtx.clearRect(0, 0, self.canvasWidth, self.canvasHeight); 153 | var links = self.linksByName.filter(function (x) { 154 | return x.tables == tablesAB; 155 | }); 156 | links.forEach(function (item, i) { 157 | 158 | var positionA = self.listA.indexOf(item.from); 159 | var positionB = self.listB.indexOf(item.to); 160 | 161 | if (positionB == -1 || positionA == -1) { 162 | console.log('error link names unknown'); 163 | return; 164 | } 165 | var Ax = 0; 166 | var Ay = self.ListHeights1[positionA]; 167 | var Bx = self.canvasWidth; 168 | var By = self.ListHeights2[positionB]; 169 | self.canvasCtx.beginPath(); 170 | self.canvasCtx.moveTo(Ax, Ay); 171 | var handleCurrentColor = self.handleColor[i % self.handleColor.length]; 172 | if (self.lineStyle == 'square-ends' || self.lineStyle == 'square-ends-dotted') { 173 | self.canvasCtx.fillStyle = handleCurrentColor; 174 | self.canvasCtx.strokeStyle = handleCurrentColor; 175 | self.canvasCtx.rect(Ax, Ay - 4, 8, 8); 176 | self.canvasCtx.rect(Bx - 8, By - 4, 8, 8); 177 | self.canvasCtx.fill(); 178 | self.canvasCtx.stroke(); 179 | self.canvasCtx.moveTo(Ax + 8, Ay); 180 | self.canvasCtx.lineTo(Ax + 16, Ay); 181 | self.canvasCtx.lineTo(Bx - 16, By); 182 | self.canvasCtx.lineTo(Bx - 8, By); 183 | self.canvasCtx.stroke(); 184 | } else { 185 | self.canvasCtx.strokeStyle = handleCurrentColor; 186 | self.canvasCtx.lineTo(Bx, By); 187 | self.canvasCtx.stroke(); 188 | } 189 | self.canvasCtx.closePath(); 190 | self.canvasCtx.lineWidth = 1; 191 | }); 192 | } 193 | this.makeLink = function (infos) { 194 | var self = this; 195 | var tablesAB = self.chosenListA + '|' + self.chosenListB; 196 | var already = false; 197 | var test = self.linksByName.filter(function (x) { 198 | return x.tables == tablesAB && x.to == infos.nameB && x.from == infos.nameA; 199 | }); 200 | if (test.length > 0) already = true; 201 | if (!already) { 202 | if (self.associationMode == 'oneToOne') { 203 | for (var i = self.linksByName.length - 1; i >= 0; i--) { 204 | if (self.linksByName[i].tables == tablesAB && self.linksByName[i].to == infos.nameB) { 205 | self.linksByName.splice(i, 1); 206 | } 207 | } 208 | 209 | for (var i = self.linksByName.length - 1; i >= 0; i--) { 210 | if (self.linksByName[i].tables == tablesAB && self.linksByName[i].from == infos.nameA) { 211 | self.linksByName.splice(i, 1); 212 | } 213 | } 214 | } 215 | self.linksByName.push({ 216 | 'tables': tablesAB, 217 | 'from': infos.nameA, 218 | 'to': infos.nameB 219 | }); 220 | $(self.$root).trigger({ 221 | type: 'fieldLinkerUpdate', 222 | what: 'addLink' 223 | }); 224 | } 225 | self.draw(); 226 | } 227 | this.eraseLinkA = function (nameA) { 228 | var self = this; 229 | var tablesAB = self.chosenListA + '|' + self.chosenListB; 230 | for (var i = self.linksByName.length - 1; i >= 0; i--) { 231 | if (self.linksByName[i].tables == tablesAB && self.linksByName[i].from == nameA) { 232 | self.linksByName.splice(i, 1); 233 | } 234 | } 235 | self.draw(); 236 | $(self.$root).trigger({ 237 | type: 'fieldLinkerUpdate', 238 | what: 'removeLink' 239 | }); 240 | } 241 | this.eraseLinkB = function (nameB) { 242 | var self = this; 243 | var tablesAB = self.chosenListA + '|' + self.chosenListB; 244 | for (var i = self.linksByName.length - 1; i >= 0; i--) { 245 | if (self.linksByName[i].tables == tablesAB && self.linksByName[i].to == nameB) { 246 | self.linksByName.splice(i, 1); 247 | } 248 | } 249 | self.draw(); 250 | $(self.$root).trigger({ 251 | type: 'fieldLinkerUpdate', 252 | what: 'removeLink' 253 | }); 254 | } 255 | this.readUserPreferences = function () { 256 | var self = this; 257 | if (self.data.options.className) { 258 | self.className = self.data.options.className; 259 | } 260 | if(self.data.options.whiteSpace){ 261 | self.whiteSpace = self.data.options.whiteSpace; 262 | } 263 | if (self.data.options.lineStyle) { 264 | if (self.data.options.lineStyle == 'square-ends' || self.data.options.lineStyle == 'square-ends-dotted') 265 | self.lineStyle = self.data.options.lineStyle; 266 | } 267 | if (self.data.options.lineColor) { 268 | self.lineColor = self.data.options.lineColor; 269 | } 270 | if (self.data.options.handleColor) { 271 | self.handleColor = self.data.options.handleColor.split(','); 272 | } 273 | if (self.data.options.associationMode) { 274 | self.associationMode = self.data.options.associationMode; 275 | } 276 | if (self.data.options.mobileClickIt != undefined) { 277 | self.mobileClickIt = self.data.options.mobileClickIt; 278 | } 279 | if(self.isTouchScreen){ 280 | self.mobileClickIt = true; 281 | } 282 | 283 | } 284 | this.fillChosenLists = function () { 285 | var self = this; 286 | self.listNames = []; 287 | self.listA = []; 288 | self.listB = []; 289 | if (self.chosenListA == '' || self.chosenListB == '') { 290 | self.chosenListA = self.data.Lists[0].name; 291 | self.chosenListB = self.data.Lists[1].name; 292 | } 293 | self.keyNameA = self.data.Lists[0].keyName || ''; 294 | self.keyNameB = self.data.Lists[1].keyName || ''; 295 | 296 | self.data.Lists.forEach(function (x) { 297 | self.listNames.push(x.name); 298 | if (x.name == self.chosenListA) { 299 | x.list.forEach(function (y) { 300 | self.listA.push(y); 301 | }); 302 | } 303 | if (x.name == self.chosenListB) { 304 | if (x.mandatories != undefined) { 305 | self.mandatories = x.mandatories; 306 | } 307 | x.list.forEach(function (y) { 308 | self.listB.push(y); 309 | }); 310 | } 311 | }); 312 | } 313 | this.makeDropDownForLists = function () { 314 | var self = this; 315 | self.dropDownForLists = $(''); 316 | self.dropDownForLists 317 | .css('width', '100%'); 318 | self.listNames.forEach(function (x) { 319 | var $option = $(''); 320 | $option 321 | .val(x) 322 | .text(x) 323 | .appendTo(self.dropDownForLists); 324 | }); 325 | } 326 | this.drawColumnsAtLoadTime = function () { 327 | var self = this; 328 | self.$root.html(''); 329 | self.$main = $('
'); 330 | self.$main 331 | .appendTo(self.$root) 332 | .addClass('FL-main ' + self.className) 333 | .css({ 334 | 'position': 'relative', 335 | 'width': '100%', 336 | 'text-align': 'left' 337 | }); 338 | 339 | self.$leftDiv = $('
'); 340 | self.$leftDiv 341 | .appendTo(self.$main) 342 | .addClass('FL-left') 343 | .css({ 344 | 'float': 'left', 345 | 'width': '40%', 346 | 'display': 'inline-block', 347 | 'text-align': 'left', 348 | 'white-space': self.whiteSpace 349 | }) 350 | .append(self.dropDownForLists.clone()); 351 | 352 | self.$leftDiv.find('select') 353 | .attr('id', 'select1') 354 | .val(self.chosenListA) 355 | .on('change', function () { 356 | self.chosenListA = $(this).val(); 357 | self.fillChosenLists(); 358 | }); 359 | 360 | self.$ulLeft = $(''); 361 | self.$ulLeft 362 | .appendTo(self.$leftDiv) 363 | .css({ 364 | 'text-align': 'left', 365 | 'list-style': 'none' 366 | }); 367 | 368 | self.$midDiv = $('
'); 369 | self.$midDiv 370 | .appendTo(self.$main) 371 | .addClass('FL-mid') 372 | .css({ 373 | 'display': 'inline-block', 374 | 'width': '20%' 375 | }); 376 | 377 | self.$rightDiv = $('
'); 378 | self.$rightDiv 379 | .appendTo(self.$main) 380 | .addClass('FL-right') 381 | .css({ 382 | 'float': 'right', 383 | 'width': '40%', 384 | 'display': 'inline-block', 385 | 'text-align': 'left', 386 | 'white-space': self.whiteSpace 387 | }) 388 | .append(self.dropDownForLists.clone()); 389 | 390 | if (self.data.options.buttonErase) { 391 | self.$btn = $(''); 392 | self.$btn 393 | .appendTo(self.$root.find('.FL-main')) 394 | .attr('type', 'button') 395 | .addClass('btn btn-default btn-sm eraseLink') 396 | .attr("style","position:absolute;top:-6px;right:0;opacity:0.9;") 397 | .html(self.data.options.buttonErase); 398 | } 399 | 400 | self.$rightDiv.find('select') 401 | .attr('id', 'select2') 402 | .val(self.chosenListB) 403 | .on('change', function () { 404 | self.chosenListB = $(this).val(); 405 | self.fillChosenLists(); 406 | }); 407 | 408 | self.$ulRight = $(''); 409 | self.$ulRight 410 | .appendTo(self.$leftDiv) 411 | .css({ 412 | 'text-align': 'left', 413 | 'list-style': 'none' 414 | }); 415 | } 416 | this.computeListHeight = function (li) { 417 | // outerHeight(true) adds margins too, full step is simply full outerHeight / 2 between li siblings 418 | if(!$(li).hasClass('hidden')){ 419 | var step = Math.ceil($(li).outerHeight(true) / 2); 420 | return Math.floor($(li).position().top + step); 421 | } 422 | } 423 | this.drawColumnsContentA = function () { 424 | var self = this; 425 | 426 | if (self.$ulLeft.length == 1) { 427 | self.$ulLeft.empty(); 428 | } else { 429 | self.$ulLeft = $(''); 430 | self.$ulLeft.appendTo(self.$leftDiv) 431 | } 432 | 433 | self.$ulLeft 434 | .attr('data-col', self.chosenListA) 435 | .css({ 436 | 'text-align': 'left', 437 | 'list-style': 'none' 438 | }); 439 | 440 | self.listA.forEach(function (x, i) { 441 | let nrItems = Object.keys(x).length; 442 | if (self.hideLink) { 443 | nrItems--; 444 | } 445 | if (nrItems < 0) { 446 | nrItems = 1; 447 | } 448 | let percent = (100 / nrItems) + '%'; 449 | var $li = $('
  • '); 450 | let item = x; 451 | let id = x; 452 | $li 453 | .appendTo(self.$ulLeft) 454 | .attr('data-offset', i) 455 | .attr('data-name', id) 456 | .css({ 457 | 'width': '100%', 458 | 'position': 'relative' 459 | }); 460 | var $div = $('
    '); 461 | $div 462 | .appendTo($li) 463 | .attr('ondrop', 'LM_drop(event)') 464 | .attr('ondragover', 'LM_allowDrop(event)') 465 | .attr('ondragstart', 'LM_drag(event)') 466 | .attr('draggable', 'true') 467 | .css({ 468 | 'width': '80%' 469 | }) 470 | .html(item); 471 | var $eraseIcon = $(''); 472 | $eraseIcon 473 | .appendTo($li) 474 | .addClass('fa fa-undo unlink') 475 | .attr('draggable', 'false') 476 | .css({ 477 | 'right': '28px', 478 | 'color': '#aaa', 479 | 'position': 'absolute', 480 | 'top': '50%', 481 | 'transform': 'translateY(-50%)' 482 | }); 483 | var $pullIcon = $(''); 484 | $pullIcon 485 | .appendTo($li) 486 | .addClass('fa fa-arrows-alt link') 487 | .attr('draggable', 'false') 488 | .css({ 489 | 'right': '8px', 490 | 'color': '#aaa', 491 | 'position': 'absolute', 492 | 'top': '50%', 493 | 'transform': 'translateY(-50%)' 494 | }); 495 | }); 496 | // Computing the vertical offset of the middle of each cell. 497 | self.ListHeights1 = []; 498 | 499 | $(self.$ulLeft).find('li').each(function (i, li) { 500 | var val = self.computeListHeight(li); 501 | self.ListHeights1.push(val); 502 | }); 503 | 504 | if (!self.mobileClickIt) { 505 | $(self.$ulLeft).find('li').off('mousedown').on('mousedown', function (e) { 506 | // we make a move object to keep track of the origine and also remember that we are starting a mouse drag (mouse is down) 507 | if (self.isDisabled) return; 508 | self.move = {}; 509 | self.move.offsetA = $(this).data('offset'); 510 | self.move.nameA = $(this).data('name'); 511 | self.move.offsetB = -1; 512 | self.move.nameB = -1; 513 | }); 514 | } 515 | 516 | if (self.isTouchScreen && !self.mobileClickIt) { 517 | // On mousedown in left List : 518 | $(self.$root).find('.link').off('touchstart').on('touchstart', function (e) { 519 | 520 | if (self.isDisabled) return; 521 | self.move = {}; 522 | self.move.offsetA = $(this).parent().data('offset'); 523 | self.move.nameA = $(this).parent().data('name'); 524 | self.move.offsetB = -1; 525 | self.move.nameB = -1; 526 | 527 | var originalEvent = e.originalEvent; 528 | if (originalEvent != null && originalEvent.touches != undefined) { 529 | var touch = originalEvent.touches[0]; 530 | if (move != null) { 531 | var mouseEvent = new MouseEvent('mousedown', { 532 | clientX: touch.clientX, 533 | clientY: touch.clientY 534 | }); 535 | self.drawImmediate(mouseEvent); 536 | } 537 | } 538 | }); 539 | 540 | $(self.$root).find('.link').off('touchmove').on('touchmove', function (e) { 541 | var originalEvent = e.originalEvent; 542 | if (originalEvent != null && originalEvent.touches != undefined) { 543 | var touch = originalEvent.touches[0]; 544 | var mouseEvent = new MouseEvent('mousemove', { 545 | clientX: touch.clientX, 546 | clientY: touch.clientY 547 | }); 548 | 549 | if (self.move != null) { 550 | self.drawImmediate(mouseEvent); 551 | } 552 | 553 | } 554 | }); 555 | 556 | $(self.$root).find('.link').off('touchend').on('touchend', function (e) { 557 | if (self.isDisabled) return; 558 | 559 | var originalEvent = e.originalEvent; 560 | if (originalEvent != null && originalEvent.touches != undefined) { 561 | var touch = originalEvent.changedTouches[0]; 562 | var mousePosition = { 563 | x: touch.clientX, 564 | y: touch.clientY 565 | }; 566 | if (self.move != null) { 567 | let found = false; 568 | $(self.$ulRight).find('li').each(function (i) { 569 | if (!found) { 570 | var rect = this.getBoundingClientRect(); 571 | //left, top, right, bottom, width, height 572 | if (mousePosition.x >= rect.left && mousePosition.x <= rect.right && mousePosition.y >= rect.top && mousePosition.y <= rect.bottom) { 573 | if (self.associationMode == 'oneToOne') { 574 | self.eraseLinkB($(this).data('name')); // we erase an existing link if any 575 | } 576 | self.move.offsetB = $(this).data('offset'); 577 | self.move.nameB = $(this).data('name'); 578 | var infos = JSON.parse(JSON.stringify(move)); 579 | self.move = null; 580 | self.makeLink(infos); 581 | found = true; 582 | } 583 | } 584 | }); 585 | if (!found) { 586 | self.draw(); 587 | } 588 | } 589 | } 590 | }); 591 | } 592 | 593 | if (!self.mobileClickIt) { 594 | $(self.$ulLeft).find('li').off('mouseup').on('mouseup', function (e) { 595 | if (self.isDisabled) return; 596 | // We do a mouse up on the left side : the drag is canceled 597 | self.move = null; 598 | }); 599 | 600 | $(self.$ulLeft).find('li').off('click').on('click', function (e) { 601 | if (self.isDisabled) return; 602 | self.eraseLinkA($(this).data('name')); 603 | self.draw(); 604 | }); 605 | } 606 | 607 | if (self.mobileClickIt) { 608 | $(self.$ulLeft).find('li').off('click').on('click', function (e) { 609 | if (self.isDisabled) return; 610 | let el = $(this); 611 | $(self.$root).find('.selected').removeClass('selected'); 612 | el.addClass('selected'); 613 | self.move = {}; 614 | self.move.offsetA = el.data('offset'); 615 | self.move.nameA = el.data('name'); 616 | self.move.offsetB = -1; 617 | self.move.nameB = -1; 618 | }); 619 | } 620 | 621 | $(self.$root).find('.unlink').off('click').on('click', function (e) { 622 | if (self.isDisabled) return; 623 | self.eraseLinkA($(this).data('name')); 624 | self.draw(); 625 | }); 626 | } 627 | this.drawColumnsContentB = function () { 628 | var self = this; 629 | if (self.$ulRight.length == 1) { 630 | self.$ulRight.empty(); 631 | } else { 632 | self.$ulRight = $(''); 633 | self.$ulRight.appendTo(self.$rightDiv) 634 | } 635 | self.$ulRight 636 | .appendTo(self.$rightDiv) 637 | .attr('data-col', self.chosenListB) 638 | .css({ 639 | 'text-align': 'left', 640 | 'list-style': 'none' 641 | }); 642 | 643 | self.listB.forEach(function (x, i) { 644 | let item = x; 645 | let id = x; 646 | var isMandatory = (self.mandatories.indexOf(x) != -1); 647 | var $li = $('
  • '); 648 | $li 649 | .appendTo(self.$ulRight) 650 | .attr('data-offset', i) 651 | .attr('data-name', id) 652 | .attr('data-mandatory', isMandatory) 653 | .attr('draggable', 'true'); 654 | var $div = $('
    '); 655 | $div 656 | .appendTo($li) 657 | .attr('ondrop', 'LM_drop(event)') 658 | .attr('ondragover', 'LM_allowDrop(event)') 659 | .attr('ondragstart', 'LM_drag(event)') 660 | .attr('draggable', 'true') 661 | .css({ 662 | 'width': '80%' 663 | }) 664 | .html(item); 665 | if (isMandatory && self.mandatoryTooltips) { 666 | $li 667 | .attr('data-placement', 'top') 668 | .attr('title', self.mandatoryErrorMessage); 669 | if (self.bootstrap_enabled) 670 | $li.tooltip(); 671 | } 672 | }); 673 | // Computing the vertical offset of the middle of each cell. 674 | self.ListHeights2 = []; 675 | 676 | $(self.$ulRight).find('li').each(function (i, li) { 677 | var val = self.computeListHeight(li); 678 | self.ListHeights2.push(val); 679 | }); 680 | // Mouse up on the right side 681 | $(self.$ulRight).find('li').off('mouseup').on('mouseup', function (e) { 682 | if (self.isDisabled) return; 683 | if (self.move != null) { // no drag 684 | if (self.associationMode == 'oneToOne') { 685 | self.eraseLinkB($(this).data('name')); // we erase an existing link if any 686 | } 687 | self.move.offsetB = $(this).data('offset'); 688 | self.move.nameB = $(this).data('name'); 689 | var infos = JSON.parse(JSON.stringify(self.move)); 690 | self.move = null; 691 | self.makeLink(infos); 692 | } 693 | }); 694 | 695 | $(self.$ulRight).find('li').off('dblclick').on('dblclick', function (e) { 696 | if (self.isDisabled) return; 697 | self.eraseLinkB($(this).data('name')); // we erase an existing link if any 698 | self.draw(); 699 | }); 700 | // mousemove over a right cell 701 | $(self.$ulRight).find('li').off('mousemove').on('mousemove', function (e) { 702 | if (self.isDisabled) return; 703 | if (self.move != null) { // drag occuring 704 | var _from = self.move.offsetA; 705 | var _To = $(this).data('offset'); 706 | var Ax = 0; 707 | var Ay = self.ListHeights1[_from]; 708 | var Bx = self.canvasWidth; 709 | var By = self.ListHeights2[_To]; 710 | self.draw(); 711 | self.canvasCtx.beginPath(); 712 | var color = self.handleColor[_from % self.handleColor.length]; 713 | self.canvasCtx.fillStyle = 'white'; 714 | self.canvasCtx.strokeStyle = color; 715 | self.canvasCtx.moveTo(Ax, Ay); 716 | self.canvasCtx.lineTo(Bx, By); 717 | self.canvasCtx.stroke(); 718 | } 719 | }); 720 | 721 | if (self.mobileClickIt) { 722 | $(self.$ulRight).find('li').off('click').on('click', function (e) { 723 | $(self.$root).find('.selected').removeClass('selected'); 724 | if (self.isDisabled || self.move === null) return; 725 | self.move.offsetB = $(this).data('offset'); 726 | self.move.nameB = $(this).data('name'); 727 | var infos = JSON.parse(JSON.stringify(move)); 728 | self.move = null; 729 | self.makeLink(infos); 730 | }); 731 | } 732 | } 733 | this.createCanvas = function () { 734 | var self = this; 735 | self.canvasId = 'cnv_' + Date.now(); 736 | var w = self.$midDiv.width(); 737 | var h2 = self.$rightDiv.height(); 738 | var h1 = self.$leftDiv.height(); 739 | var h = h1 >= h2 ? h1 : h2; 740 | self.$canvas = $(''); 741 | self.$canvas 742 | .appendTo(self.$midDiv) 743 | .attr('id', self.canvasId) 744 | .css({ 745 | 'width': w + 'px', 746 | 'height': h + 'px' 747 | }); 748 | self.canvasWidth = w; 749 | self.canvasHeight = h; 750 | self.canvasPtr = document.getElementById(self.canvasId); 751 | if(self.canvasPtr){ 752 | self.canvasPtr.width = self.canvasWidth; 753 | self.canvasPtr.height = self.canvasHeight; 754 | self.canvasCtx = self.canvasPtr.getContext("2d"); 755 | } 756 | } 757 | this.getTouchPos = function (e) { 758 | var self = this; 759 | var rect = self.canvasPtr.getBoundingClientRect(); 760 | return { 761 | x: e.clientX - rect.left, 762 | y: e.clientY - rect.top 763 | }; 764 | } 765 | this.drawImmediate = function (e) { 766 | var self = this; 767 | self.canvasCtx.clearRect(0, 0, self.canvasWidth, self.canvasHeight); 768 | // we redraw all the existing links 769 | self.draw(); 770 | self.canvasCtx.beginPath(); 771 | // we draw the new would-be link 772 | var _from = self.move.offsetA; 773 | var color = self.handleColor[_from % self.handleColor.length]; 774 | self.canvasCtx.fillStyle = 'white'; 775 | self.canvasCtx.strokeStyle = color; 776 | var Ax = 0; 777 | var Ay = self.ListHeights1[_from]; 778 | // mouse position relative to the canvas 779 | //var Bx = e.offsetX; 780 | //var By = e.offsetY; 781 | var relativePosition = self.getTouchPos(e); 782 | var Bx = relativePosition.x; 783 | var By = relativePosition.y; 784 | self.canvasCtx.moveTo(Ax, Ay); 785 | self.canvasCtx.lineTo(Bx, By); 786 | self.canvasCtx.stroke(); 787 | 788 | } 789 | this.setListeners = function () { 790 | var self = this; 791 | if (self.data.options.buttonErase) { 792 | $(self.$root).find('.FL-main .eraseLink').on('click', function (e) { 793 | if (self.isDisabled) return; 794 | self.linksByName.length = 0; 795 | self.draw(); 796 | $(self.selector).trigger({ 797 | type: 'fieldLinkerUpdate', 798 | what: 'removeLink' 799 | }); 800 | }); 801 | } 802 | // mousemove over the canvas 803 | $(self.$canvas).on('mousemove', function (e) { 804 | if (self.isDisabled) return; 805 | if (self.move != null) { 806 | self.drawImmediate(e); 807 | } 808 | }); 809 | $(self.$main).on('mouseup', function (e) { 810 | if (self.isDisabled) return; 811 | if (self.move != null) { 812 | self.move = null; 813 | self.draw(); 814 | } 815 | }); 816 | // this is a remnant of LinksMaker : could handle more than 2 columns 817 | // this won't change unluss we trigger it 818 | $(self.$leftDiv).find('select').on('change', function (e) { 819 | if (self.isDisabled) return; 820 | self.chosenListA = $(this).val(); 821 | $(self.$rightDiv).find('select option').each(function () { 822 | $(this) 823 | .attr('disabled', $(this).val() == self.chosenListA); 824 | }); 825 | self.drawColumnsContentA(); 826 | self.draw(); 827 | }); 828 | 829 | $(self.$rightDiv).find('select').on('change', function (e) { 830 | if (self.isDisabled) return; 831 | self.chosenListB = $(this).val(); 832 | $(self.$leftDiv).find('select option').each(function () { 833 | $(this) 834 | .attr('disabled', $(this).val() == this.chosenListB); 835 | }); 836 | self.drawColumnsContentB(); 837 | self.draw(); 838 | }); 839 | } 840 | this.setGlobalRedraw = function(){ 841 | var self = this; 842 | $(self.selector).on('LM_Message_Redraw', function () { 843 | self.mandatories = []; 844 | self.move = null; 845 | self.listA = []; 846 | self.listB = []; 847 | self.FL_Factory_Lists.Lists.forEach(function (x) { 848 | if (x.name == self.chosenListA) { 849 | let dict = {}; 850 | x.list.forEach(function (y) { 851 | if (!dict[y]) { 852 | dict[y] = 1; 853 | } else { 854 | dict[y] + 1; 855 | y += '(' + dict[y] + ')'; 856 | } 857 | }); 858 | self.listA = x.list; 859 | } 860 | if (x.name == self.chosenListB) { 861 | self.listB = x.list; 862 | if (x.mandatories != undefined) { 863 | self.mandatories = x.mandatories; 864 | } 865 | } 866 | }); 867 | self.drawColumnsContentA(); 868 | self.drawColumnsContentB(); 869 | self.draw(); 870 | }); 871 | } 872 | this.setError = function(message){ 873 | var self = this; 874 | self.onError = true; 875 | throw self.errMsg + message; 876 | } 877 | this.init = function(input,onFilter){ 878 | 879 | var self = this; 880 | if (!input) { 881 | setError('no input options provided (param 2)'); 882 | } 883 | self.data = JSON.parse(JSON.stringify(input)); 884 | self.FL_Factory_Lists = self.data; 885 | 886 | if (!onFilter) { 887 | self.FL_Original_Factory_Lists = JSON.parse(JSON.stringify( self.data )); 888 | } 889 | if (!self.data.Lists || self.data.Lists.length < 2) { 890 | self.setError('provide at least 2 lists'); 891 | } 892 | } 893 | this.deduplicate = function(){ 894 | var self = this; 895 | self.listsNr = self.data.Lists.length; 896 | 897 | for (let i = 0; i < self.listsNr; i++) { 898 | let dict = {}; 899 | for (let j = 0; j < self.data.Lists[i].list.length; j++) { 900 | let val = self.data.Lists[i].list[j]; 901 | if (!dict[val]) { 902 | dict[val] = 1; 903 | } else { 904 | dict[val] += 1; 905 | self.data.Lists[i].list[j] += '(' + dict[val] + ')'; 906 | } 907 | } 908 | } 909 | } 910 | this.changeSelects = function(){ 911 | var self = this; 912 | $(self.selector) 913 | .find('.FL-left select') 914 | .trigger('change') 915 | .css('border', 'none') 916 | .css('appearance', 'none') 917 | .attr('disabled', 'true'); 918 | 919 | $(self.selector) 920 | .find('.FL-right select') 921 | .trigger('change') 922 | .css('border', 'none') 923 | .css('appearance', 'none') 924 | .attr('disabled', 'true'); 925 | } 926 | this.manageExistingLinks = function(){ 927 | var self = this; 928 | if (self.data.existingLinks) { 929 | self.linksByName = self.data.existingLinks; 930 | var tablesAB = self.chosenListA + '|' + self.chosenListB; 931 | self.linksByName.forEach(function (x) { 932 | x.tables = tablesAB; 933 | }); 934 | } 935 | } 936 | this.manageResize= function(){ 937 | var self = this; 938 | $(window).resize(function () { 939 | self.canvasWidth = $(self.selector).find('.FL-main .FL-mid').width(); 940 | self.canvasPtr.width = self.canvasWidth; 941 | $('#' + self.canvasId).css('width', self.canvasWidth + 'px'); 942 | self.draw(); 943 | }); 944 | } 945 | this.eraseLinks = function(){ 946 | var self = this; 947 | self.linksByName.length = 0; 948 | self.draw(); 949 | } 950 | this.getLinks = function(){ 951 | var self = this; 952 | if (!self.onError) { 953 | var isMandatoryError = false; 954 | let links = null; 955 | var errorMessage = self.mandatoryErrorMessage + ' : '; 956 | var fieldInErrorName = ''; 957 | self.mandatories.forEach(function (m, i) { 958 | if (!isMandatoryError) { 959 | var match = self.linksByName.filter(function (link) { 960 | return link.to == m; 961 | }); 962 | if (match.length == 0) { 963 | isMandatoryError = true; 964 | fieldInErrorName = m; 965 | } 966 | } 967 | }); 968 | if (isMandatoryError) { 969 | return { 970 | 'error': true, 971 | 'errorMessage': errorMessage + fieldInErrorName, 972 | 'links': [] 973 | }; 974 | } else { 975 | links = []; 976 | self.linksByName.forEach(function (x) { 977 | links.push({ 978 | from: x.from, 979 | to: x.to 980 | }); 981 | }); 982 | return { 983 | 'error': false, 984 | 'errorMessage': '', 985 | 'links': links 986 | }; 987 | } 988 | } else { 989 | return []; 990 | } 991 | } 992 | this.changeParameters = function(input){ 993 | var self = this; 994 | if (!self.onError) { 995 | if (input) { 996 | var options = JSON.parse(JSON.stringify(input)); 997 | if (options.className) { 998 | self.className = options.className; 999 | } 1000 | if(options.whiteSpace){ 1001 | self.whiteSpace = options.whiteSpace; 1002 | 1003 | self.$leftDiv.css("white-space",self.whiteSpace); 1004 | self.$rightDiv.css("white-space",self.whiteSpace); 1005 | 1006 | self.ListHeights1 = []; 1007 | 1008 | $(self.$ulLeft).find('li').each(function (i, li) { 1009 | var val = self.computeListHeight(li); 1010 | self.ListHeights1.push(val); 1011 | }); 1012 | 1013 | self.ListHeights2 = []; 1014 | 1015 | $(self.$ulRight).find('li').each(function (i, li) { 1016 | var val = self.computeListHeight(li); 1017 | self.ListHeights2 .push(val); 1018 | }); 1019 | 1020 | } 1021 | if (options.lineStyle) { 1022 | self.lineStyle = options.lineStyle; 1023 | } 1024 | if (options.lineColor) { 1025 | self.lineColor = options.lineColor; 1026 | } 1027 | if (options.handleColor) { 1028 | self.handleColor = options.handleColor.split(','); 1029 | } 1030 | if (options.associationMode) { 1031 | let unicityTokenA = ''; 1032 | let unicityTokenB = ''; 1033 | let formerAssociation = self.associationMode; 1034 | self.associationMode = options.associationMode; 1035 | if (self.associationMode == 'oneToOne' && formerAssociation == 'manyToMany') { 1036 | let unicityDict = {}; 1037 | for (var i = self.linksByName.length - 1; i >= 0; i--) { 1038 | unicityTokenA = self.linksByName[i].tables + '_A_' + self.linksByName[i]['from']; 1039 | unicityTokenB = self.linksByName[i].tables + '_B_' + self.linksByName[i]['to']; 1040 | let doDelete = false; 1041 | if (!unicityDict[unicityTokenA]) { 1042 | unicityDict[unicityTokenA] = true; 1043 | } else { 1044 | doDelete = true; 1045 | } 1046 | if (!unicityDict[unicityTokenB]) { 1047 | unicityDict[unicityTokenB] = true; 1048 | } else { 1049 | doDelete = true; 1050 | } 1051 | if (doDelete) { 1052 | self.linksByName.splice(i, 1); 1053 | } 1054 | } 1055 | } 1056 | } 1057 | } 1058 | self.draw(); 1059 | } 1060 | } 1061 | this.enable = function(doEnable){ 1062 | var self = this; 1063 | self.isDisabled = !doEnable; 1064 | 1065 | $(self.$root) 1066 | .find('.eraseLink') 1067 | .prop('disabled', self.isDisabled); 1068 | 1069 | if(doEnable) { 1070 | $(self.$root) 1071 | .find('li') 1072 | .removeClass('inactive') 1073 | .find('div') 1074 | .prop('draggable',true); 1075 | }else{ 1076 | $(self.$root) 1077 | .find('li') 1078 | .addClass('inactive') 1079 | .find('div') 1080 | .prop('draggable',false); 1081 | } 1082 | 1083 | $(self.$root) 1084 | .find('select') 1085 | .prop('disabled', self.isDisabled); 1086 | 1087 | self.globalAlpha = self.isDisabled ? 0.5 : 1; 1088 | 1089 | self.draw(); 1090 | 1091 | } 1092 | } 1093 | // utils 1094 | function LM_allowDrop(ev) { 1095 | // ev.dataTransfer.dropEffect = "none"; // dropping is not allowed 1096 | console.log(ev.currentTarget); 1097 | ev.preventDefault(); 1098 | } 1099 | 1100 | function LM_drag(ev) { 1101 | let $target = $(ev.target); 1102 | let data = {}; 1103 | data.name = $target.parent().attr('data-name'); 1104 | data.col = $target.parent().parent().attr('data-col'); 1105 | data.offset = $target.parent().attr('data-offset'); 1106 | ev.dataTransfer.setData('text/plain', JSON.stringify({ 1107 | 'data': data 1108 | })); 1109 | } 1110 | 1111 | function LM_drop(ev) { 1112 | ev.preventDefault(); 1113 | let src = JSON.parse(ev.dataTransfer.getData('text')); 1114 | if (src) { 1115 | src = src.data; 1116 | } 1117 | let $target = $(ev.target); 1118 | let $root = $target.closest(".fieldsLinker").parent(); 1119 | let currentDropId = $root.attr("id"); 1120 | let factory = null; 1121 | if($root.length==1){ 1122 | let oneMemory = fieldsLinkerMemory.filter(function(x,i){ 1123 | return x.selector.id == currentDropId; 1124 | }); 1125 | if(oneMemory.length >0){ 1126 | factory = oneMemory[0].factory; 1127 | } 1128 | 1129 | if (factory != null) { 1130 | let dest = {}; 1131 | dest.name = $target.parent().attr('data-name'); 1132 | dest.col = $target.parent().parent().attr('data-col'); 1133 | dest.offset = $target.parent().attr('data-offset'); 1134 | if (src.col == dest.col && src.offset != dest.offset && src.name != dest.name) { 1135 | factory.work.FL_Factory_Lists.Lists.forEach(function (x) { 1136 | if (x.name == src.col) { 1137 | let indexA = x.list.indexOf(src.name); 1138 | let indexB = x.list.indexOf(dest.name); 1139 | if (indexA != -1 && indexB != -1) { 1140 | let temp = x.list[indexA]; 1141 | x.list[indexA] = x.list[indexB]; 1142 | x.list[indexB] = temp; 1143 | } 1144 | } 1145 | }); 1146 | $($root).trigger('LM_Message_Redraw'); 1147 | } 1148 | } 1149 | } 1150 | 1151 | } 1152 | 1153 | 1154 | function is_touch_device() { // from bolmaster2 - stackoverflow 1155 | var prefixes = ' -webkit- -moz- -o- -ms- '.split(' '); 1156 | var mq = function (query) { 1157 | return window.matchMedia(query).matches; 1158 | }; 1159 | 1160 | if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { 1161 | return true; 1162 | } 1163 | 1164 | // include the 'heartz' as a way to have a non matching MQ to help terminate the join 1165 | // https://git.io/vznFH 1166 | var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); 1167 | return mq(query); 1168 | } 1169 | --------------------------------------------------------------------------------