1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 25 var _MODEL_ = "model"; 26 var _INSTANCE_ = "instance"; 27 var _INHERIT_ = "inherit"; 28 var _MODELITEM_ = "modelitem"; 29 30 31 /** 32 * 33 * 34 * @private 35 */ 36 XModel = function(attributes) { 37 // get a unique id for this form 38 XFG.assignUniqueId(this, "_Model_"); 39 40 // copy any attributes passed in directly into this object 41 if (attributes) { 42 for (var prop in attributes) { 43 this[prop] = attributes[prop]; 44 } 45 } 46 47 if (this.items == null) this.items = []; 48 49 this._pathIndex = {}; 50 this._pathGetters = {}; 51 this._parentGetters = {}; 52 this._itemsAreInitialized = false; 53 this._errorMessages = {}; 54 55 if (this.getDeferInit() == false) { 56 this.initializeItems(); 57 } 58 } 59 XModel.toString = function() { return "[Class XModel]"; } 60 XModel.prototype.toString = function() { return "[XModel " + this.__id + "]"; } 61 62 XModel.prototype.pathDelimiter = "/"; 63 XModel.prototype.getterScope = _INSTANCE_; 64 XModel.prototype.setterScope = _INSTANCE_; 65 66 // set deferInit to false to initialize all modelItems when the model is created 67 // NOTE: this is generally a bad idea, and all XForms are smart enough 68 // to tell their models to init before they need them... 69 XModel.prototype.deferInit = true; 70 XModel.prototype.getDeferInit = function () { return this.deferInit } 71 72 73 XModel.prototype.initializeItems = function() { 74 if (this._itemsAreInitialized) return; 75 76 var t0 = new Date().getTime(); 77 78 this.__nestedItemCount = 0; //DEBUG 79 80 // initialize the items for the form 81 this.items = this.initItemList(this.items, null); 82 83 this._itemsAreInitialized = true; 84 85 var t1 = new Date().getTime(); 86 //DBG.println(this,".initializeItems(): w/ ", this.__nestedItemCount," items took ", (t1 - t0), " msec"); 87 } 88 89 90 91 XModel.prototype.initItemList = function(itemAttrs, parentItem) { 92 var items = []; 93 for (var i = 0; i < itemAttrs.length; i++) { 94 items[i] = this.initItem(itemAttrs[i], parentItem); 95 } 96 this.__nestedItemCount += itemAttrs.length; //DEBUG 97 return items; 98 } 99 100 101 XModel.prototype.initItem = function(itemAttr, parentItem) { 102 // if we already have a form item, assume it's been initialized already! 103 if (itemAttr.__isXModelItem) return itemAttr; 104 105 // create the XFormItem subclass from the item attributes passed in 106 // (also links to the model) 107 var item = XModelItemFactory.createItem(itemAttr, parentItem, this); 108 109 110 111 // have the item initialize it's sub-items, if necessary (may be recursive) 112 item.initializeItems(); 113 114 return item; 115 } 116 117 XModel.prototype.addItem = function(item, parentItem) { 118 if (!item.__isXModelItem) item = this.initItem(item, parentItem); 119 if (parentItem == null) { 120 this.items.push(item); 121 } else { 122 parentItem.addItem(item); 123 } 124 } 125 126 // add an item to our index, so we can find it easily later 127 XModel.prototype.indexItem = function(item, path) { 128 this._pathIndex[path] = item; 129 } 130 131 132 133 134 135 136 // 137 // getting modelItems, parent items, their paths, etc 138 // 139 140 XModel.prototype.getItem = function(path, createIfNecessary) { 141 // try to find the item by the path, return if we found it 142 var item = this._pathIndex[path]; 143 if (item != null) return this._pathIndex[path]; 144 145 // if we didn't find it, try normalizing the path 146 var normalizedPath = this.normalizePath(path); 147 // convert any "#1", etc to just "#" 148 for (var i = 0; i < normalizedPath.length; i++) { 149 if (normalizedPath[i].charAt(0) == "#") normalizedPath[i] = "#"; 150 } 151 // and if we find it, save that item under the original path and return it 152 item = this._pathIndex[normalizedPath.join(this.pathDelimiter)]; 153 if (item != null) { 154 this._pathIndex[path] = item; 155 return item; 156 } 157 158 if (createIfNecessary != true) return null; 159 160 // get each parent item (creating if necessary) until we get to the end 161 var parentItem = null; 162 for (var p = 0; p < normalizedPath.length; p++) { 163 var itemPath = normalizedPath.slice(0, p+1).join(this.pathDelimiter); 164 var item = this.getItem(itemPath, false); 165 if (item == null) { 166 //DBG.println("making modelItem for ", itemPath); 167 item = XModelItemFactory.createItem({id:normalizedPath[p]}, parentItem, this); 168 } 169 parentItem = item; 170 } 171 return item; 172 } 173 174 175 176 // "normalize" a path and return it split on the itemDelimiter for this model 177 XModel.prototype.normalizePath = function (path) { 178 if (path.indexOf("[") > -1) { 179 path = path.split("[").join("/#"); 180 path = path.split("]").join(""); 181 } 182 if (path.indexOf(".") > -1) { 183 path = path.split(/[\/\.]+/); 184 var outputPath = []; 185 for (var i = 0; i < path.length; i++) { 186 var step = path[i]; 187 if (step == "..") { 188 outputPath.pop(); 189 } else if (step != ".") { 190 outputPath.push(step); 191 } 192 } 193 return outputPath; 194 } 195 return path.split(this.pathDelimiter); 196 } 197 198 199 XModel.prototype.getParentPath = function (path) { 200 path = this.normalizePath(path); 201 return path.slice(0, path.length - 1); 202 } 203 204 XModel.prototype.getLeafPath = function (path) { 205 path = this.normalizePath(path); 206 return path[path.length - 1]; 207 } 208 209 210 211 212 213 214 XModel.prototype.getInstanceValue = function (instance, path) { 215 var getter = this._getPathGetter(path); 216 return getter.call(this, instance); 217 } 218 219 XModel.prototype.getParentInstanceValue = function (instance, path) { 220 var getter = this._getParentPathGetter(path); 221 return getter.call(this, instance); 222 } 223 224 XModel.prototype.setInstanceValue = function (instance, path, value) { 225 //DBG.println("setInstanceValue(",path,"): ", value, " (",typeof value,")"); 226 var parentValue = this.getParentInstanceValue(instance, path); 227 if (parentValue == null) { 228 parentValue = this.setParentInstanceValues(instance, path); 229 } 230 var modelItem = this.getItem(path, true); 231 var leafPath = this.getLeafPath(path); 232 var ref = modelItem.ref; 233 if (leafPath.charAt(0) == "#") ref = parseInt(leafPath.substr(1)); 234 235 if (modelItem.setter) { 236 // convert "/" to "." in the ref 237 if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join("."); 238 239 var setter = modelItem.setter; 240 var scope = modelItem.setterScope; 241 if (scope == _INHERIT_) scope = this.setterScope; 242 if (scope == _INSTANCE_) { 243 instance[setter](value, parentValue, ref); 244 } else if (scope == _MODEL_) { 245 this[setter](value, instance, parentValue, ref); 246 } else { 247 modelItem[setter](value, instance, parentValue, ref); 248 } 249 } else { 250 if (typeof ref == "string" && ref.indexOf(this.pathDelimiter) > -1) { 251 ref = ref.split(this.pathDelimiter); 252 for (var i = 0; i < ref.length - 1; i++) { 253 parentValue = parentValue[ref[i]]; 254 } 255 ref = ref.pop(); 256 } 257 parentValue[ref] = value; 258 var parentItem = modelItem.getParentItem(); 259 if(parentItem) { 260 var parentPath = this.getParentPath(path).join(this.pathDelimiter); 261 if(parentItem.setter) { 262 XModel.prototype.setInstanceValue.call(this,instance, parentPath, parentValue); 263 } else { 264 var event = new DwtXModelEvent(instance, parentItem, parentPath, parentValue); 265 parentItem.notifyListeners(DwtEvent.XFORMS_VALUE_CHANGED, event); 266 } 267 } 268 } 269 270 //notify listeners that my value has changed 271 var event = new DwtXModelEvent(instance, modelItem, path, value); 272 modelItem.notifyListeners(DwtEvent.XFORMS_VALUE_CHANGED, event); 273 return value; 274 } 275 276 277 XModel.prototype.setParentInstanceValues = function (instance, path) { 278 var pathList = this.getParentPath(path); 279 for (var i = 0; i < pathList.length; i++) { 280 var itemPath = pathList.slice(0, i+1).join(this.pathDelimiter); 281 var itemValue = this.getInstanceValue(instance, itemPath); 282 if (itemValue == null) { 283 var modelItem = this.getItem(itemPath, true); 284 var defaultValue = modelItem.getDefaultValue(); 285 itemValue = this.setInstanceValue(instance, itemPath, defaultValue); 286 } 287 } 288 return itemValue; 289 } 290 291 292 293 294 295 296 297 //NOTE: model.getInstance() gets count of PARENT 298 // "modelItem" is a pointer to a modelItem, or an path as a string 299 XModel.prototype.getInstanceCount = function (instance, path) { 300 var list = this.getParentInstanceValue(instance, path); 301 if (list != null && list.length) return list.length; 302 return 0; 303 } 304 305 306 // "path" is a path of id's 307 XModel.prototype.addRowAfter = function (instance, path, afterRow) { 308 var newInstance = null; 309 310 var modelItem = this.getItem(path); 311 if (modelItem) { 312 newInstance = this.getNewListItemInstance(modelItem); 313 } else { 314 newInstance = ""; 315 } 316 var list = this.getInstanceValue(instance, path); 317 if (list == null) { 318 list = []; 319 } 320 321 list.splice(afterRow+1, 0, newInstance); 322 this.setInstanceValue(instance, path, list); 323 } 324 325 326 XModel.prototype.getNewListItemInstance = function (modelItem) { 327 var listItem = modelItem.listItem; 328 if (listItem == null) return ""; 329 return this.getNewInstance(listItem); 330 } 331 332 XModel.prototype.getNewInstance = function (modelItem) { 333 if (modelItem.defaultValue != null) return modelItem.defaultValue; 334 335 var type = modelItem.type; 336 switch (type) { 337 case _STRING_: 338 return ""; 339 340 case _NUMBER_: 341 return 0; 342 343 case _OBJECT_: 344 var output = {}; 345 if (modelItem.items) { 346 for (var i = 0; i < modelItem.items.length; i++) { 347 var subItem = modelItem.items[i]; 348 if (subItem.ref) { 349 output[subItem.ref] = this.getNewInstance(subItem); 350 } else if (subItem.id) { 351 output[subItem.id] = this.getNewInstance(subItem); 352 } 353 } 354 355 } 356 return output; 357 358 case _LIST_: 359 return []; 360 361 case _DATE_: 362 case _TIME_: 363 case _DATETIME_: 364 return new Date(); 365 366 default: 367 return ""; 368 } 369 } 370 371 372 373 // "modelItem" is a pointer to a modelItem, or an path as a string 374 XModel.prototype.removeRow = function (instance, path, instanceNum) { 375 var list = this.getInstanceValue(instance, path); 376 if (list == null) return; 377 378 var isString = false; 379 if(list instanceof String || typeof(list) == "string") 380 isString=true; 381 382 var tmpList = isString ? list.split(",") : list; 383 var newList = []; 384 var cnt = tmpList.length; 385 for (var i=0;i<cnt;i++) { 386 if(i != instanceNum) { 387 newList.push(tmpList[i]); 388 } 389 } 390 391 this.setInstanceValue(instance, path, newList); 392 } 393 394 395 396 397 398 399 // for speed, we create optimized functions to traverse paths in the instance 400 // to actually return values for an instance. Make them here. 401 // 402 XModel.prototype._getPathGetter = function (path) { 403 //DBG.println("_getPathGetter(",path,")"); 404 var getter = this._pathGetters[path]; 405 if (getter != null) return getter; 406 407 getter = this._makePathGetter(path); 408 //DBG.println("assigning path getter for ", path, " to ", getter); 409 this._pathGetters[path] = getter; 410 return getter; 411 } 412 413 XModel.prototype._getParentPathGetter = function (path) { 414 var getter = this._parentGetters[path]; 415 if (getter != null) return getter; 416 417 var parentPath = this.getParentPath(path).join(this.pathDelimiter); 418 getter = this._getPathGetter(parentPath); 419 this._parentGetters[path] = getter; 420 this._pathGetters[parentPath] = getter; 421 return getter; 422 } 423 424 XModel.prototype._makePathGetter = function (path) { 425 if (path == null) return new Function("return null"); 426 427 // normalizePath() converts to an array, fixes all "." and ".." items, and changes [x] to #x 428 var pathList = this.normalizePath(path); 429 430 // forget any leading slashes 431 if (pathList[0] == "") pathList = pathList.slice(1); 432 433 // DBG.println("_makePathGetter(", path, "): ", pathList); 434 var methodSteps = []; 435 var pathToStep = ""; 436 437 for (var i = 0; i < pathList.length; i++) { 438 var pathStep = pathList[0, i]; 439 440 if (pathStep.charAt(0) == "#") { 441 pathStep = pathStep.substr(1); 442 pathToStep = pathToStep + "#"; 443 } else { 444 pathToStep = pathToStep + pathStep; 445 446 } 447 var modelItem = this.getItem(pathToStep, true); 448 var ref = modelItem.ref; 449 450 // convert "/" to "." in the ref 451 if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join("."); 452 453 if (modelItem.getter) { 454 var getter = modelItem.getter; 455 var scope = modelItem.getterScope; 456 if (scope == _INHERIT_) { 457 scope = this.getterScope; 458 } 459 460 if (scope == _INSTANCE_) { 461 methodSteps.push("if(instance) {"); 462 methodSteps.push(" current = instance."+ getter+ "(current, '"+ref+"');"); 463 methodSteps.push("}"); 464 } else if (scope == _MODEL_) { 465 methodSteps.push("current = this."+ getter+ "(instance, current, '"+ref+"');"); 466 } else { 467 methodSteps.push("current = this.getItem(\""+ pathToStep+ "\")."+ getter+ "(instance, current, '"+ref+"');"); 468 } 469 470 } else if (ref == "#") { 471 methodSteps.push("if(current) {"); 472 methodSteps.push(" current = current[" + pathStep + "];"); 473 methodSteps.push("}"); 474 } else { 475 methodSteps.push("if(current) {"); 476 methodSteps.push(" current = current." + ref + ";"); 477 methodSteps.push("}"); 478 } 479 pathToStep += this.pathDelimiter; 480 } 481 482 var methodBody = AjxBuffer.concat( 483 "try {\r", 484 "var current = instance;\r", 485 "\t", methodSteps.join("\r\t"), "\r", 486 "} catch (e) {\r ", 487 " DBG.println('Error in getting path for \"", path, "\": ' + e);\r", 488 " current = null;\r", 489 "}\r", 490 "return current;\r" 491 ); 492 //DBG.println(path,"\r\t", methodSteps.join("\r\t"), "\r"); 493 var method = new Function("instance", methodBody); 494 495 return method; 496 } 497 498 499 500 501 502 503 504 505 // error messages 506 // NOTE: every call to XModel.prototype.registerError() should be translated! 507 508 XModel._errorMessages = {}; 509 XModel.registerErrorMessage = XModel.prototype.registerErrorMessage = function (id, message) { 510 this._errorMessages[id] = message; 511 } 512 XModel.registerErrorMessage("unknownError", "Unknown error."); 513 XModel.prototype.defaultErrorMessage = "unknownError"; 514 515 516 // set the default error message for the model (it's not a bad idea to override this in your models!) 517 XModel.prototype.getDefaultErrorMessage = function (modelItem) { 518 if (modelItem && modelItem.errorMessage) { 519 return modelItem.getDefaultErrorMessage(); 520 } 521 522 return this.defaultErrorMessage; 523 } 524 525 XModel.prototype.getErrorMessage = function (id, arg0, arg1, arg2, arg3, arg4) { 526 var msg = this._errorMessages[id]; 527 if (msg == null) msg = XModel._errorMessages[id]; 528 529 if (msg == null) { 530 DBG.println("getErrorMessage('", id, "'): message not found. If this is an actual error message, add it to the XModel error messages so it can be translated."); 531 return id; 532 } 533 if (arg0 !== null) msg = msg.split("{0}").join(arg0); 534 if (arg1 !== null) msg = msg.split("{1}").join(arg1); 535 if (arg2 !== null) msg = msg.split("{2}").join(arg2); 536 if (arg3 !== null) msg = msg.split("{3}").join(arg3); 537 if (arg4 !== null) msg = msg.split("{4}").join(arg4); 538 return msg; 539 } 540