diff --git a/.vs/SpecIF-Viewer/v16/.suo b/.vs/SpecIF-Viewer/v16/.suo index 9256227..709331c 100644 Binary files a/.vs/SpecIF-Viewer/v16/.suo and b/.vs/SpecIF-Viewer/v16/.suo differ diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json index a6ab3dd..6a3e4af 100644 --- a/.vs/VSWorkspaceState.json +++ b/.vs/VSWorkspaceState.json @@ -11,5 +11,6 @@ "\\src\\vendor\\assets\\javascripts", "\\src\\vendor\\assets\\stylesheets" ], + "SelectedNode": "\\src\\vendor\\assets\\javascripts\\toXhtml.js", "PreviewInSolutionExplorer": false } \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite index 1bb86c1..284e3ea 100644 Binary files a/.vs/slnx.sqlite and b/.vs/slnx.sqlite differ diff --git a/src/check.html b/src/check.html index 643fad2..73ca19d 100644 --- a/src/check.html +++ b/src/check.html @@ -40,7 +40,7 @@ document.body.appendChild(el) } let pend=4; -getScript( 'https://code.jquery.com/jquery-3.5.1.min.js' ); +getScript('https://code.jquery.com/jquery-3.6.0.min.js'); getScript( './config/definitions.js?'+ Date.now().toString() ); // with cache-busting getScript( './config/moduleManager.js?'+ Date.now().toString() ); getScript( './'+fileName+'.js?'+ Date.now().toString() ); diff --git a/src/check.ts b/src/check.ts index bec1d3b..3926773 100644 --- a/src/check.ts +++ b/src/check.ts @@ -100,10 +100,11 @@ function checkSpecif():IApp { self.logout ); }; - self.export = function() { - if( !self.cache.selectedProject || !self.cache.selectedProject.data.id ) - message.show( i18n.MsgNoProjectLoaded, {severity:'warning', duration:CONFIG.messageDisplayTimeShort} ); - self.cache.selectedProject.chooseFormatAndExport(); + self.export = function (): void { + if (self.cache.selectedProject && self.cache.selectedProject.isLoaded()) + self.cache.selectedProject.chooseFormatAndExport(); + else + message.show(i18n.MsgNoProjectLoaded, { severity: 'warning', duration: CONFIG.messageDisplayTimeShort }); }; /* self.updateMe = function() { self.me.beginUpdate(); diff --git a/src/config/definitions.ts b/src/config/definitions.ts index 8153859..3da7065 100644 --- a/src/config/definitions.ts +++ b/src/config/definitions.ts @@ -7,7 +7,7 @@ .. or even better as Github issue (https://github.com/GfSE/SpecIF-Viewer/issues) */ const CONFIG:any = {}; - CONFIG.appVersion = "1.0.s", + CONFIG.appVersion = "1.0.t", CONFIG.specifVersion = "1.0"; CONFIG.imgURL = './vendor/assets/images'; // CONFIG.userNameAnonymous = 'anonymous'; // as configured in the server @@ -542,8 +542,8 @@ const vocabulary = { case "specif_status": oT = "ReqIF.ForeignState"; break; case "dcterms_author": // deprecated, for compatibility case "dcterms_creator": oT = "ReqIF.ForeignCreatedBy"; break; - // case "specif_createdat": - // case "dcterms_modified": oT = "ReqIF.ForeignCreatedAt"; // exists? + // case "specif_createdat": + // case "dcterms_modified": oT = "ReqIF.ForeignCreatedAt"; // exists? default: oT = iT; }; return oT; @@ -680,13 +680,20 @@ const RE:any = {}; // For example, the ARCWAY Cockpit export uses this pattern: // RE.tagA = new RegExp( ']+)>([\\s\\S]*?)', 'g' ); - RE.tagImg = new RegExp( ']+)/>', 'g' ); + RE.tagImg = new RegExp(']+)/>', 'g'); + RE.tagObject = /]+?)(\/>|>)/g; + RE.attrType = /type="([^"]+)"/; + RE.attrData = /data="([^"]+)"/; -const reSO = ']+)(/>|>([^<]*?))'; +const reSO = ']+?)(/>|>([^<]*?))'; RE.tagSingleObject = new RegExp( reSO, 'g' ); - RE.tagNestedObjects = new RegExp( ']+)>[\\s]*'+reSO+'([\\s\\S]*?)', 'g' ); + RE.tagNestedObjects = new RegExp( ']+?)>[\\s]*'+reSO+'([\\s\\S]*?)', 'g' ); RE.inQuotes = /"(\S[^"]*?\S)"|'(\S[^']*?\S)'/i; // empty space in the middle allowed, but not as first and last character - RE.inBrackets = /\((\S[^\)]*?\S)\)|\[(\S[^\]]*?\S)\]/i; // empty space in the middle allowed, but not as first and last character + +const inBr = "\\((\\S[^\\)]*?\\S)\\)|\\[(\\S[^\\]]*?\\S)\\]"; // empty space in the middle allowed, but not as first and last character +// RE.inBrackets = new RegExp( inBr, 'i'); + RE.inBracketsAtEnd = new RegExp(inBr + "$", 'i'); + RE.withoutBracketsAtEnd = /^(.*?)\s+(\(\S[^\)]*?\S\)|\[\S[^\]]*?\S\])$/i; const tagStr = "(<\\/?)([a-z]{1,10}(?: [^<>]+)?\\/?>)"; RE.tag = new RegExp( tagStr, 'g' ); @@ -704,5 +711,5 @@ const tokenGroup = "(p|div|br|b|i|em|span|ul|ol|li|a|table|thead|tbody|tfoot|th| // $3: any of the tokens listed in tokenGroup // $4: the rest of the tag including '>' or '/>' -///////////////// -const nbsp = ' '; // non-breakable space + RE.splitNamespace = /^(\w+:|\w+\.)?(\w+)$/; + diff --git a/src/config/locales/iLaH-de.i18n.ts b/src/config/locales/iLaH-de.i18n.ts index 79b9234..12193b0 100644 --- a/src/config/locales/iLaH-de.i18n.ts +++ b/src/config/locales/iLaH-de.i18n.ts @@ -524,11 +524,13 @@ function LanguageTextsDe() { self.SpecIF_Origin = "Quelle"; // oder "Herkunft" self.SpecIF_Source = self.LblSource; self.SpecIF_Target = self.LblTarget; + self.SpecIF_DataObject = "Datenobjekt"; // self.SpecIF_Author = "Autor"; // self.SpecIF_Authors = "Autoren"; self.IREB_Stakeholder = "Stakeholder"; self.SpecIF_Responsible = "Verantwortlicher"; self.SpecIF_Responsibles = "Verantwortliche"; + self.SpecIF_UserRole = "Nutzerrolle"; // attribute names used by the Interaction Room: self.IR_Annotation = "Annotation"; self.IR_AnnotationDescription = "Eine Interaction-Room '"+self.IR_Annotation+"' weist auf einen Punkt besonderen Interesses hin. Hierzu gehören Werte, die Kundenbedürfnisse widerspiegeln oder positiven Effekt auf die Ziele der Organisation haben, Produkteigenschaften, deren Umsetzung Nutzen/Aufwand verursacht, und Herausforderungen, die während der Entwicklung zu berücksichtigen sind."; diff --git a/src/config/locales/iLaH-en.i18n.ts b/src/config/locales/iLaH-en.i18n.ts index 8be68dc..9a83121 100644 --- a/src/config/locales/iLaH-en.i18n.ts +++ b/src/config/locales/iLaH-en.i18n.ts @@ -524,11 +524,13 @@ function LanguageTextsEn() { self.SpecIF_Origin = "Origin"; self.SpecIF_Source = self.LblSource; self.SpecIF_Target = self.LblTarget; + self.SpecIF_DataObject = "Data-Object"; // self.SpecIF_Author = "Author"; // self.SpecIF_Authors = "Authors"; self.IREB_Stakeholder = "Stakeholder"; self.SpecIF_Responsible = "Responsible"; self.SpecIF_Responsibles = "Responsibles"; + self.SpecIF_UserRole = "User-Role"; // attribute names used by the Interaction Room: self.IR_Annotation = "Annotation"; self.IR_AnnotationDescription = "An Interaction-Room '"+self.IR_Annotation+"' indicates a point of special interest. Examples are customer or user value, product features with prominent benefit,effort or challenges during development."; diff --git a/src/config/locales/iLaH-fr.i18n.ts b/src/config/locales/iLaH-fr.i18n.ts index a1d2da7..e183d2a 100644 --- a/src/config/locales/iLaH-fr.i18n.ts +++ b/src/config/locales/iLaH-fr.i18n.ts @@ -523,12 +523,14 @@ function LanguageTextsFr() { self.SpecIF_Instantiation = "Instanciation"; self.SpecIF_Origin = "Origine"; self.SpecIF_Source = self.LblSource; - self.SpecIF_Target = self.LblTarget; + self.SpecIF_Target = self.LblTarget; + self.SpecIF_DataObject = "Objet de Données"; // self.SpecIF_Author = "Auteur"; // self.SpecIF_Authors = "Auteurs"; self.IREB_Stakeholder = "Stakeholder"; self.SpecIF_Responsible = "Responsable"; self.SpecIF_Responsibles = "Responsables"; + self.SpecIF_UserRole = "Rôle"; // attribute names used by the Interaction Room: self.IR_Annotation = "Annotation"; self.IR_AnnotationDescription = "An Interaction-Room '"+self.IR_Annotation+"' indicates a point of special interest. Examples are customer or user value, product features with prominent benefit,effort or challenges during development."; diff --git a/src/config/moduleManager.ts b/src/config/moduleManager.ts index 1869504..e817aae 100644 --- a/src/config/moduleManager.ts +++ b/src/config/moduleManager.ts @@ -444,7 +444,7 @@ var app:IApp, setViewToLeaf( mo, params ); return; - function setViewFromRoot( le:IModule, pL ):void { + function setViewFromRoot( le:IModule, pL:any[] ):void { // step up, if there is a parent view: if( le.parent.selectedBy ) { // all levels get access to the parameters besides view, if needed: @@ -455,7 +455,7 @@ var app:IApp, // set this level's view controller to choose the desired view: le.parent.ViewControl.show( pL ) } - function setViewToLeaf(le: IModule, pL ):void { + function setViewToLeaf(le: IModule, pL: any[] ):void { // step down, if there is a child view: function findDefault(vL: IModule[]): IModule { for( var i=vL.length-1; i>-1; i-- ) { @@ -532,13 +532,13 @@ var app:IApp, getScript( 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1/js/bootstrap.min.js' ); return true; case "bootstrapDialog": getCss( "https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-dialog/1.35.4/css/bootstrap-dialog.min.css" ); getScript( 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap3-dialog/1.35.4/js/bootstrap-dialog.min.js' ); return true; - case "tree": getCss( "https://cdnjs.cloudflare.com/ajax/libs/jqtree/1.5.3/jqtree.css" ); - getScript( 'https://cdnjs.cloudflare.com/ajax/libs/jqtree/1.5.3/tree.jquery.js' ); return true; - case "fileSaver": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.4/FileSaver.min.js' ); return true; - case "zip": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.6.0/jszip.min.js' ); return true; + case "tree": getCss( "https://cdnjs.cloudflare.com/ajax/libs/jqtree/1.6.1/jqtree.css" ); + getScript( 'https://cdnjs.cloudflare.com/ajax/libs/jqtree/1.6.1/tree.jquery.js' ); return true; + case "fileSaver": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js' ); return true; + case "zip": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js' ); return true; case "jsonSchema": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/ajv/4.11.8/ajv.min.js' ); return true; - case "excel": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.16.8/xlsx.full.min.js' ); return true; - case "bpmnViewer": getScript( 'https://unpkg.com/bpmn-js@7.2.1/dist/bpmn-viewer.production.min.js' ); return true; + case "excel": getScript( 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.2/xlsx.full.min.js' ); return true; + case "bpmnViewer": getScript( 'https://unpkg.com/bpmn-js@8.7.3/dist/bpmn-viewer.production.min.js' ); return true; case "graphViz": // getCss( "https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.1/vis-network.min.css" ); getScript( 'https://cdnjs.cloudflare.com/ajax/libs/vis/4.20.1/vis-network.min.js' ); return true; // case "pouchDB": getScript( 'https://unpkg.com/browse/pouchdb@7.2.2/dist/pouchdb.min.js' ); return true; @@ -641,7 +641,7 @@ var app:IApp, // case CONFIG.files: getScript( loadPath+'modules/files-0.93.1.js'); return true; default: console.warn( "Module loader: Module '"+mod+"' is unknown." ); return false; - } + }; }; return false; @@ -652,7 +652,7 @@ var app:IApp, // (third party libraries delivered by CDN have a version in the path); // it must work for the regular app and the embedded app: function bust(url: string): string { - return url + (url.startsWith(loadPath) ? "?" + CONFIG.appVersion : "") + return url + (url.startsWith(loadPath) ? "?" + CONFIG.appVersion : ""); } function getCss( url:string ):void { $('head').append('' ); @@ -694,7 +694,7 @@ var app:IApp, callWhenReady() else throw Error("No callback provided to continue after module loading."); - } + }; } }(); class State { @@ -716,13 +716,13 @@ class State { case undefined: case true: this.state = true; - this.hideWhenSet.forEach((v:string):void =>{ + this.hideWhenSet.forEach((v: string): void => { $(v).hide(); }); - this.showWhenSet.forEach((v:string):void =>{ + this.showWhenSet.forEach((v: string): void => { $(v).show(); - }) - } + }); + }; } reset ():void { this.state = false; diff --git a/src/edit.html b/src/edit.html index 643fad2..eb196a4 100644 --- a/src/edit.html +++ b/src/edit.html @@ -40,7 +40,7 @@ document.body.appendChild(el) } let pend=4; -getScript( 'https://code.jquery.com/jquery-3.5.1.min.js' ); +getScript( 'https://code.jquery.com/jquery-3.6.0.min.js' ); getScript( './config/definitions.js?'+ Date.now().toString() ); // with cache-busting getScript( './config/moduleManager.js?'+ Date.now().toString() ); getScript( './'+fileName+'.js?'+ Date.now().toString() ); diff --git a/src/edit.ts b/src/edit.ts index 472b870..6cc4d6b 100644 --- a/src/edit.ts +++ b/src/edit.ts @@ -175,10 +175,11 @@ function editSpecif():IApp { self.logout ); }; - self.export = function() { - if( !self.cache.selectedProject || !self.cache.selectedProject.data.id ) - message.show( i18n.MsgNoProjectLoaded, {severity:'warning', duration:CONFIG.messageDisplayTimeShort} ); - self.cache.selectedProject.chooseFormatAndExport(); + self.export = function (): void { + if (self.cache.selectedProject && self.cache.selectedProject.isLoaded()) + self.cache.selectedProject.chooseFormatAndExport(); + else + message.show(i18n.MsgNoProjectLoaded, { severity: 'warning', duration: CONFIG.messageDisplayTimeShort }); }; /* self.updateMe = function() { self.me.beginUpdate(); diff --git a/src/embedded.ts b/src/embedded.ts index 8a5d767..4b7581a 100644 --- a/src/embedded.ts +++ b/src/embedded.ts @@ -107,7 +107,7 @@ function embeddedSpecif():IApp { }; self.show = function() { // data and type are valid, but it is necessary to indicate that the data is not zipped: - self.ioSpecif.init( {mediaTypeOf: attachment2mediaType} ); + self.ioSpecif.init( {mediaTypeOf: Lib.attachment2mediaType} ); self.ioSpecif.verify( {name:'data.specif'} ); self.busy.set(); @@ -132,12 +132,12 @@ function embeddedSpecif():IApp { CONFIG.showTimelag ); }) - .fail( stdError ); + .fail( Lib.stdError ); }, - stdError + Lib.stdError ); }) - .fail( stdError ); + .fail( Lib.stdError ); }; self.logout = function() { self.me.logout(); diff --git a/src/modules/cache.mod.ts b/src/modules/cache.mod.ts index 5a2a980..7cf5159 100644 --- a/src/modules/cache.mod.ts +++ b/src/modules/cache.mod.ts @@ -35,684 +35,617 @@ interface IFileWithContent extends SpecifFile { blob?: Blob; dataURL?: string; } -interface IProject { - data: CSpecIF; - exporting: boolean; - abortFlag: boolean; - init: Function; - isLoaded: Function; - create: Function; - update: Function; - createContent: Function; - readContent: Function; - updateContent: Function; - deleteContent: Function; - createResource: Function; - readStatementsOf: Function; - createFolderWithResourcesByType: Function; - createFolderWithGlossary: Function; - deduplicate: Function; - exportFormatClicked: Function; - exportOptionsClicked: Function; - exportOptions: Function; - chooseFormatAndExport: Function; - exportAs: Function; - abort: Function; -} -// This will be merged with the basic declaration of IModule in moduleManager.ts: -// interface IModule { -interface IProjects extends IModule { +class CCache { + // Common Cache for all locally handled projects (SpecIF data-sets) cacheInstances: boolean; - projects: IProject[]; - selectedProject: IProject; - create: Function; -} - -moduleManager.construct({ - name: 'cache' -}, (self: IProjects) => { - // Construct a representative of the selected project with cached data: - // ToDo: enforce CONFIG.maxItemsToCache - -/* var autoLoadId, // max 1 autoLoad chain - autoLoadCb; // callback function when the cache has been updated */ - self.cacheInstances = true; - - // initialization is at the end of this constructor. - self.init = ():boolean =>{ - // initialize/clear all variables: - self.projects = []; - self.selectedProject = undefined; - - /* autoLoadId = undefined; // stop any autoLoad chain - autoLoadCb = undefined; */ - - return true - }; - self.create = (prj:SpecIF, opts:any ): JQueryDeferred => { - // in this implementation, delete existing projects to save memory space: - self.projects.length = 0; - - // append a project to the list: - self.projects.push( Project() ); - self.selectedProject = self.projects[self.projects.length-1]; - return self.selectedProject.create( prj, opts ); - }; -/* self.update = (prj:SpecIF, opts:any ) => { - if (!prj) return; - // search the project and select it: - ... - // update: - ... - }; - // Periodically update the selected project with the current server state in a multi-user context: - self.startAutoLoad = ( cb )=>{ -// if( !self.cacheInstances ) return; -// console.info( 'startAutoLoad' ); - if( typeof(cb)=="function" ) { autoLoadCb = cb }; - autoLoadId = genID( 'aU-' ); - // get all resources from the server to fill the cache: - setTimeout( ()=>{ autoLoad(autoLoadId) }, 600 ) // start a little later ... - }; - self.stopAutoLoad = ()=>{ -// console.info('stopAutoLoad'); - autoLoadId = null; - loading = false - }; - self.loadInstances = ( cb )=>{ - // for the time being - until the synchronizing will be implemented: -// if( !self.cacheInstances ) return; - // load the instances of the selected hierarchy (spec) into the cache (but not the types): -// console.debug( 'self.loadInstances', self.selectedHierarchy, cb ); - if( self.selectedHierarchy ) { - loading = true; - // update all resources referenced by the selectedHierarchy: - loadObjsOf( self.selectedHierarchy ) - .done( ()=>{ -// loadRelsOf( self.selectedHierarchy ); - // update the hierarchy (outline). - // it is done after the resources to reflect any change in the hierarchy made during the loading. - self.readContent( 'hierarchy', self.selectedHierarchy, true ) // true: reload - // - call cb to refresh the app: - .done( ()=>{ - if( typeof(cb)=="function" ) cb(); - loading = false - }) - .fail( (xhr)=>{ - loading = false - }) - }) - .fail( (xhr)=>{ - loading = false - }) + dataTypes: DataType[]; + propertyClasses: PropertyClass[]; + resourceClasses: ResourceClass[]; + statementClasses: StatementClass[]; + resources: Resource[]; // list of resources as referenced by the hierarchies + statements: Statement[]; + hierarchies: SpecifNode[]; // listed specifications (aka hierarchies, outlines) of all loaded projects + files: IFileWithContent[]; + constructor(opts:any) { + this.cacheInstances = opts.cacheInstances; + for (var le of standardTypes.listName.keys()) + this[standardTypes.listName.get(le)] = []; + } + length(ctg: string): number { + // Return the number of cached items per category: + return this[standardTypes.listName.get(ctg)].length; + } + has(ctg: string, req: Item[] | Item | string): boolean { + if (req == 'all') + // this is a stupid request, in fact. + throw Error("Querying whether 'all' items are available is pretty useless."); + + if (Array.isArray(req)) { + let L = this[standardTypes.listName.get(ctg)]; + for (var i = req.length - 1; i > -1;i--) { + if (indexById(L, req.id || req) < 0) return false; + }; + return true; } - }; - self.load = (opts)=>{ - var lDO = $.Deferred(); - - // load referenced resources and statements ... - if( opts.loadObjects ) { - if( opts.loadAllSpecs ) - loadAll( 'resource' ) - .done( ()=>{ - if( opts.loadRelations ) - return loadAll( 'statement' ) - .done( lDO.resolve ) - .fail( lDO.reject ); - // else - lDO.resolve() - }) - .fail( lDo.reject ) + else + return indexById(this[standardTypes.listName.get(ctg)],req)>-1; + } + put(ctg: string, item: Item[] | Item): void { + if (!item || Array.isArray(item) && item.length < 1) + return; + // If item is a list, all elements must have the same category. + function cacheIfNewerE(L: Item[], e: Item): number { // ( list, entry ) + // Add or update the item e in a list L, if created later: + let n = typeof (e) == 'object' ? indexById(L, e.id) : L.indexOf(e); + // add, if not yet listed: + if (n < 0) { + L.push(e); + return L.length - 1; + }; + // Update, if newer: + if (L[n].changedAt && e.changedAt && new Date(L[n].changedAt) < new Date(e.changedAt)) + L[n] = e; + return n; + } + function cacheIfNewerL(L: Item[], es: Item[]): void { // ( list, entries ) + // add or update the items es in a list L: + es.forEach((e) => { cacheIfNewerE(L, e) }) + // this operation cannot fail: + return true; + } + let fn = Array.isArray(item) ? cacheIfNewerL : cacheIfNewerE; + switch (ctg) { + case 'hierarchy': + case 'dataType': + case 'propertyClass': + case 'resourceClass': + case 'statementClass': + // @ts-ignore - addressing is perfectly ok + return fn(this[standardTypes.listName.get(ctg)], item); + case 'resource': + case 'statement': + case 'file': + if (this.cacheInstances) { + // @ts-ignore - addressing is perfectly ok + return fn(this[standardTypes.listName.get(ctg)], item); + }; + return true; + case 'node': + if (Array.isArray(item)) + throw Error("No list of nodes supported."); +// console.debug('cache',ctg,item); + return this.putNode(item as INodeWithPosition); + default: + throw Error("Invalid category '" + ctg + "'."); + }; + } + get(ctg: string, req: Item[] | Item | string): Item[] { + // Read items from cache + // - req can be single or a list, + // - each element can be an object with attribute id or an id string + // - original lists and items are delivered, so don't change them! + // @ts-ignore - addressing is perfectly ok + let itmL: Item[] = this[standardTypes.listName.get(ctg)], + idx: number; + + if (req == 'all') + return [].concat(itmL); // return all cached items in a new list + + if (Array.isArray(req)) { + let allFound = true, i = 0, I = req.length; + var rL: Item[] = []; + while (allFound && i < I) { + idx = indexById(itmL, req[i].id || req[i]); + if (idx > -1) { + rL.push(itmL[idx]); + i++; + } + else + allFound = false; + }; + if (allFound) + return rL; else - loadObjsOf( self.selectedHierarchy ) - .done( ()=>{ - if( opts.loadRelations ) - return loadRelsOf( self.selectedHierarchy ) - .done( lDO.resolve ) - .fail( lDO.reject ); - // else - lDO.resolve() - }) - .fail( lDo.reject ); - return - } else { - lDO.resolve() + return []; + } + else { + // is a single item: + idx = indexById(itmL, req.id || req); + if (idx > -1) + return [itmL[idx]] + else + return []; }; - return lDO - }; -*/ - return self; -}); + } + del(ctg: string, item: Item): boolean | undefined { + if (!item) return; + let fn = Array.isArray(item) ? Lib.uncacheL : Lib.uncacheE; + switch (ctg) { + case 'hierarchy': + case 'dataType': + case 'propertyClass': + case 'resourceClass': + case 'statementClass': + // @ts-ignore - addressing is perfectly ok + return fn(this[standardTypes.listName.get(ctg)], item); + case 'resource': + case 'statement': + case 'file': + if (this.cacheInstances) + // @ts-ignore - addressing is perfectly ok + return fn(this[standardTypes.listName.get(ctg)], item); + return true; + case 'node': + if (Array.isArray(item)) + item.forEach((el: SpecifNode) => { delNodes(this.hierarchies, el) }) + else + delNodes(this.hierarchies, item as SpecifNode); + return true; + default: + throw Error("Invalid category '" + ctg + "'."); + }; + // all cases have a return statement .. + + function delNodes(L: SpecifNode[], el: SpecifNode): void { + // Delete all nodes specified by the element; + // if el is the node, 'id' will be used to identify it (obviously at most one node), + // and if el is the referenced resource, 'resource' will be used to identify all referencing nodes. + if (Array.isArray(L)) + for (var h = L.length - 1; h > -1; h--) { + if (L[h].id == el.id || L[h].resource == el.resource) { + L.splice(h, 1); + break; // can't delete any children + }; + // step down, if the node hasn't been deleted: + delNodes(L[h].nodes as SpecifNode[], el); + }; + } + } + private putNode(e: INodeWithPosition): boolean { + // add or replace a node in a hierarchy; + // e may specify a predecessor or parent, the former prevails if both are specified + // - if there is no predecessor or it isn't found, insert as first element + // - if no parent is specified or the parent isn't found, insert at root level + + // 1. Delete the node, if it exists somewhere to prevent + // that there are multiple nodes with the same id; + // Thus, 'putNode' is in fact a 'move': + this.del('node', { id: e.id }); + + // 2. Insert the node, if the predecessor exists somewhere: + if (Lib.iterateNodes( + this.hierarchies, + // continue searching until found: + (nd: SpecifNode) => { return nd.id != e.predecessor }, + // insert the node after the predecessor: + (ndL: SpecifNode[]) => { + let i = indexById(ndL as Item[], e.predecessor); + if (i > -1) ndL.splice(i + 1, 0, e); + } + )) + return true; -function Project(): IProject { - // Constructor for a project containing SpecIF data. - var self: any = {}, - // loading = false, // true: data is being gathered from the server. - fileName: string; - self.init = (): void => { - // initialize/clear all variables: - self.data = new CSpecIF(); + // 3. Insert the node, if the parent exists somewhere: + if (Lib.iterateNodes( + this.hierarchies, + // continue searching until found: + (nd: SpecifNode) => { + if (nd.id == e['parent']) { + if (!Array.isArray(nd.nodes)) nd.nodes = []; + // we will not find a predecessor at this point any more, + // so insert as first element of the children: + nd.nodes.unshift(e); + return false; // stop searching + }; + return true; // continue searching + } + // no list function + )) + return true; - // loading = false; - self.exporting = false; // prevent concurrent exports - self.abortFlag = false + // 4. insert the node as first root element, otherwise: + this.hierarchies.unshift(e); + return false; + } + clear(ctg?:string) { + if (ctg) + this[standardTypes.listName.get(ctg)].length = 0 + else + for (var le of standardTypes.listName.keys()) + this[standardTypes.listName.get(le)].length = 0; + } +} +class CElement { + category: string; + listName: string; + isEqual: Function; + isCompatible: Function; + substitute: Function; + constructor(ctg: string, eqF: Function, compF: Function, subsF: Function) { + this.category = ctg; + this.listName = standardTypes.listName.get(ctg) as string; + this.isEqual = eqF; + this.isCompatible = compF; + this.substitute = subsF; + } +} +class CProject { + // Applies the project data (SpecIF data-set) to the respective data sources + // - Common Cache (for all locally known projects) + // - assigned Server(s) + // @ts-ignore - initialized by this.setMeta() + id: string; + // @ts-ignore - initialized by this.setMeta() + title: string; + // @ts-ignore - initialized by this.setMeta() + description?: string; + // @ts-ignore - initialized by this.setMeta() + generator?: string; + // @ts-ignore - initialized by this.setMeta() + generatorVersion?: string; + // @ts-ignore - initialized by this.setMeta() + createdAt?: string; + // @ts-ignore - initialized by this.setMeta() + createdBy?: string; + hierarchies: SpecifNode[] = []; // reference the specifications (aka hierarchies, outlines) of the project. + data: CCache; +/* myRole = i18n.LblRoleProjectAdmin; + cre; + upd; + del = app.title != i18n.LblReader; + locked = app.title == i18n.LblReader; + exp: boolean = true; // permission to export */ + exporting: boolean = false; // prevent concurrent exports + abortFlag: boolean = false; + types: CElement[]; + fileName: string = ""; + + constructor(spData: SpecIF, cache: CCache) { + this.setMeta(spData); + this.data = cache; + // remember the hierarchies associated with this projects - the cache holds all: + for (var i = spData.hierarchies.length - 1; i > -1; i--) { + this.hierarchies.unshift({ id: spData.hierarchies[i].id, revision: spData.hierarchies[i].revision}); + }; + // this.exp = true; + + // Create a table of types and relevant attributes: + this.types = [ + new CElement('dataType', this.equalDT, this.compatibleDT, this.substituteDT.bind(this)), + new CElement('propertyClass', this.equalPC, this.compatiblePC.bind(this), this.substitutePC.bind(this)), + new CElement('resourceClass', this.equalRC.bind(this), this.compatibleRC.bind(this), this.substituteRC.bind(this)), + new CElement('statementClass', this.equalSC, this.compatibleSC.bind(this), this.substituteSC.bind(this)) + ]; }; - self.isLoaded = (): boolean => { - return typeof (self.data.id) == 'string' && self.data.id.length > 0 + isLoaded(): boolean { + return typeof (this.id) == 'string' && this.id.length > 0; }; - - self.create = (newD: any, opts: any): JQueryDeferred => { + setMeta(spD: SpecIF): void { + this.id = spD.id; + this.title = spD.title; + this.description = spD.description; + this.generator = spD.generator; + this.generatorVersion = spD.generatorVersion; + this.createdAt = spD.createdAt; + this.createdBy = spD.createdBy; + /* this.myRole = i18n.LblRoleProjectAdmin; + this.cre = this.data.upd = this.data.del = app.title != i18n.LblReader; + this.locked = app.title == i18n.LblReader; + this.exp = true; */ + }; + getMeta(): CSpecIF { + var spD = new CSpecIF(); + spD.id = this.id; + spD.title = this.title; + spD.description = this.description; + spD.generator = this.generator; + spD.generatorVersion = this.generatorVersion; + spD.createdAt = this.createdAt; + spD.createdBy = this.createdBy; + return spD; + }; + create(newD: SpecIF, opts: any):Promise { // create a project, if there is no project with the given id, or replace a project with the same id. // (The roles/permissions and the role assignment to users are preserved, when import via ReqIF-file is made) // If there is no newD.id, it will be generated by the server. // Use jQuery instead of ECMA Promises for the time being, because of progress notification. - var sDO = $.Deferred(); + var sDO = $.Deferred(), + self = this, // make the class attributes and methods available within local function 'finalize' + pend = 0; - // Create the specified project: - let ti = newD.title, - pend: number; + this.abortFlag = false; - self.abortFlag = false; newD = new CSpecIF(newD); // transform to internal data structure // console.debug('app.cache.selectedProject.data.create',newD); - if ( newD.isValid() ) { - self.data.setMeta(newD); + if (newD.isValid()) { // Create the project // The project meta-data and each item are created as a separate document in a document database; // at the same time the cache is updated. sDO.notify(i18n.MsgLoadingTypes, 30); - pend = 8; - self.createContent('dataType', newD.dataTypes) - .then(finalize, sDO.reject); - self.createContent('propertyClass', newD.propertyClasses) - .then(finalize, sDO.reject); - self.createContent('resourceClass', newD.resourceClasses) - .then(finalize, sDO.reject); - self.createContent('statementClass', newD.statementClasses) - .then(finalize, sDO.reject); - self.createContent('file', newD.files) - .then(finalize, sDO.reject); - self.createContent('resource', newD.resources) - .then(finalize, sDO.reject); - self.createContent('statement', newD.statements) - .then(finalize, sDO.reject); - self.createContent('hierarchy', newD.hierarchies) - .then(finalize, sDO.reject); + pend = standardTypes.iterateLists( + (key,value) => { + this.createContent(key, newD[value]) + .then(finalize, sDO.reject); + } + ); + if (opts.addGlossary) { pend++; - self.createFolderWithGlossary(self.data, opts) + this.createFolderWithGlossary(opts) .then(finalize, sDO.reject); }; } else { - sDO.reject({ status: 995, statusText: i18n.lookup('MsgImportFailed', ti) }); + sDO.reject({ status: 995, statusText: i18n.lookup('MsgImportFailed', newD.title) }); }; return sDO; function finalize(): void { if (--pend < 1) { sDO.notify(i18n.MsgLoadingFiles, 100); - hookStatements(); - - if (opts.deduplicate) - self.deduplicate(); // deduplicate equal items + self.hookStatements(); + self.deduplicate(opts); // deduplicate equal items // ToDo: Update the server ! sDO.resolve() } } - }; -/* self.updateMeta = ( prj )=>{ - if( !prj ) return; - // update only the provided properties: - for( var p in prj ) self[p] = prj[p]; - // Update the meta-data (header): - // return server.project(self).update() - }; - self.read = ( prj, opts )=>{ - // Assemble the data of the project from all documents in a document database: - switch( typeof(opts) ) { - case 'boolean': - // for backward compatibility: - opts = {reload: opts, loadAllSpecs: false, loadObjects: false, loadRelations: false}; - break; - case 'object': - // normal case (as designed): - // if( typeof opts.reload!='boolean' ) opts.reload = false; - break; - default: - opts = {reload: false} - }; -// console.debug( 'cache.read', opts, self.data.id, prj ); - - var pDO = $.Deferred(); - // Read from cache in certain cases: - if( self.data.isLoaded && !opts.reload && ( !prj || prj.id==self.data.id ) ) { - // return the loaded project: - pDO.resolve( self ); - return pDO - }; - // else - return null - }; */ - class CType { - ctg: string; - list: string; - isEqual: Function; - isCompatible: Function; - substitute: Function; - constructor(ctg:string, eqF:Function, compF:Function, subsF:Function) { - this.ctg = ctg; - this.list = standardTypes.listNameOf(ctg); - this.isEqual = eqF; - this.isCompatible = compF; - this.substitute = subsF; - } - } -// Create a table of types and relevant attributes: - const types = [ - new CType('dataType', equalDT, compatibleDT, substituteDT), - new CType('propertyClass', equalPC, compatiblePC, substitutePC), - new CType('resourceClass', equalRC, compatibleRC, substituteRC), - new CType('statementClass', equalSC, compatibleSC, substituteSC) - ]; - - function equalDT(r: DataType, n: DataType): boolean { - // return true, if reference and new dataType are equal: - if( r.type!=n.type ) return false; - switch (r.type) { - case TypeEnum.XsDouble: - if( r.fractionDigits!=n.fractionDigits ) return false; - // no break - case TypeEnum.XsInteger: - return r.minInclusive == n.minInclusive && r.maxInclusive == n.maxInclusive; - case TypeEnum.XsString: - case TypeEnum.XHTML: - return r.maxLength == n.maxLength; - case TypeEnum.XsEnumeration: - // Perhaps we must also look at the title .. - // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType - if( r.values.length!=n.values.length ) return false; - // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType - for( var i=n.values.length-1; i>-1; i-- ) - // assuming that the titles/values don't matter: - // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType - if( indexById(r.values, n.values[i].id)<0 ) return false; - // the list of enumerated values *is* equal, - // finally check for the multiple flag: - return (r.multiple && n.multiple || !r.multiple && !n.multiple); - default: - return true; - }; - } - function equalPC(r: PropertyClass, n: PropertyClass):boolean { - // return true, if reference and new propertyClass are equal: - return r.dataType == n.dataType - && r.title == n.title - && (r.multiple && n.multiple || !r.multiple && !n.multiple); } - function equalRC(r: ResourceClass, n: ResourceClass): boolean { - // return true, if reference and new resourceClass are equal: - if( !r.isHeading&&n.isHeading || r.isHeading&&!n.isHeading ) return false; - return r.title==n.title - && eqL( r.propertyClasses, n.propertyClasses ) - // && eqL( r.instantiation, n.instantiation ) - // --> the instantiation setting of the reference shall prevail - } - function equalSC(r:StatementClass, n:StatementClass): boolean { - // return true, if reference and new statementClass are equal: - return r.title==n.title - && eqSCL( r.propertyClasses, n.propertyClasses ) - && eqSCL( r.subjectClasses, n.subjectClasses ) - && eqSCL( r.objectClasses, n.objectClasses ) - && eqSCL( r.instantiation, n.instantiation ); + read(): SpecIF { + // collect all items of this project from the cache containing elements of multiple projects + // (.. so far only one project, so the selection-process is pretty simple ..) + var spD: SpecIF = this.getMeta(); + for (var le of standardTypes.listName.keys()) + spD[standardTypes.listName.get(le)] = this.data.get(le, 'all'); +// console.debug('#',spD); + + return spD; + } +/* update(newD: SpecIF, opts: any) { + var uDO = $.Deferred(); + newD = new CSpecIF(newD); // transform to internal data structure + uDO.resolve(); + return uDO; + } */ + adopt(newD: SpecIF, opts?: any) { + // First check whether BPMN collaboration and process have unique ids: +// console.debug('adopt project',newD); - function eqSCL(rL:any, nL:any): boolean { -// console.debug('eqSCL',rL,nL); - // return true, if both lists have equal members, - // in this case we allow also less specified statementClasses - // (for example, when a statement is created from an Excel sheet): - if( !Array.isArray(nL) ) return true; - // no or empty lists are allowed and considerated equal: - let rArr = Array.isArray(rL) && rL.length>0, - nArr = Array.isArray(nL) && nL.length>0; - if( !rArr&&nArr - || rL.length!=nL.length ) return false; - // the sequence may differ: - for( var i=rL.length-1; i>-1; i-- ) - if( nL.indexOf( rL[i] )<0 ) return false; - return true; - } - } + var uDO = $.Deferred(), + self = this, // make the class attributes and methods available within local function 'finalize' + dta = this.data, + pend = 0; - function equalKey(r: KeyObject, n: KeyObject): boolean { - // Return true if both keys are equivalent; - // this applies if only an id is given or a key with id and revision: - return itemIdOf(r)==itemIdOf(n) - && r.revision==n.revision; - } - RE.splitNamespace = /^(\w+:|\w+\.)?(\w+)$/; - function equalR(prj: SpecIF, r:Resource, n:Resource): boolean { -// console.debug('equalR',r,n,resClassTitleOf(r,dta),resClassTitleOf(n,dta)); - // return true, if reference and new resource are equal. - // ToDo: Consider, if model-elements are only considered equal, if they have the same type, - // i.e. if a property with title CONFIG.propClassType has the same value + newD = new CSpecIF(newD); // transform to internal data structure - // Sort out most cases with minimal computing: - if (r.title != n.title || resClassTitleOf(r, prj) != resClassTitleOf(n, prj)) - return false; + // 1. Integrate the types: + // a) if different id, save new one and use it. + // (the case of different id and same content will be covered by deduplicate() at the end) + // b) if same id and same content, just use it (no action) + // c) if same id and different content, save with new id and update all references + if (newD.isValid()) { +// console.debug('adopt #1',simpleClone(self.data),simpleClone(newD)); + this.types.forEach((ty) => { + // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed + if (Array.isArray(newD[ty.listName])) { + let itmL: any[] = []; + // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed + newD[ty.listName].forEach((nT) => { + // nT is a type/class in new data + // types are compared by id: + let idx = indexById(dta[ty.listName], nT.id); + if (idx < 0) { + // a) there is no item with the same id + itmL.push(nT); + } + else { + // there is an item with the same id. + // if( !ty.isEqual( self.data[ty.listName][idx], nT) ) { + if( !ty.isCompatible(dta[ty.listName][idx], nT, {mode:"include"}) ) { + // there is an item with the same id and different content. + // c) create a new id and update all references: + // Note: According to the SpecIF schema, dataTypes may have no additional XML-attribute + // ToDo: In ReqIF an attribute named "Reqif.ForeignId" serves the same purpose as 'alterId': + let alterId = nT.id; + nT.id += '-' + simpleHash(new Date().toISOString()); + ty.substitute(newD, nT, { id: alterId }); + itmL.push(nT); + console.info("When adopting a project" + (newD.id ? " with id " + newD.id : "") + + ", a class with same id and incompatible content has been encountered: " + alterId + + "; it has been saved with a new identifier " + nT.id + "."); + }; + // b) no action + }; + }); + // @ts-ignore - newD[ty.listName] is a valid address + console.info((newD[ty.listName].length - itmL.length) + " " + ty.listName + " adopted and " + itmL.length + " added."); + pend++; + this.createContent(ty.category, itmL) + .then(finalize, uDO.reject); + }; + }); + /* ALTERNATIVE + * // 1. Integrate the types: + // a) if same id and different content, save with new id and update all references + // b) if same id and same content, just use it (no action) + // c) if different id and same content, adopt existing class and update all references + // d) if different id and different content, save new one and use it. + var uDO = $.Deferred(), + self = this, // make the class attributes and methods available within local function 'finalize' + dta = this.data, + pend = 0; + // console.debug('adopt #1',simpleClone(self.data),simpleClone(newD)); + this.types.forEach((ty) => { + // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed + if (Array.isArray(newD[ty.listName])) { + }; + }); */ +// console.debug('#2',simpleClone(dta),simpleClone(newD)); - // Here, both resources have equal titles and class-titles: + // 2. Integrate the instances: + // a) if different title or type, save new one and use it. + // b) if same title and type, just use it and update all references + if (Array.isArray(newD.resources)) { + let itmL: Resource[] = []; + newD.resources.forEach((nR: Resource) => { + // nR is a resource in the new data - // Only a genuine title will be considered truly equal, but not a default title - // being equal to the content of property CONFIG.propClassType is not considered equal - // (for example BPMN endEvents which don't have a genuine title): - let typ = valByTitle(r, CONFIG.propClassType, prj), - rgT = RE.splitNamespace.exec(typ); - // rgT[2] contains the type without namespace (works also, if there is no namespace). - return (!rgT || rgT[2] != r.title); - // Note that the content of the new resource is lost; - // this is no problem, if the new data is a BPMN model, for example, - // which usually have a title and do not carry any description. - } - function equalS(r:Statement, n:Statement): boolean { - // return true, if reference and new statement are equal: - // ToDo: Model-elements are only equal, if they have the same type, - // i.e. a property with title CONFIG.propClassType has the same value - return r['class']==n['class'] - && equalKey(r.subject,n.subject) - && equalKey(r.object,n.object); - } - function equalF(r: any, n: any): boolean { - // return true, if reference and new file are equal: - return r.id == n.id - && r.title == n.title - && r.type == n.type; - } - function eqL(rL: any[], nL: any[]): boolean { - // return true, if both lists have equal members: - // no or empty lists are allowed and considerated equal: - let rArr = Array.isArray(rL) && rL.length>0, - nArr = Array.isArray(nL) && nL.length>0; - if( !rArr&&!nArr ) return true; - if( !rArr&&nArr - || rArr&&!nArr - || rL.length!=nL.length ) return false; - // the sequence may differ: - for( var i=rL.length-1; i>-1; i-- ) - if( nL.indexOf( rL[i] )<0 ) return false; - return true; - } - function typeIsCompatible(ctg: string, refC:any, newC:any, mode?: string): xhrMessage { - // if(refC.id!=newC.id) return {status:0}; - // else: identifiers are equal: - // console.debug( 'typeIsCompatible', refC, newC ); - switch (ctg) { - case 'dataType': - // A dataType is incompatible, if an existing one has the same id and a smaller value range. - // A dataType is compatible, if an existing one has the same id and an equal or larger value range. - switch (refC.type) { - case TypeEnum.XsBoolean: - case TypeEnum.XsDateTime: - return { status: 0 }; - case TypeEnum.XHTML: - case TypeEnum.XsString: - // console.debug( refC.maxLength>newC.maxLength-1 ); - if (refC.maxLength == undefined) - return { status: 0 }; - if (newC.maxLength == undefined || refC.maxLength < newC.maxLength) - return { status: 951, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }; - return { status: 0 }; - case TypeEnum.XsDouble: - // to be compatible, the new 'fractionDigits' must be lower or equal: - if (refC.fractionDigits < newC.fractionDigits) - return { status: 952, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }; - // else: go on ... - case TypeEnum.XsInteger: - // to be compatible, the new 'maxInclusive' must be lower or equal and the new 'minInclusive' must be higher or equal: - // console.debug( refC.maxInclusivenewC.minInclusive ); - if (refC.maxInclusive < newC.maxInclusive || refC.minInclusive > newC.minInclusive) - return { status: 953, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" } - else - return { status: 0 }; - case TypeEnum.XsEnumeration: - // to be compatible, every value of the new 'enumeration' must be present in the present one: - // ToDo: Add a new enum value to an existing enum dataType. - var idx: number; - for (var v = newC.values.length - 1; v > -1; v--) { - idx = indexById(refC.values, newC.values[v].id); - // the id must be present: - if (idx < 0) - return { status: 954, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }; - // ... and the titles must be equal: - if (refC.values[idx].title != newC.values[v].title) - return { status: 955, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" } - }; - return { status: 0 }; - }; - // should never arrive here ... as every branch in every case above has a return. - throw Error("Invalid data type."); - case 'propertyClass': - return equalPC(refC, newC) ? { status: 0 } - : { status: 956, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" }; - case 'statementClass': - // To be compatible, all sourceTypes of newC must be contained in the sourceTypes of refC; - // no sourceTypes means that all resourceClasses are permissible as subject. - // ... and similarly for the targetTypes: - if (refC.sourceTypes && !newC.sourceTypes - || refC.sourceTypes && newC.sourceTypes && !containsById(refC.sourceTypes, newC.sourceTypes)) { - return { status: 961, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" } - }; - if (refC.targetTypes && !newC.targetTypes - || refC.targetTypes && newC.targetTypes && !containsById(refC.targetTypes, newC.targetTypes)) { - return { status: 962, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" } - }; - // else: so far everything is OK, but go on checking ... (no break!) - case 'resourceClass': - // A resourceClass or statementClass is incompatible, if it has an equally-named property class with a different dataType - // A resourceClass or statementClass is compatible, if all equally-named propertyClasses have the same dataType - if (!newC.propertyClasses || !newC.propertyClasses.length) - return { status: 0 }; - // else: The new type has at least one property. - if (mode == 'match' && (!refC.propertyClasses || refC.propertyClasses.length < 1)) - return { status: 963, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" }; - var idx:number, npc:PropertyClass; - for (var a = newC.propertyClasses.length - 1; a > -1; a--) { - npc = newC.propertyClasses[a]; - if (npc.id) { - // If an id exists, it must be equal to one of refC's propertyClasses: - idx = indexById(refC.propertyClasses, npc.id) - } else { - // If there is no id, the type is new and there are no referencing elements, yet. - // So it does not matter. - // But there must be a property class with the same name: - idx = indexByTitle(refC.propertyClasses, npc.title) - }; - if (idx < 0) { - // The property class in the new data is not found in the existing (reference) data: - if (mode == 'match') - // the property class is expected and thus an error is signalled: - return { status: 964, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" } - else - // cases 'extend' and 'ignore'; - // either the property will be created later on, or it will be ignored; - // we are checking only in a first pass. - continue; + // Adopt resource with the same id, title and class right away: + let eR: Resource = itemById(dta.resources, nR.id); // resource in the existing data + if (eR && this.equalR(eR, nR)) return; + + // Adopt resource, if it's class belongs to a collection of class-titles and is not excluded from deduplication. + // The folders are excluded from consolidation, because it may happen that there are + // multiple folders with the same name but different description in different locations of the hierarchy. + if (CONFIG.modelElementClasses.concat(CONFIG.diagramClasses).indexOf(resClassTitleOf(nR, newD)) > -1 + && CONFIG.excludedFromDeduplication.indexOf(valByTitle(nR, CONFIG.propClassType, newD)) < 0 + ) { + // Check for a resource with the same title: + eR = itemByTitle(dta.resources, nR.title); // resource in the existing data + // If there is an instance with the same title ... and if the types match; + // the class title reflects the role of it's instances ... + // and is less restrictive than the class ID: +// console.debug('~1',nR,eR?eR:''); + if (eR + && CONFIG.excludedFromDeduplication.indexOf(valByTitle(eR, CONFIG.propClassType, dta)) < 0 + && resClassTitleOf(nR, newD) == resClassTitleOf(eR, dta) + // && valByTitle(nR,CONFIG.propClassType,newD)==valByTitle(eR,CONFIG.propClassType,dta) + ) { +// console.debug('~2',eR,nR); + // There is an item with the same title and type, + // adopt it and update all references: + this.substituteR(newD, eR, nR, { rescueProperties: true }); + return; + } }; - // else: the property class is present; in this case and in all modes the dataTypes must be equal: - if (refC.propertyClasses[idx].dataType != npc.dataType) { - return { status: 965, statusText: "new " + ctg + " '" + newC.id + "' is incompatible" } - } - }; - return { status: 0 }; - }; - // should never arrive here ... - throw Error("Invalid category '" + ctg + "'."); - } - function compatibleDT(refC: DataType, newC: DataType): boolean { - return typeIsCompatible("dataType", refC, newC).status == 0; - } - function compatiblePC(refC: PropertyClass, newC: PropertyClass): boolean { - return typeIsCompatible("propertyClass", refC, newC).status == 0; - } - function compatibleRC(refC: ResourceClass, newC: ResourceClass, mode?: string): boolean { - return typeIsCompatible("resourceClass", refC, newC, mode).status == 0; - } - function compatibleSC(refC: StatementClass, newC: StatementClass): boolean { - return typeIsCompatible("statementClass", refC, newC).status == 0; - } - function substituteDT(prj: SpecIF,rId:string,nId:string):void { - // For all propertyClasses, substitute new by the original dataType: - substituteProp(prj.propertyClasses,'dataType',rId,nId); - } - function substitutePC(prj: SpecIF, rId: string, nId: string):void { - // For all resourceClasses, substitute new by the original propertyClass: - substituteLe(prj.resourceClasses,'propertyClasses',rId,nId); - // Also substitute the resource properties' class: - prj.resources.forEach( (res)=>{ - substituteProp(res.properties,'class',rId,nId); - }); - // The same with the statementClasses: - substituteLe(prj.statementClasses,'propertyClasses',rId,nId); - prj.statements.forEach( (sta)=>{ - substituteProp(sta.properties,'class',rId,nId) - }); - } - function substituteRC(prj: SpecIF, rId: string, nId: string):void { - // Substitute new by original resourceClass: - substituteLe(prj.statementClasses,'subjectClasses',rId,nId); - substituteLe(prj.statementClasses,'objectClasses',rId,nId); - substituteProp(prj.resources,'class',rId,nId); - } - function substituteSC(prj: SpecIF, rId: string, nId: string):void { - // Substitute new by original statementClass: - substituteProp(prj.statements, 'class', rId, nId); - } - function substituteR(prj: SpecIF, r: Resource, n: Resource, opts?: any): void { - // Substitute resource n by r in all references of n, - // where r is always an element of self.data. - // But: Rescue any property of n, if undefined for r. - // console.debug('substituteR',r,n,prj.statements); - if (opts && opts.rescueProperties) { - // Rescue any property value of n, - // if the corresponding property of the adopted resource r is undefined or empty; - // looking at the property types, which ones are in common: - n.properties.forEach((nP) => { - if (hasContent(nP.value)) { - // check whether existing resource has similar property; - // a property is similar, if it has the same title, - // where the title may be defined with the property class. - let pT = propTitleOf(nP, prj), - rP = propByTitle(r, pT, self.data); -// console.debug('substituteR 3a',nP,pT,rP,hasContent(valByTitle( r, pT, self.data ))); - if (!hasContent(valByTitle(r, pT, self.data)) - // dataTypes must be compatible: - && compatibleDT(dataTypeOf(self.data, rP['class']), dataTypeOf(prj, nP['class']))) { - // && typeIsCompatible( 'dataType', dataTypeOf(self.data,rP['class']), dataTypeOf(prj,nP['class']) ).status==0 ) { - rP.value = nP.value; + // Execution gets here, unless a substitution has taken place; + // thus add the new resource as separate instance: + + // Note that in theory, there shouldn't be any conflicting ids, but in reality there are; + // for example it has been observed with BPMN/influx which is based on bpmn.io like cawemo. + // ToDo: make it an option. + + // Check, whether the existing model has an element with the same id, + // and since it does have a different title or different type (otherwise it would have been substituted above), + // assign a new id to the new element: + if (this.duplicateId(dta, nR.id)) { + let newId = Lib.genID('R-'); + // first assign new ID to all references: + this.substituteR(newD, { id: newId } as Resource, nR); + // and then to the resource itself: + nR.id = newId }; - }; - }); - }; - // In the rare case that the ids are identical, there is no need to update the references: - if (r.id == n.id) return; +// console.debug('+ resource',nR); + itmL.push(nR) + }); + console.info((newD.resources.length - itmL.length) + " resources adopted and " + itmL.length + " added."); + pend++; + this.createContent('resource', itmL) + .then(finalize, uDO.reject); + }; +// console.debug('#3',simpleClone(dta),simpleClone(newD)); - // 1. Memorize the replaced id, if not yet listed: - if (!Array.isArray(r.alternativeIds)) r.alternativeIds = []; - cacheE(r.alternativeIds, n.id); + // 3. Create the remaining items; + // this.createContent('statement', newD.statements) could be called, + // but then the new elements would replace the existing ones. + // In case of 'adopt' the existing shall prevail! + if (Array.isArray(newD.statements)) { + let itmL: Statement[] = []; + newD.statements.forEach((nS: Statement) => { + // nR is a resource in the new data - // 2. Replace the references in all statements: - prj.statements.forEach((st: Statement) => { - if (equalKey(st.object, n)) { if (st.object.id) { st.object.id = itemIdOf(r) } else { st.object = itemIdOf(r) } }; - if (equalKey(st.subject, n)) { if (st.subject.id) { st.subject.id = itemIdOf(r) } else { st.subject = itemIdOf(r) } } - // ToDo: Is the substitution is too simple, if a key is used? - }); + // Adopt statement with the same id, title and class right away: + let eS: Statement = itemById(dta.statements, nS.id); // statement in the existing data + if (eS && this.equalS(eS, nS)) return; + // Else, create new element: + itmL.push(nS); + }); + console.info((newD.statements.length - itmL.length) + " statements adopted and " + itmL.length + " added."); + pend++; + this.createContent('statement', itmL) + .then(finalize, uDO.reject); + }; + pend++; + this.createContent('hierarchy', newD.hierarchies) + .then(finalize, uDO.reject); - // 3. Replace the references in all hierarchies: - substituteRef(prj.hierarchies, r.id, n.id); + if (Array.isArray(newD.files)) { + let itmL: any[] = []; + newD.files.forEach((nF: any) => { + // nR is a resource in the new data - // 4. Make sure all statementClasses allowing n.class also allow r.class (the class of the adopted resource): - prj.statementClasses.forEach((sC: StatementClass) => { - if (Array.isArray(sC.subjectClasses) && sC.subjectClasses.indexOf(n['class']) > -1) cacheE(sC.subjectClasses, r['class']); - if (Array.isArray(sC.objectClasses) && sC.objectClasses.indexOf(n['class']) > -1) cacheE(sC.objectClasses, r['class']); - }); - } - function substituteProp(L,propN:string,rAV:string,dAV:string):void { - // replace ids of the duplicate item by the id of the original one; - // this applies to the property 'propN' of each member of the list L: - if( Array.isArray(L) ) - L.forEach( (e)=>{ if(e[propN]==dAV) e[propN] = rAV } ); - } - function substituteLe(L,propN:string,rAV:string,dAV:string):void { - // Replace the duplicate id by the id of the original item; - // so replace dAV by rAV in the list named 'propN' - // (for example: in L[i][propN] (which is a list as well), replace dAV by rAV): - let idx:number; - if (Array.isArray(L)) - L.forEach((e) => { - // e is a resourceClass or statementClass: - if (Array.isArray(e[propN])) { - idx = e[propN].indexOf(dAV); - if (idx > -1) { - // dAV is an element of e[propN] - // - replace dAV with rAV - // - in case rAV is already member of the list, just remove dAV - if (e[propN].indexOf(rAV) > -1) - e[propN].splice(idx, 1) - else - e[propN].splice(idx, 1, rAV); - }; - }; - }); - } - function substituteRef(L,rId:string,dId:string):void { - // For all hierarchies, replace any reference to dId by rId; - // eliminate double entries in the same folder (together with the children): - iterateNodes( L, - // replace resource id: - (nd)=>{ if(nd.resource==dId) {nd.resource=rId}; return true }, - // eliminate duplicates within a folder (assuming that it will not make sense to show the same resource twice in a folder; - // for example it is avoided that the same diagram is shown twice if it has been imported twice: - (ndL)=>{ for(var i=ndL.length-1; i>0; i--) { if( indexBy(ndL.slice(0,i),'resource',ndL[i].resource)>-1 ) { ndL.splice(i,1) }}} - ); - // ToDo: Make it work, if keys are used as a reference. + // Adopt equal file right away: + let eF: any = itemById(dta.files, nF.id); // file in the existing data + if (eF && this.equalF(eF, nF)) return; + // Else, create new element: + itmL.push(nF); + }); + console.info((newD.files.length - itmL.length) + " files adopted and " + itmL.length + " added."); + pend++; + this.createContent('file', itmL) + .then(finalize, uDO.reject); + }; + } + else { + uDO.reject({ status: 995, statusText: i18n.lookup('MsgImportFailed', newD.title) }); + }; + return uDO; + + function finalize(): void { + if (--pend < 1) { +// console.debug('#4',simpleClone(dta),simpleClone(newD)); + // 4. Finally some house-keeping: + self.hookStatements(); + self.deduplicate(opts); // deduplicate equal items + // ToDo: Save changes from deduplication to the server. +// console.debug('#5',simpleClone(dta),opts); + self.createFolderWithResourcesByType(opts) + .then( + () => { + self.createFolderWithGlossary(opts) + .then(uDO.resolve, uDO.reject) + }, + uDO.reject + ); + }; + } } - function hookStatements( dta?:SpecIF ):void { - if( typeof(dta)!='object' || !dta.id ) dta = self.data; + private hookStatements(): void { + let dta = this.data; // console.debug('hookStatements',dta); // For all statements with a loose end, hook the resource // specified by title or by a property titled dcterms:identifier: - dta.statements.forEach( (st:Statement)=>{ + dta.statements.forEach((st: Statement) => { // Check every statement; // it is assumed that only one end is loose: - if( st.subjectToFind ) { + if (st.subjectToFind) { // Find the resource with a value of property titled CONFIG.propClassId: - let s, sL = itemsByVisibleId( dta.resources, st.subjectToFind ); - if( sL.length>0 ) + let s, sL = itemsByVisibleId(dta.resources, st.subjectToFind); + if (sL.length > 0) s = sL[0]; else // Find the resource with the given title: - s = itemByTitle( dta.resources, st.subjectToFind ); + s = itemByTitle(dta.resources, st.subjectToFind); // console.debug('hookStatements subject',s); - if( s ) { + if (s) { st.subject = s.id; delete st.subjectToFind; return; }; }; - if( st.objectToFind ) { + if (st.objectToFind) { // Find the resource with a value of property titled CONFIG.propClassId: - let o, oL = itemsByVisibleId( dta.resources, st.objectToFind ); - if( oL.length>0 ) + let o, oL = itemsByVisibleId(dta.resources, st.objectToFind); + if (oL.length > 0) o = oL[0]; else // Find the resource with the given title: - o = itemByTitle( dta.resources, st.objectToFind ); + o = itemByTitle(dta.resources, st.objectToFind); // console.debug('hookStatements object',o); - if( o ) { + if (o) { st.object = o.id; delete st.objectToFind; return; @@ -721,48 +654,145 @@ function Project(): IProject { }); return; - function itemsByVisibleId(L,vId:string):Resource[] { + function itemsByVisibleId(L, vId: string): Resource[] { // return a list with all elements in L having a property // containing a visible id with value vId; // should only be one resulting element: - return forAll( L, (r)=>{ - if( visibleIdOf(r)==vId ) return r; + return Lib.forAll(L, (r) => { + if (visibleIdOf(r) == vId) return r; }); } } - self.createFolderWithResourcesByType = ( dta:SpecIF, opts?:any ):Promise =>{ + private duplicateId(dta: any, id: string): boolean { + // check whether there is an item with the same id in dta. + // If so, return the item: + if (dta.id == id) return true; + for (var i in dta) { + if (Array.isArray(dta[i])) { + for (var j = dta[i].length - 1; j > -1; j--) { + if (this.duplicateId(dta[i][j], id)) return true; + }; + }; + }; + return false; + } + private deduplicate(opts?:any): void { + // Uses the cache. + // ToDo: update the server. + if (!opts || !opts.deduplicate) return; + + let dta = this.data, + n: number, r: number, nR: Resource, rR: Resource; +// console.debug('deduplicate',simpleClone(dta)); + + // 1. Deduplicate equal types having different ids; + // the first of a equivalent pair in the list is considered the reference or original ... and stays, + // whereas the second in a pair is removed. + this.types.forEach((ty) => { + let lst = dta[ty.listName]; + // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed + if (Array.isArray(lst)) + // skip last loop, as no duplicates can be found: + // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed + for (n = lst.length - 1; n > 0; n--) { + for (r = 0; r < n; r++) { +// console.debug( '##', lst[r],lst[n],ty.isEqual(lst[r],lst[n]) ); + // Do it for all types: + // @ts-ignore - this addressing is perfectly fine and the list names are defined. + if (ty.isEqual(lst[r], lst[n])) { + // Are equal, so substitute it's ids by the original item: + // @ts-ignore - this addressing is perfectly fine and the list names are defined. + ty.substitute(dta, lst[r], lst[n]); + // @ts-ignore - this addressing with a string is perfectly supported + console.info(ty.category + " with id=" + lst[n].id + " and title=" + lst[n].title + " has been removed because it is a duplicate of id=" + lst[r].id); + // ... and remove the duplicate item: + // @ts-ignore - this addressing is perfectly fine and the list names are defined. + lst.splice(n, 1); + // skip the remaining iterations of the inner loop: + break; + }; + }; + }; + }); +// console.debug( 'deduplicate 1', simpleClone(dta) ); + + // 2. Remove duplicate resources: + // skip last loop, as no duplicates can be found: + for (n = dta.resources.length - 1; n > 0; n--) { + for (r = 0; r < n; r++) { + // Do it for all model-elements and diagrams, + // but exclude process gateways and generated events for optional branches: + nR = dta.resources[n]; + rR = dta.resources[r]; +// console.debug( 'duplicate resource ?', rR, nR ); + if (CONFIG.modelElementClasses.concat(CONFIG.diagramClasses).indexOf(resClassTitleOf(rR, dta)) > -1 + && this.equalR(rR, nR) + && CONFIG.excludedFromDeduplication.indexOf(valByTitle(nR, CONFIG.propClassType, dta)) < 0 + && CONFIG.excludedFromDeduplication.indexOf(valByTitle(rR, CONFIG.propClassType, dta)) < 0 + ) { + // Are equal, so remove the duplicate resource: +// console.debug( 'duplicate resource', rR, nR, valByTitle( nR, CONFIG.propClassType, dta ) ); + this.substituteR(dta, rR, nR, { rescueProperties: true }); + console.info("Resource with id=" + nR.id + " and title=" + nR.title + " has been removed because it is a duplicate of id=" + rR.id); + dta.resources.splice(n, 1); + // skip the remaining iterations of the inner loop: + break; + }; + }; + }; +// console.debug( 'deduplicate 2', simpleClone(dta) ); + + // 3. Remove duplicate statements: + // skip last loop, as no duplicates can be found: + for (n = dta.statements.length - 1; n > 0; n--) { + for (r = 0; r < n; r++) { + // Do it for all statements: + if (this.equalS(dta.statements[r], dta.statements[n])) { + // Are equal, so remove the duplicate statement: + // @ts-ignore - the elements are defined + console.info("Statement with id=" + dta.statements[n].id + " and class=" + dta.statements[n]['class'] + " has been removed because it is a duplicate of id=" + dta.statements[r].id); + dta.statements.splice(n, 1); + // skip the remaining iterations of the inner loop: + break; + }; + }; + }; +// console.debug( 'deduplicate 3', simpleClone(dta) ); + // return undefined + } + private createFolderWithResourcesByType(opts: any): Promise { // Collect all business processes, requirements etc according to 'resourcesToCollect': + let dta = this.data; const resourcesToCollect = [ - { type: CONFIG.resClassProcess, flag:"collectProcesses", folder: CONFIG.resClassProcesses, folderNamePrefix:"FolderProcesses-"} + { type: CONFIG.resClassProcess, flag: "collectProcesses", folder: CONFIG.resClassProcesses, folderNamePrefix: "FolderProcesses-" } ]; - var r:Instance = itemById( dta.resources, dta.hierarchies[0].resource ), - rC:ResourceClass = itemById( dta.resourceClasses, r['class'] ), - prp:Property = itemByTitle( r.properties, CONFIG.propClassType ), + var r: Instance = itemById(dta.resources, dta.hierarchies[0].resource), + rC: ResourceClass = itemById(dta.resourceClasses, r['class']), + prp: Property = itemByTitle(r.properties, CONFIG.propClassType), // the type of the hierarchy root can be specified by a property titled CONFIG.propClassType // or by the title of the resourceClass: - singleHierarchyRoot = dta.hierarchies.length==1 - && (prp && CONFIG.hierarchyRoots.indexOf(prp.value)>-1 - || rC && CONFIG.hierarchyRoots.indexOf(rC.title)>-1); + singleHierarchyRoot = dta.hierarchies.length == 1 + && (prp && CONFIG.hierarchyRoots.indexOf(prp.value) > -1 + || rC && CONFIG.hierarchyRoots.indexOf(rC.title) > -1); return new Promise( - (resolve,reject)=>{ + (resolve, reject) => { if (typeof (opts) != 'object') { resolve(); return; }; - if( typeof(dta)!='object' || !dta.id ) dta = self.data; - let apx = simpleHash(dta.id), + let apx = simpleHash(this.id), tim = new Date().toISOString(); - function resDoesNotExist(rL:any[],res:Resource):boolean { - for (var i = rL.length - 1; i > -1; i--) - if (rL[i].r.id == res.id) return false; - return true; - } + function resDoesNotExist(rL: any[], res: Resource): boolean { + for (var i = rL.length - 1; i > -1; i--) + if (rL[i].r.id == res.id) return false; + return true; + } resourcesToCollect.forEach( - ( r2c )=>{ + (r2c) => { // console.debug('rc2c',r2c,opts); - if( !opts[r2c.flag] ) { resolve(); return; }; + if (!opts[r2c.flag]) { resolve(); return; }; // Assuming that the folder objects for the respective folder are available // 1 Find all resp. folders (e.g. process folder): @@ -771,88 +801,90 @@ function Project(): IProject { res: Resource, pV; // console.debug('createFolderWithResourcesByType',dta.hierarchies,opts); - iterateNodes( dta.hierarchies, - (nd:SpecifNode)=>{ - // get the referenced resource: - res = itemById( dta.resources, nd.resource ); - // find the property defining the type: - pV = valByTitle(res,CONFIG.propClassType,dta); - // collect all nodes to delete, there should be only one: - if( pV==r2c.folder ) { - delL.push( nd ); - }; - // collect all elements for the new folder, - // but avoid duplicate entries: - if (pV == r2c.type && resDoesNotExist(creL,res)) { - creL.push( {n:nd,r:res} ); - }; - return true; // continue always to the end - } + Lib.iterateNodes( + dta.get("hierarchy", "all"), + (nd: SpecifNode) => { + // get the referenced resource: + res = dta.get("resource", nd.resource)[0]; + // find the property defining the type: + pV = valByTitle(res, CONFIG.propClassType, dta); + // collect all nodes to delete, there should be only one: + if (pV == r2c.folder) { + delL.push(nd); + }; + // collect all elements for the new folder, + // but avoid duplicate entries: + if (pV == r2c.type && resDoesNotExist(creL, res)) { + creL.push({ n: nd, r: res }); + }; + return true; // continue always to the end + } ); // console.debug('createFolderWithResourcesByType',delL,creL); // 2. Delete any existing folders: // (Alternative: Keep folder and delete only the children.) - self.deleteContent( 'node', delL ) - .then( - ()=>{ - // Create a folder with all respective objects (e.g. diagrams): - if( creL.length>0 ) { - // 3. Sort the list alphabetically by the resources' title: - sortBy( creL, (el:any)=>{ return el.r.title } ); - - // 4. Create a new combined folder: - let newD = { - id: 'Create ' + r2c.type + ' ' + new Date().toISOString(), - $schema: 'https://specif.de/v1.0/schema.json', - dataTypes: [ - standardTypes.get('dataType',"DT-ShortString"), - standardTypes.get('dataType',"DT-Text") - ], - propertyClasses: [ - standardTypes.get('propertyClass',"PC-Name"), - standardTypes.get('propertyClass',"PC-Description"), - standardTypes.get('propertyClass',"PC-Type") - ], - resourceClasses: [ - standardTypes.get('resourceClass',"RC-Folder") - ], - resources: Folder( r2c.folderNamePrefix+apx, CONFIG.resClassProcesses ), - hierarchies: [] - }; - // use the update function to eliminate duplicate types: - self.update( newD, {mode:'adopt'} ) - .done( ()=>{ - // Finally create the node referencing the folder to create: - let nd: INodeWithPosition = { - id: "H"+r2c.folderNamePrefix+apx, - resource: r2c.folderNamePrefix+apx, - // re-use the nodes with their references to the resources: - nodes: forAll( creL, (pr)=>{ return pr.n; } ), - changedAt: tim + this.deleteContent('node', delL) + .then( + () => { + // Create a folder with all respective objects (e.g. diagrams): + if (creL.length > 0) { + // 3. Sort the list alphabetically by the resources' title: + Lib.sortBy(creL, (el: any) => { return el.r.title }); + + // 4. Create a new combined folder: + let newD = { + id: 'Create ' + r2c.type + ' ' + new Date().toISOString(), + $schema: 'https://specif.de/v1.0/schema.json', + dataTypes: [ + standardTypes.get('dataType', "DT-ShortString"), + standardTypes.get('dataType', "DT-Text") + ], + propertyClasses: [ + standardTypes.get('propertyClass', "PC-Name"), + standardTypes.get('propertyClass', "PC-Description"), + standardTypes.get('propertyClass', "PC-Type") + ], + resourceClasses: [ + standardTypes.get('resourceClass', "RC-Folder") + ], + resources: Folder(r2c.folderNamePrefix + apx, CONFIG.resClassProcesses), + hierarchies: [] }; - // Insert the hierarchy node as first element of a hierarchy root - // - if it is present and - // - if there is only one hierarchy root - // or as first element at root level, otherwise: - if( singleHierarchyRoot ) - nd.parent = dta.hierarchies[0].id; - self.createContent( 'node', nd ) - .then( resolve, reject ); - }) - .fail( reject ); - } else { - resolve(); - }; - }, - reject - ); + // use the update function to eliminate duplicate types: + this.adopt(newD) + .done(() => { + // Finally create the node referencing the folder to create: + let nd: INodeWithPosition = { + id: "H" + r2c.folderNamePrefix + apx, + resource: r2c.folderNamePrefix + apx, + // re-use the nodes with their references to the resources: + nodes: Lib.forAll(creL, (pr) => { return pr.n; }), + changedAt: tim + }; + // Insert the hierarchy node as first element of a hierarchy root + // - if it is present and + // - if there is only one hierarchy root + // or as first element at root level, otherwise: + if (singleHierarchyRoot) + nd.parent = dta.hierarchies[0].id; + this.createContent('node', nd) + .then(resolve, reject); + }) + .fail(reject); + } + else { + resolve(); + }; + }, + reject + ); } ); return; - function Folder(fId:string,ti:string):Resource[] { - var fL:Resource[] = [{ + function Folder(fId: string, ti: string): Resource[] { + var fL: Resource[] = [{ id: fId, class: "RC-Folder", title: ti, @@ -867,12 +899,12 @@ function Project(): IProject { } ) }; - self.createFolderWithGlossary = ( dta:SpecIF, opts:any ):Promise =>{ + private createFolderWithGlossary(opts: any): Promise { +// console.debug('createFolderWithGlossary'); + let dta = this.data; return new Promise( - (resolve,reject)=>{ + (resolve, reject) => { if (typeof (opts) != 'object' || !opts.addGlossary) { resolve(); return; }; - if( typeof(dta)!='object' || !dta.id ) dta = self.data; - // Assumes that the folder objects for the glossary are available // 1. Delete any existing glossaries // 1.1 Find all Glossary folders: @@ -880,101 +912,103 @@ function Project(): IProject { diagramL: Resource[] = [], res: Resource, pV, - apx = simpleHash(self.data.id), + apx = simpleHash(this.id), tim = new Date().toISOString(); -// console.debug('createFolderWithGlossary',dta.hierarchies); - iterateNodes( dta.hierarchies, - (nd: SpecifNode):boolean =>{ - // get the referenced resource: - res = itemById( dta.resources, nd.resource ); - // check, whether it is a glossary: - pV = valByTitle(res,CONFIG.propClassType,dta); - // collect all items to delete, there should be only one: - if( pV==CONFIG.resClassGlossary ) { - delL.push( nd ) - }; - // collect all diagrams which are referenced in the hierarchy - // for inclusion in the new folders: - if( isDiagram( res ) ) { - diagramL.push( res ) - }; - return true // continue always to the end - } +// console.debug('createFolderWithGlossary',this.hierarchies); + Lib.iterateNodes( + dta.get("hierarchy","all"), + (nd: SpecifNode): boolean => { + // get the referenced resource: + res = dta.get("resource", nd.resource)[0]; + // check, whether it is a glossary: + pV = valByTitle(res, CONFIG.propClassType, this.data); + // collect all items to delete, there should be only one: + if (pV == CONFIG.resClassGlossary) { + delL.push(nd) + }; + // collect all diagrams which are referenced in the hierarchy + // for inclusion in the new folders: + if (isDiagram(res)) { + diagramL.push(res) + }; + return true // continue always to the end + } ); // 1.2 Delete now: // console.debug('createFolderWithGlossary',delL,diagramL); - self.deleteContent( 'node', delL ) - .then( - ()=>{ - // 2. Create a new combined glossary: - if( diagramL.length>0 ) { - let newD = { - id: 'Create Glossary ' + new Date().toISOString(), - $schema: 'https://specif.de/v1.0/schema.json', - dataTypes: [ - standardTypes.get('dataType',"DT-ShortString"), - standardTypes.get('dataType',"DT-Text") - ], - propertyClasses: [ - standardTypes.get('propertyClass',"PC-Name"), - standardTypes.get('propertyClass',"PC-Description"), - standardTypes.get('propertyClass',"PC-Type") - ], - resourceClasses: [ - standardTypes.get('resourceClass',"RC-Folder") - ], - resources: Folders(), - hierarchies: NodeList(self.data.resources) - }; -// console.debug('glossary',newD); - // use the update function to eliminate duplicate types; - // 'opts.addGlossary' must not be true to avoid an infinite loop: - self.update( newD, {mode:'adopt'} ) - .done( resolve ) - .fail( reject ); - } else { - resolve(); - } - }, - reject - ); + this.deleteContent('node', delL) + .then( + () => { + // 2. Create a new combined glossary: + if (diagramL.length > 0) { + let newD = { + id: 'Create Glossary ' + new Date().toISOString(), + $schema: 'https://specif.de/v1.0/schema.json', + dataTypes: [ + standardTypes.get('dataType', "DT-ShortString"), + standardTypes.get('dataType', "DT-Text") + ], + propertyClasses: [ + standardTypes.get('propertyClass', "PC-Name"), + standardTypes.get('propertyClass', "PC-Description"), + standardTypes.get('propertyClass', "PC-Type") + ], + resourceClasses: [ + standardTypes.get('resourceClass', "RC-Folder") + ], + resources: Folders(), + hierarchies: NodeList(this.data.resources) + }; +// console.debug('glossary',newD); + // use the update function to eliminate duplicate types; + // 'opts.addGlossary' must not be true to avoid an infinite loop: + this.adopt(newD) + .done(resolve) + .fail(reject); + } else { + resolve(); + } + }, + reject + ); return; - function isDiagram( r:Resource ):boolean { + function isDiagram(r: Resource): boolean { // a resource is a diagram, if it's type has a title 'SpecIF:Diagram': // .. or if it has a property dcterms:type with value 'SpecIF:Diagram': // .. or if it has at least one statement with title 'SpecIF:shows': - return resClassTitleOf( r, dta )==CONFIG.resClassDiagram - || valByTitle(r,CONFIG.propClassType,dta)==CONFIG.resClassDiagram - || dta.statements.filter( - (sta)=>{ - return staClassTitleOf( sta, dta )==CONFIG.staClassShows && sta.subject==r.id - } - ).length>0; + return resClassTitleOf(r, dta) == CONFIG.resClassDiagram + || valByTitle(r, CONFIG.propClassType, dta) == CONFIG.resClassDiagram + || dta.get("statement","all").filter( + (sta) => { + return staClassTitleOf(sta) == CONFIG.staClassShows && sta.subject == r.id + } + ).length > 0; } /* function extractByType(fn) { - var L=[], el; - iterateNodes( dta.hierarchies, - (nd)=>{ - el = fn(nd); - if( el ) { Array.isArray(el)? L.concat(el) : L.push( el ) }; - return true // continue always to the end - } - ); - return L; - } - function extractDiagrams() { - return extractByType( - (nd)=>{ - // get the referenced resource: - var res = itemById( dta.resources, nd.resource ); - if( isDiagram( res ) ) return res; - } - ); - } */ - function Folders():Resource[] { + var L=[], el; + Lib.iterateNodes( + self.hierarchies, + (nd)=>{ + el = fn(nd); + if( el ) { Array.isArray(el)? L.concat(el) : L.push( el ) }; + return true // continue always to the end + } + ); + return L; + } + function extractDiagrams() { + return extractByType( + (nd)=>{ + // get the referenced resource: + var res = itemById( self.resources, nd.resource ); + if( isDiagram( res ) ) return res; + } + ); + } */ + function Folders(): Resource[] { // Create the resources for folder and subfolders of the glossary: - var fL:Resource[] = [{ + var fL: Resource[] = [{ id: "FolderGlossary-" + apx, class: "RC-Folder", title: CONFIG.resClassGlossary, @@ -985,27 +1019,27 @@ function Project(): IProject { changedAt: tim }]; // Create a folder resource for every model-element type: - CONFIG.modelElementClasses.forEach( (mEl:string)=>{ + CONFIG.modelElementClasses.forEach((mEl: string) => { fL.push({ id: "Folder-" + mEl.jsIdOf() + "-" + apx, class: "RC-Folder", - title: mEl+'s', // just adding the 's' is an ugly quickfix ... that works for now. + title: mEl + 's', // just adding the 's' is an ugly quickfix ... that works for now. properties: [], changedAt: tim }); }); return fL; } - function NodeList(resources:Resource[]):SpecifNode[] { + function NodeList(resources: Resource[]): SpecifNode[] { // a. Add the folders: - let gl:SpecifNode = { - id: "H-FolderGlossary-" + apx, - resource: "FolderGlossary-" + apx, - nodes: [], - changedAt: tim + let gl: SpecifNode = { + id: "H-FolderGlossary-" + apx, + resource: "FolderGlossary-" + apx, + nodes: [], + changedAt: tim }; // Create a hierarchy node for each folder per model-element type - CONFIG.modelElementClasses.forEach( function (mEl:string) { + CONFIG.modelElementClasses.forEach(function (mEl: string) { gl.nodes.push({ id: "N-Folder-" + mEl.jsIdOf() + "-" + apx, resource: "Folder-" + mEl.jsIdOf() + "-" + apx, @@ -1016,40 +1050,40 @@ function Project(): IProject { // Create a list tL of collections per model-element type; // assuming that type adoption/deduplication is not always successful // and that there may be multiple resourceClasses per model-element type: - let idx:number, - tL = forAll( CONFIG.modelElementClasses, ()=>{ return [] } ); + let idx: number, + tL = Lib.forAll(CONFIG.modelElementClasses, () => { return [] }); // Each collection carries the ids of resourceClasses for the given model-element type: - self.data.resourceClasses.forEach( - (rC:ResourceClass)=>{ + dta.get("resourceClass","all").forEach( + (rC: ResourceClass) => { idx = CONFIG.modelElementClasses.indexOf(rC.title); - if( idx>-1 ) tL[idx].push(rC.id); + if (idx > -1) tL[idx].push(rC.id); } ); // console.debug('gl tL',gl,tL); // b. list all statements typed SpecIF:shows of diagrams found in the hierarchy: - let staL = dta.statements.filter( - (s)=>{ return staClassTitleOf( s, dta )==CONFIG.staClassShows && indexById( diagramL, s.subject )>-1; } - ); + let staL = dta.get("statement", "all").filter( + (s) => { return staClassTitleOf(s) == CONFIG.staClassShows && indexById(diagramL, s.subject) > -1; } + ); // console.debug('gl tL dL',gl,tL,staL); // c. Add model-elements by class to the respective folders. // In case of model-elements the resource class is distinctive; // the title of the resource class indicates the model-element type. // List only resources which are shown on a referenced diagram: - let resL = resources.filter( (r)=>{ return indexBy( staL, 'object', r.id )>-1 } ); + let resL = resources.filter((r) => { return indexBy(staL, 'object', r.id) > -1 }); // in alphanumeric order: - sortByTitle( resL ); + Lib.sortByTitle(resL); // ToDo: consider to sort by the title property via elementTitleOf() // Categorize resources: resL.forEach( - (r:Resource):void =>{ + (r: Resource): void => { // ... using the collections per fundamental model-element type: - for( idx=tL.length-1;idx>-1;idx-- ) { - if( tL[idx].indexOf( r['class'] )>-1 ) break; + for (idx = tL.length - 1; idx > -1; idx--) { + if (tL[idx].indexOf(r['class']) > -1) break; }; - if( idx>-1 ) + if (idx > -1) gl.nodes[idx].nodes.push({ // Create new hierarchy node with reference to the resource: // ID should be the same when the glossary generated multiple times, @@ -1064,1407 +1098,1555 @@ function Project(): IProject { } } ) - }; - self.deduplicate = ( dta?:SpecIF ):void =>{ - // Uses the cache. - // ToDo: update the server. - if( typeof(dta)!='object' || !dta.id ) dta = self.data; -// console.debug('deduplicate',simpleClone(dta)); - let n:number,r:number,nR:Resource,rR:Resource; + } + createResource(rC: ResourceClass): Promise { + // Create an empty form (resource instance) for the resource class rC: + // see https://codeburst.io/a-simple-guide-to-es6-promises-d71bacd2e13a + // and https://javascript.info/promise-chaining + return new Promise( + (resolve, reject) => { + // Get the class's permissions. So far, it's property permissions are not loaded ... + var res: Resource; - // 1. Deduplicate equal types having different ids; - // the first of a equivalent pair in the list is considered the reference or original ... and stays, - // whereas the second in a pair is removed. - types.forEach( (ty)=>{ - // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed - if( Array.isArray(dta[ty.list]) ) - // skip last loop, as no duplicates can be found: - // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed - for( n=dta[ty.list].length-1; n>0; n-- ) { - for( r=0; r { + /* // ToDo: + if (ctg == "hierarchy" && item == "all") + // Return only the hierarchies of this project: + item = this.hierarchies; */ + resolve(this.data.get(ctg, item)); + }, opts.timelag); }; - }; - }; - return false; + } + ); } - // var updateModes = ["adopt","match","extend","ignore"]; - self.update = (newD, opts:any): JQueryDeferred => { -// console.debug('update',newD,opts); - // Use jQuery instead of ECMA Promises for the time being, because of progress notification. - var uDO = $.Deferred(); - - newD = new CSpecIF(newD); // transform to internal data structure - - switch( opts.mode ) { - case 'update': - // updateWithLastChanged( newD, opts ); - // break; - case 'adopt': - adopt(newD, opts); - break; - default: - uDO.reject({status:999,statusText:'Invalid update mode specified'}); - }; - return uDO; - - // -------------------------------- - // The processing per mode: - function adopt(nD: SpecIF, opts:any):void { - // First check whether BPMN collaboration and process have unique ids: -// console.debug('adopt project',nD); - // ToDo: The new collaboration id gets lost, so far! + updateContent (ctg: string, item: Item[] | Item): Promise { + // ctg is a member of [resource, statement, hierarchy], 'null' is returned in all other cases. + function updateCh(itm: Item): void { + itm.changedAt = new Date().toISOString(); + itm.changedBy = app.me.userName; + } - // 1. Integrate the types: - // a) if different id, save new one and use it. - // b) if same id and same content, just use it (no action) - // c) if same id and different content, save with new id and update all references - let pend = 0; -// console.debug('adopt #1',simpleClone(self.data),simpleClone(nD)); - types.forEach( (ty)=>{ - // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed - if( Array.isArray(nD[ty.list]) ) { - let itmL:any[] = []; - // @ts-ignore - dta is defined in all cases and the addressing using a string is allowed - nD[ty.list].forEach( (nT)=>{ - // nT is a type/class in new data - // types are compared by id: - let idx = indexById( self.data[ty.list], nT.id ); - if( idx<0 ) { - // a) there is no item with the same id - itmL.push( nT ); - } else { - // there is an item with the same id. - // if( !ty.isEqual( self.data[ty.list][idx], nT) ) { - if (!ty.isCompatible(self.data[ty.list][idx], nT)) { - // there is an item with the same id and different content. - // c) create a new id and update all references: - // Note: According to the SpecIF schema, dataTypes may have no additional XML-attribute - // ToDo: In ReqIF an attribute named "Reqif.ForeignId" serves the same purpose as 'alterId': - let alterId = nT.id; - nT.id += '-' + simpleHash(new Date().toISOString()); - ty.substitute( nD, nT.id, alterId ); - itmL.push(nT); - console.info("When importing a project"+(nD.id? " with id "+nD.id : "")+" using mode " + opts.mode + - ", a class with same id and incompatible content has been encountered: " + alterId + - "; it has been saved with a new identifier "+nT.id+"."); - }; - // b) no action + return new Promise( + (resolve) => { + switch (ctg) { + case 'node': + throw Error("Nodes can only be created or deleted"); + // case 'resource': + // case 'statement': + // case 'hierarchy': + // no break + default: +// console.debug('updateContent - cache', ctg ); + if (Array.isArray(item)) + item.forEach(updateCh) + else + updateCh(item); + this.data.put(ctg, item) + }; + resolve(); + } + ); + } + deleteContent(ctg: string, item: Item): Promise { + // ctg is a member of [dataType, resourceClass, statementClass, propertyClass, resource, statement, hierarchy] +/* function isInUse( ctg, itm ) { + function dTIsInUse( L, dT ) { + let i=null; + for( var e=L.length-1;e>-1;e-- ) { + i = L[e].propertyClasses?indexBy(L[e].propertyClasses,'dataType',dT.id):-1; +// console.debug('dTIsInUse',dT,L,e,i); + if( i>-1 ) return true }; - }); - // @ts-ignore - nD[ty.list] is a valid address - console.info((nD[ty.list].length - itmL.length)+" "+ty.list+" adopted and "+itmL.length+" added."); - pend++; - self.createContent( ty.ctg, itmL ) - .then( finalize, uDO.reject ); + return false + } + function aCIsInUse( ctg, sT ) { + let c = ctg.substr(0,ctg.length-4), // xyzType --> xyz, xyzClass ?? + L = cacheOf(c), + i = indexBy(L,ctg,sT.id); +// console.debug('aCIsInUse',sT,c,L,i); + // ToDo: In project.html, the resource cache is empty, but the resourceClass may be in use, anyways. + // Similarly with statements. + return ( i>-1 ) + } + function pCIsInUse( L, pT ) { + if( L==undefined ) return false; // can't be in use, if the list is not (yet) defined/present. + let i=null; + // ToDo: In project.html, the resource cache is empty, but the property class may be in use, anyways. + // Also a deleted resource may have used the propertyClass. + // As it stores only the newest types, the ReqIF Server will refuse to delete the type. + // In case of PouchDB, all revisions of classes/types are stored, so it is sufficient to check whether there are currently some elements using the type. + // Similarly with statements. + for( var e=L.length-1;e>-1;e-- ) { + i = L[e].properties?indexBy(L[e].properties,'class',pT.id):-1; +// console.debug('pCIsInUse property class',pT,L,e,i); + if( i>-1 ) return true + }; + return false + } +// console.debug('isInUse',ctg,item); + switch( ctg ) { + case 'dataType': return dTIsInUse(self.data.allClasses,itm); + case 'resourceClass': + case 'statementClass': return aCIsInUse(ctg,itm); + case 'class': return pCIsInUse(self.data.resources,itm) + || pCIsInUse(self.data.hierarchies,itm) + || pCIsInUse(self.data.statements,itm); }; - }); -// console.debug('#2',simpleClone(self.data),simpleClone(nD)); - - // 2. Integrate the instances: - // a) if different title or type, save new one and use it. - // b) if same title and type, just use it and update all references - if (Array.isArray(nD.resources)) { - let itmL:Resource[] = []; - nD.resources.forEach((nR: Resource) => { - // nR is a resource in the new data - - // Adopt resource with the same id, title and class right away: - let eR: Resource = itemById(self.data.resources, nR.id); // resource in the existing data - if (eR && equalR(self.data, eR, nR)) return; + return false + } */ - // Adopt resource, if it's class belongs to a collection of class-titles and is not excluded from deduplication. - // The folders are excluded from consolidation, because it may happen that there are - // multiple folders with the same name but different description in different locations of the hierarchy. - if (CONFIG.modelElementClasses.concat(CONFIG.diagramClasses).indexOf(resClassTitleOf(nR, nD)) > -1 - && CONFIG.excludedFromDeduplication.indexOf(valByTitle(nR, CONFIG.propClassType, nD)) < 0 - ) { - // Check for a resource with the same title: - eR = itemByTitle(self.data.resources, nR.title); // resource in the existing data - // If there is an instance with the same title ... and if the types match; - // the class title reflects the role of it's instances ... - // and is less restrictive than the class ID: - // console.debug('~1',nR,eR?eR:''); - if (eR - && CONFIG.excludedFromDeduplication.indexOf(valByTitle(eR, CONFIG.propClassType, self.data)) < 0 - && resClassTitleOf(nR, nD) == resClassTitleOf(eR, self.data) - // && valByTitle(nR,CONFIG.propClassType,nD)==valByTitle(eR,CONFIG.propClassType,self.data) - ) { - // console.debug('~2',eR,nR); - // There is an item with the same title and type, - // adopt it and update all references: - substituteR(nD, eR, nR, { rescueProperties: true }); +// console.debug('deleteContent',ctg,item); + return new Promise( + (resolve, reject) => { + // Do not delete types which are in use; + switch (ctg) { + /* case 'class': + case 'dataType': + case 'resourceClass': + case 'statementClass': + if( Array.isArray(item) ) return null; // not yet supported + if( isInUse(ctg,item) ) { + reject({status:972, statusText:i18n.Err400TypeIsInUse}); return; + }; + // no break; */ + case "resource": + case "statement": + case "node": +// console.debug('deleteContent',ctg,item); + if (this.data.del(ctg, item)) + break; + reject({ status: 999, statusText: ctg + ' ' + item.id + ' not found and thus not deleted.' }); + return; + default: + reject({ status: 999, statusText: 'Category ' + ctg + ' is unknown; item ' + item.id + ' could not be deleted.' }); + return; + }; + resolve(); + } + ); + }; + readStatementsOf(res: Resource, opts?: any): Promise { + // Get the statements of a resource ... there are 2 use-cases: + // - All statements between resources appearing in a hierarchy shall be shown for navigation; + // it is possible that a resource is deleted (from all hierarchies), but not it's statements. + // --> set 'showComments' to false + // - All comments referring to the selected resource shall be shown; + // the resource is found in the cache, but the comment is not. + // --> set 'showComments' to true + // - It is assumed that the hierarchies contain only model-elements shown on a visible diagram, + // so only stetements are returned only for visible resources. + // - In addirion, only statements are returned which are shown on a visible diagram. + // (perhaps both checks are not necessary, as visible statements only referto vosible resources ...) + + if (typeof (opts) != 'object') opts = {}; + let sCL: StatementClass[]; + return new Promise( + (resolve, reject) => { + this.readContent('statementClass', 'all') + .then( + (sCs: StatementClass[]) => { + sCL = sCs; + return this.readContent('statement', 'all'); } - }; + ) + .then( + (sL: Statement[]) => { + // make a list of shows statements for all diagrams shown in the hierarchy: + let showsL = sL.filter((s) => { return staClassTitleOf(s) == CONFIG.staClassShows && isReferencedByHierarchy(Lib.itemIdOf(s.subject)) }); + // filter all statements involving res as subject or object: + resolve( + sL.filter( + (s) => { + let sC: StatementClass = itemById(sCL, s['class'] as string); + return (res.id == Lib.itemIdOf(s.subject) || res.id == Lib.itemIdOf(s.object)) + // statement must be visible on a diagram referenced in a hierarchy + // or be a shows statement itself. + // ToDo: - Some Archimate relations are implicit (not shown on a diagram) and are unduly suppressed, here) + && (opts.dontCheckStatementVisibility + // Accept manually created relations (including those imported via Excel): + || !sC.instantiation || sC.instantiation.indexOf(Instantiation.User) > -1 + || indexBy(showsL, "object", s.id) > -1 + || titleOf(sC) == CONFIG.staClassShows) + // AND fulfill certain conditions: + && ( + // related subject and object must be referenced in the tree to be navigable, + // also, the statement must not be declared 'hidden': + !opts.showComments + // cheap tests first: + && titleOf(sC) != CONFIG.staClassCommentRefersTo + && CONFIG.hiddenStatements.indexOf(s.title) < 0 + && isReferencedByHierarchy(Lib.itemIdOf(s.subject)) + && isReferencedByHierarchy(Lib.itemIdOf(s.object)) + // In case of a comment, the comment itself is not referenced in the tree: + || opts.showComments + && titleOf(sC) == CONFIG.staClassCommentRefersTo + && isReferencedByHierarchy(Lib.itemIdOf(s.object)) + ) + } + ) + ); + } + ) + .catch(reject); + } + ); + } + // Select format and options with a modal dialog, then export the data: + private chooseExportOptions(fmt) { + const exportOptionsClicked = 'app.cache.selectedProject.exportOptionsClicked()'; + var pnl = '
' + // + "

"+i18n.LblOptions+"

" + // add 'zero width space' (​) to make the label = div-id unique: + + textField('​' + i18n.LblProjectName, this.title, { typ: 'line', handle: exportOptionsClicked }) + + textField('​' + i18n.LblFileName, this.title, { typ: 'line', handle: exportOptionsClicked }); + switch (fmt) { + case 'epub': + case 'oxml': + pnl += checkboxField( + // i18n.LblOptions, + i18n.modelElements, + [ + { title: i18n.withOtherProperties, id: 'withOtherProperties', checked: false }, + { title: i18n.withStatements, id: 'withStatements', checked: false } + ], + { handle: exportOptionsClicked } + ); + }; + pnl += '
'; + // console.debug('chooseExportOptions',fmt,pnl); + return pnl; + } + exportFormatClicked(): void { + // Display options depending on selected format: + // In case of ReqIF OOXML and ePub, let the user choose the language, if there are more than one: + document.getElementById("expOptions").innerHTML = this.chooseExportOptions(radioValue(i18n.LblFormat)); - // Execution gets here, unless a substitution has taken place; - // thus add the new resource as separate instance: +// console.debug('exportFormatClicked',radioValue( i18n.LblFormat )); + } + exportOptionsClicked(): void { + // Obtain selected options: + // add 'zero width space' (​) to make the label = div-id unique: + this.title = textValue('​' + i18n.LblProjectName); + this.fileName = textValue('​' + i18n.LblFileName); - // Note that in theory, there shouldn't be any conflicting ids, but in reality there are; - // for example it has been observed with BPMN/influx which is based on bpmn.io like cawemo. - // ToDo: make it an option. +// console.debug('exportOptionsClicked',self.title,fileName); + } + chooseFormatAndExport() { + if (this.exporting) return; - // Check, whether the existing model has an element with the same id, - // and since it does have a different title or different type (otherwise it would have been substituted above), - // assign a new id to the new element: - if (duplicateId(self.data, nR.id)) { - let newId = genID('R-'); - // first assign new ID to all references: - substituteR(nD, { id: newId } as Resource, nR); - // and then to the resource itself: - nR.id = newId - }; - // console.debug('+ resource',nR); - itmL.push(nR) - }); - console.info((nD.resources.length - itmL.length) + " resources adopted and " + itmL.length+" added."); - pend++; - self.createContent('resource', itmL) - .then(finalize, uDO.reject); - }; -// console.debug('#3',simpleClone(self.data),simpleClone(nD)); + var self = this; + const exportFormatClicked = 'app.cache.selectedProject.exportFormatClicked()'; + // @ts-ignore - BootstrapDialog() is loaded at runtime + new BootstrapDialog({ + title: i18n.LblExport + ": '" + this.title + "'", + type: 'type-primary', + /* // @ts-ignore - BootstrapDialog() is loaded at runtime + size: BootstrapDialog.SIZE_WIDE, */ + message: () => { + var form = '
' + // + '
' + + '
' + + '
' + // + "

"+i18n.LblFormat+"

" + + "

" + i18n.MsgExport + "

" + + radioField( + i18n.LblFormat, + [ + { title: 'SpecIF v' + CONFIG.specifVersion, id: 'specif', checked: true }, + { title: 'HTML with embedded SpecIF v' + CONFIG.specifVersion, id: 'html' }, + { title: 'ReqIF v1.2', id: 'reqif' }, + // { title: 'RDF', id: 'rdf' }, + { title: 'Turtle (experimental)', id: 'turtle' }, + { title: 'ePub v2', id: 'epub' }, + { title: 'MS WORD® (Open XML)', id: 'oxml' } + ], + { handle: exportFormatClicked } // options depend on format + ) + + '
' + + '
' + // + '
' + + '
' + + this.chooseExportOptions('specif') // parameter must correspond to the checked option above + + '
' + + '
'; + return $(form) + }, + buttons: [ + { + label: i18n.BtnCancel, + action: (thisDlg) => { + thisDlg.close() + } + }, + { + label: i18n.BtnExport, + cssClass: 'btn-success', + action: (thisDlg) => { + // Get index of option: + app.busy.set(); + message.show(i18n.MsgBrowserSaving, { severity: 'success', duration: CONFIG.messageDisplayTimeShort }); +// console.debug('options',checkboxValues( i18n.LblOptions )); + let options = { + format: radioValue(i18n.LblFormat), + fileName: this.fileName + }; + // further options according to the checkboxes: + checkboxValues(i18n.modelElements).forEach( + (op) => { + options[op] = true + } + ); + this.exportAs(options).then( + // app.busy.reset, --> doesn't work for some reason, 'this' within reset() is undefined ... + () =>{ app.busy.reset() }, + handleError + ); + thisDlg.close(); + } + } + ] + }) + .open(); - // 3. Create the remaining items; - // self.createContent('statement', nD.statements) could be called, - // but then the new elements would replace the existing ones. - // In case of 'adopt' the existing shall prevail! - if (Array.isArray(nD.statements)) { - let itmL: Statement[] = []; - nD.statements.forEach((nS: Statement) => { - // nR is a resource in the new data + // --- + function handleError(xhr: xhrMessage): void { + self.exporting = false; + app.busy.reset(); + message.show(xhr); + } + } + private exportAs(opts?: any): Promise { + var self = this; - // Adopt statement with the same id, title and class right away: - let eS: Statement = itemById(self.data.statements, nS.id); // statement in the existing data - if (eS && equalS(eS, nS)) return; - // Else, create new element: - itmL.push(nS); - }); - console.info((nD.statements.length - itmL.length) + " statements adopted and " + itmL.length + " added."); - pend++; - self.createContent('statement', itmL) - .then(finalize, uDO.reject); - }; - pend++; - self.createContent( 'hierarchy', nD.hierarchies ) - .then( finalize, uDO.reject ); + if (!opts) opts = {}; + if (!opts.format) opts.format = 'specif'; + // in certain cases, try to export files with the same name in PNG format, as well. + // - ole: often, preview images are supplied in PNG format; + // - svg: for generation of DOC or ePub, equivalent images in PNG-format are needed. + // if( typeof(opts.preferPng)!='boolean' ) opts.preferPng = true; ... is the default + // if( !opts.alternatePngFor ) opts.alternatePngFor = ['svg','ole']; ... not yet supported - if (Array.isArray(nD.files)) { - let itmL:any[] = []; - nD.files.forEach((nF:any) => { - // nR is a resource in the new data + return new Promise((resolve, reject) =>{ - // Adopt equal file right away: - let eF:any = itemById(self.data.files, nF.id); // file in the existing data - if (eF && equalF(eF, nF)) return; - // Else, create new element: - itmL.push(nF); - }); - console.info((nD.files.length - itmL.length) + " files adopted and " + itmL.length+" added."); - pend++; - self.createContent('file', itmL) - .then(finalize, uDO.reject); + if (self.exporting) { + // prohibit multiple entry + reject({ status: 999, statusText: "Export in process, please wait a little while" }); + } + else { + // if (self.data.exp) { // check permission + self.exporting = true; // set status to prohibit multiple entry + + switch (opts.format) { + // case 'rdf': + case 'turtle': + case 'reqif': + case 'html': + case 'specif': + storeAs(opts); + break; + case 'epub': + case 'oxml': + publish(opts); + }; + // } + // else { + // reject({ status: 999, statusText: "No permission to export" }); + // }; }; return; - function finalize():void { - if(--pend<1) { -// console.debug('#4',simpleClone(self.data),simpleClone(nD)); - // 4. Finally some house-keeping: - hookStatements(); - self.deduplicate(); // deduplicate equal items - // ToDo: Save changes from deduplication to the server. -// console.debug('#5',simpleClone(self.data),opts); - self.createFolderWithResourcesByType(self.data,opts) - .then( - ()=>{ - self.createFolderWithGlossary(self.data,opts) - .then( uDO.resolve, uDO.reject ) - }, - uDO.reject - ); + function publish(opts: any): void { + if (!opts || ['epub', 'oxml'].indexOf(opts.format) < 0) { + // programming error! + reject(); + return; }; - } - } - /* function updateWithLastChanged( nD, opts ) { - console.debug('update project',nD,opts); - // Update a loaded project with data of the new: - // - Types with the same id must be compatible - // - New types will be added - // - Instances with newer changedAt replace older ones - // - Both the id and alternativeIds are used to associate existing and new instances - // In a first pass check, if there is any incompatible type making an update impossible: - rc = classesAreCompatible('dataType',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - rc = classesAreCompatible('propertyClass',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - rc = classesAreCompatible('resourceClass',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - rc = classesAreCompatible('statementClass',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - console.info("All existing types are compatible with '"+newD.title+"'"); - } + // ToDo: Get the newest data from the server. +// console.debug( "publish", opts ); - // newD is new data in 'internal' data structure - // add new elements - // update elements with the same id - // exception: since types cannot be updated, return with error in case newD contains incompatible types - // There are four modes with respect to the types: - // - "match": if a type in newD with the same id is already present and it differs, quit with error-code. - // This is the minimum condition and true for all of the following modes, as well. - // - "deduplicate": if an identical type in newD with a different id is found, take the existing one - // and update the instances of the suppressed class. - // - "extend": in addition to "deduplicate", combine similar types. E.g. combine integer types and take the overall value range - // or add additional propertyClasses to a resourceClass. - // - "ignore": new propertyClasses and all their instances are ignored - mode = mode || 'deduplicate'; -// console.debug('cache.update',newD,mode); - var rc = {}, - uDO = $.Deferred(); - // newD = self.set( newD ); // transform to internal data structure - if( !newD ) { - uDO.reject({ - status: 995, - statusText: i18n.MsgImportFailed - }); - return uDO - }; + // If a hidden property is defined with value, it is suppressed only if it has this value; + // if the value is undefined, the property is suppressed in all cases. + opts.hiddenProperties = [ + { title: CONFIG.propClassType, value: CONFIG.resClassFolder }, + { title: CONFIG.propClassType, value: CONFIG.resClassOutline } + ]; - // In a first pass check, if there is any incompatible type making an update impossible: - rc = classesAreCompatible('dataType',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - rc = classesAreCompatible('resourceClass',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - rc = classesAreCompatible('statementClass',mode); - if( rc.status>0 ) { - uDO.reject( rc ); - return uDO - }; - console.info("All existing types are compatible with '"+newD.title+"'"); + opts.allResources = false; // only resources referenced by a hierarchy. + // Don't lookup titles now, but within toOxml(), so that that the publication can properly classify the properties. + opts.lookupTitles = false; // applies to self.data.toExt() + opts.lookupValues = true; // applies to self.data.toExt() + // But DO reduce to the language desired. + opts.lookupLanguage = true; + if (typeof (opts.targetLanguage) != 'string') opts.targetLanguage = browser.language; + opts.makeHTML = true; + opts.linkifyURLs = true; + opts.createHierarchyRootIfNotPresent = true; + opts.allDiagramsAsImage = true; + // opts.allImagesAsPNG = ["oxml"].indexOf(opts.format) > -1; .. not yet implemented!! + // take newest revision: + opts.revisionDate = new Date().toISOString(); - // In a second pass, start with creating any type which does not yet exist. - // Start with the datatypes; the next steps will be chained by function updateNext: - var pend=0; - addNewTypes('dataType'); + self.read().toExt(opts).then( + (expD) => { +// console.debug('publish',expD,opts); + let localOpts = { + // Values of declared stereotypeProperties get enclosed by double-angle quotation mark '«' and '»' + titleLinkTargets: CONFIG.modelElementClasses.concat(CONFIG.diagramClasses), + titleProperties: CONFIG.titleProperties.concat(CONFIG.headingProperties), + descriptionProperties: CONFIG.descProperties, + stereotypeProperties: CONFIG.stereotypeProperties, + lookup: i18n.lookup, + showEmptyProperties: CONFIG.showEmptyProperties, + imgExtensions: CONFIG.imgExtensions, + applExtensions: CONFIG.applExtensions, + // hasContent: Lib.hasContent, + propertiesLabel: opts.withOtherProperties ? 'SpecIF:Properties' : undefined, + statementsLabel: opts.withStatements ? 'SpecIF:Statements' : undefined, + fileName: self.fileName || expD.title, + colorAccent1: '0071B9', // adesso blue + done: () => { app.cache.selectedProject.exporting = false; resolve() }, + fail: (xhr) => { app.cache.selectedProject.exporting = false; reject(xhr) } + }; - return uDO + switch (opts.format) { + case 'epub': + // @ts-ignore - toEpub() is loaded at runtime + toEpub(expD, localOpts); + break; + case 'oxml': + // @ts-ignore - toOxml() is loaded at runtime + toOxml(expD, localOpts); + }; + // resolve() is called in the call-backs defined by opts + }, + reject + ); + } + function storeAs(opts: any): void { + if (!opts || ['specif', 'html', 'reqif', 'turtle'].indexOf(opts.format) < 0) + // programming error! + throw Error("Invalid format specified on export"); - function classesAreCompatible( ctg:string, mode ) { - let tL = standardTypes.listNameOf(ctg), - aL = self.data[tL], - nL = newD[tL]; - // true, if every element in nL is compatibly present in aL or if it can be added: - let j:number, rC; - for( var i=nL.length-1;i>-1;i-- ) { - for( j=aL.length-1;j>-1;j-- ) { -// console.debug('classesAreCompatible',aL[j],nL[i]); - // if a single element is incompatible the lists are incompatible: - rC = typeIsCompatible(ctg,aL[j],nL[i],mode); - // on first error occurring, quit with return code: - if( rC.status>0 ) return rC - } - }; - return {status:0} - } - function updateNext(ctg:string) { - // chains the updating of types and elements in asynchronous operation: - console.info('Finished updating:',ctg); - // having finished with elements of category 'ctg', start next step: - switch( ctg ) { - case 'dataType': addNewTypes( 'resourceClass' ); break; - case 'resourceClass': addNewTypes( 'statementClass' ); break; - case 'statementClass': updateIfChanged( 'file' ); break; - case 'file': updateIfChanged( 'resource' ); break; - case 'resource': updateIfChanged( 'statement' ); break; - case 'statement': updateIfChanged( 'hierarchy' ); break; - case 'hierarchy': - uDO.notify(i18n.MsgProjectUpdated,100); - console.info('Project successfully updated'); - uDO.resolve(); + // ToDo: Get the newest data from the server. +// console.debug( "storeAs", opts ); + + opts.allResources = false; // only resources referenced by a hierarchy. + // keep vocabulary terms: + opts.lookupTitles = false; + opts.lookupValues = false; + opts.allDiagramsAsImage = ["html","turtle","reqif"].indexOf(opts.format) > -1; + + switch (opts.format) { + case 'specif': + opts.allResources = true; // even if not referenced by a hierarchy. + // no break + case 'html': + // export all languages: + opts.lookupLanguage = false; + // opts.targetLanguage = undefined; + // keep all revisions: + // opts.revisionDate = undefined; break; - default: uDO.reject() //should never arrive here - } - } - function addNewTypes( ctg:string ) { - // Is commonly used for resource and statement classes with their propertyClasses. - let rL, nL, rT; - switch( ctg ) { - case 'dataType': rL = self.data.dataTypes; nL = newD.dataTypes; break; - case 'resourceClass': rL = self.data.resourceClasses; nL = newD.resourceClasses; break; - case 'statementClass': rL = self.data.statementClasses; nL = newD.statementClasses; break; - default: return null //should never arrive here - }; - nL.forEach( (nT)=>{ - rT = itemById(rL,nT.id); - if( rT ) { - // a type with the same id exists. - // ToDo: Add a new enum value to an existing enum dataType (server does not allow it yet) + // case 'rdf': + case 'turtle': + case 'reqif': + // only single language is supported: + opts.lookupLanguage = true; + if (typeof (opts.targetLanguage) != 'string') opts.targetLanguage = browser.language; + // XHTML is supported: + opts.makeHTML = true; + opts.linkifyURLs = true; + opts.createHierarchyRootIfNotPresent = true; + // take newest revision: + opts.revisionDate = new Date().toISOString(); + break; + default: + reject(); + return; // should never arrive here + }; +// console.debug( "storeAs", opts ); - // Add a new property class to an existing type: - switch( mode ) { - case 'match': - // Reference and new data DO match (as checked, before) - // ... so nothing needs to be done, here. - // no break - case 'ignore': - // later on, only properties for which the user has update permission will be considered, - // ... so nothing needs to be done here, either. - break; - case 'extend': - // add all missing propertyClasses: - // ToDo: Is it possible that the user does not have read permission for a property class ?? - // Then, if it is tried to create the supposedly missing property class, an error occurs. - // But currently all *types* are visible for everybody, so there is no problem. - if( nT.propertyClasses && nT.propertyClasses.length>0 ) { - // must create missing propertyClasses one by one in ascending sequence, - // because a newly added property class can be specified as predecessor: - addNewPC( rT, nT.propertyClasses, 0 ) - } - } - } else { - // else: the type does not exist and will be created, therefore: - pend++; - console.info('Creating type',nT.title); - self.createContent(nT.category,nT) - .done(()=>{ - if( --pend<1 ) updateNext( ctg ) - }) - .fail( uDO.reject ) - } - }); - // if no type needs to be created, continue with the next: - if(pend<1) updateNext( ctg ); - return + self.read().toExt(opts).then( + (expD) => { +// console.debug('storeAs', expD, opts); + let fName = self.fileName || expD.title; - function addNewPC( r, nPCs, idx ) { - // r: existing (=reference) type with its propertyClasses - // nPCs: new list of propertyClasses - // idx: current index of nPCs - if( nPCs[idx].id?itemById( r.propertyClasses, nPCs[idx].id ):itemByName( r.propertyClasses, nPCs[idx].title ) ) { - // not missing, so try next: - if( ++idx0 ) - nPCs[idx].predecessor = nPCs[idx-1].id; + toHtmlDoc(expD, opts).then( + (dta): void =>{ + let blob = new Blob([dta], { type: "text/html; charset=utf-8" }); + // @ts-ignore - saveAs() is loaded at runtime + saveAs(blob, fName + '.specif.html'); + self.exporting = false; + resolve(); + }, + (xhr): void =>{ + self.exporting = false; + reject(xhr); + } + ); + return; + }; - // add the new property class also to r: - let p = indexById( r.propertyClasses, nPCs[idx].predecessor ); - console.info('Creating property class', nPCs[idx].title); - // insert at the position similarly to the new type; - // if p==-1, then it will be inserted at the first position: - r.propertyClasses.splice( p+1, 0, nPCs[idx] ); - server.project({id:self.data.id}).allClasses({id:r.id}).class(nPCs[idx]).create() - .done( ()=>{ - // Type creation must be completed before starting to update the elements: - if( ++idx0 ) - self.updateContent(ctg,newD.files) - .done( ()=>{ - // Wait for all files to be loaded, so that resources will have higher revision numbers: - newD.files = []; - updateNext(ctg) - }) - .fail( uDO.reject ) - else - updateNext(ctg); - return; - case 'resource': itemL = newD.resources; uDO.notify(i18n.MsgLoadingObjects,50); break; - case 'statement': itemL = newD.statements; uDO.notify(i18n.MsgLoadingRelations,70); break; - case 'hierarchy': itemL = newD.hierarchies; uDO.notify(i18n.MsgLoadingHierarchies,80); break; - default: return null //should never arrive here - }; - itemL.forEach( (itm)=>{ - updateInstanceIfChanged(ctg,itm) - }); - // if list is empty, continue directly with the next item type: - if(pend<1) updateNext( ctg ) - return + // B) Processing for all formats except 'html': + // @ts-ignore - JSZip() is loaded at runtime + let zipper = new JSZip(), + zName: string, + mimetype = "application/zip"; + + // Add the files to the ZIP container: + if (expD.files) + expD.files.forEach((f) => { +// console.debug('zip a file',f); + zipper.file(f.title, f.blob); + delete f.blob; // the SpecIF data below shall not contain it ... + }); - function contentChanged(ctg:string, r, n) { // ref and new resources -// console.debug('contentChanged',ctg, r, n); - // Is commonly used for resource, statement and hierarchy instances. - if( r['class']!=n['class'] ) return null; // fatal error, they must be equal! + // Prepare the output data: + switch (opts.format) { + case 'specif': + fName += ".specif"; + zName = fName + '.zip'; + expD = JSON.stringify(expD); + break; + case 'reqif': + fName += ".reqif"; + zName = fName + 'z'; + mimetype = "application/reqif+zip"; + expD = app.ioReqif.toReqif(expD); + break; + case 'turtle': + fName += ".ttl"; + zName = fName + '.zip'; + // @ts-ignore - transformSpecifToTTL() is loaded at runtime + expD = transformSpecifToTTL("https://specif.de/examples", expD); + /* break; + case 'rdf': + if( !app.ioRdf ) { + reject({status:999,statusText:"ioRdf not loaded."}); + return; + }; + fName += ".rdf"; + expD = app.ioRdf.toRdf( expD ); */ + }; + let blob = new Blob([expD], { type: "text/plain; charset=utf-8" }); + // Add the project: + zipper.file(fName, blob); + blob = undefined; // free heap space + + // done, store the specif.zip: + zipper.generateAsync({ + type: "blob", + compression: "DEFLATE", + compressionOptions: { level: 7 }, + mimeType: mimetype + }) + .then( + (blob: Blob) => { + // successfully generated: +// console.debug("storing ZIP of '"+fName+"'."); + // @ts-ignore - saveAs() is loaded at runtime + saveAs(blob, zName); + self.exporting = false; + resolve(); + }, + (xhr: xhrMessage) => { + // an error has occurred: + console.error("Cannot create ZIP of '" + fName + "'."); + self.exporting = false; + reject(xhr); + } + ); + }, + reject + ) + } + }); + } - // Continue in case of resources and statements: - let i=null, rA=null, nA=null, rV=null, nV=null; - // 1) Are the property values equal? - // Skipped, if the new instance does not have any property (list is empty or not present). - // Statements and hierarchies often have no properties. - // Resources without properties are useless, as they do not carry any user payload (information). - // Note that the actual property list delivered by the server depends on the read privilege of the user. - // Only the properties, for which the current user has update privilege, will be compared. - // Use case: Update diagrams with model-elements only: - // Create a user with update privileges for resourceClass 'diagram' - // and property class 'title' of resourceClass 'model-element'. - // Then, only the diagrams and the title of the model-elements will be updated. - if( n.properties && n.properties.length>0 ) { - for( i=(r.properties?r.properties.length:0)-1;i>-1;i--) { - rA = r.properties[i]; -// console.debug( 'update?', r, n); - // no update, if the current user has no privilege: - if( !rA.upd ) continue; - // look for the corresponding property: - nA = itemBy( n.properties, 'class', rA['class'] ); - // no update, if there is no corresponding property in the new data: - if( !nA ) continue; - // in all other cases compare the value: - let oT = itemById( app.cache.selectedProject.data.resourceClasses, n['class'] ), // applies to both r and n - rDT = dataTypeOf( app.cache.selectedProject.data, rA['class'] ), - nDT = dataTypeOf( newD, nA['class'] ); - if( rDT.type!=nDT.type ) return null; // fatal error, they must be equal! - switch( nDT.type ) { - case 'xs:enumeration': - // value has a comma-separated list of value-IDs, - rV = enumValueOf(rDT,rA); - nV = enumValueOf(nDT,nA); -// console.debug('contentChanged','ENUM',rA,nA,rV!=nV); - if( rV!=nV ) return true; - break; - case 'xhtml': - // rV = toHex(stripCtrl(rA.value).reduceWhiteSpace()); - // nV = toHex(stripCtrl(fileRef.toServer(nA.value)).reduceWhiteSpace()); - // rV = stripCtrl(rA.value).reduceWhiteSpace(); - rV = rA.value; - // apply the same transformation to nV which has been applied to rV before storing: - // nV = stripCtrl(fileRef.toServer(nA.value)).reduceWhiteSpace(); - // nV = fileRef.toServer(nA.value); - nV = nA.value; -// console.debug('contentChanged','xhtml',rA,nA,rV!=nV); - if( rV!=nV ) return true; - // If a file is referenced, pretend that the resource has changed. - // Note that a resource always references a file having the next lower revision number than istself. - // It is possible that a file has been updated, so a referencing resource must be updated, as well. - // ToDo: Analyse whether a referenced file has really been updated. - if( RE.tagNestedObjects.test(nV) - || RE.tagSingleObject.test(nV) ) return true; - break; - default: - if( rA.value!=nA.value ) return true - } - } - }; - // 2) Statements must have equal subjectClasses and objectClasses - with equal revisions? - if( ctg == 'statement' ) { - // if( n.subject.id!=r.subject.id || n.subject.revision!=r.subject.revision) return true; - // if( n.object.id!=r.object.id || n.object.revision!=r.object.revision) return true; - if( n.subject.id!=r.subject.id - || n.object.id!=r.object.id ) return true - }; - return false // ref and new are the same - } - function updateInstanceIfChanged(ctg:string,nI) { - // Update an element/item of the specified category, if changed. - pend++; - self.readContent(ctg,nI,true) // reload from the server to obtain most recent data - .done( (rI)=>{ - // compare actual and new item: -// console.debug('updateInstanceIfChanged',ctg,rI,nI); - // ToDo: Detect parallel changes and merge interactively ... - if( Date.parse(rI.changedAt){ - // in case the nI.properties are supplied in a different order: - nA = itemBy(nI.properties,'class',rA['class']); - if( nA ) { - nA.upd = rA.upd; - nA.del = rA.del - } - }); - console.info('Updating instance',nI.title); - // ToDo: Test whether only supplied properties are updated by the server; otherwise implement the behavior, here. - self.updateContent( ctg, nI ) - .done( updateTreeIfChanged( ctg, rI, nI ) ) // update the tree, if necessary. - .fail( uDO.reject ) - } else { - // no change, so continue directly: - updateTreeIfChanged( ctg, rI, nI ) // update the tree, if necessary. - } - }) - .fail( (xhr)=Y{ - switch( xhr.status ) { - case 403: - // This is a hack to circumvent a server limitation. - // In case the user is not admin, the server delivers 403, if a resource does not exist, - // whereas it delivers 404, if it is an admin. - // Thus: If 403 is delivered and the user has read access according to the resourceClass, - // do as if 404 had been delivered. - var pT = itemById(app.cache.selectedProject.data.allClasses,nI['class']); -// console.debug('403 instead of 404',nI,pT); - if( !pT.rea || !pT.cre ) { uDO.reject(xhr); return }; - // else the server should have delivered 404, so go on ... - case 404: -// console.debug('not found',xhr.status); - // no item with this id, so create a new one: - self.createContent(ctg,nI) - .done(()=>{ - if( --pend<1 ) updateNext( ctg ) - }) - .fail( uDO.reject ) - break; - default: - uDO.reject(xhr) - } - }) - } - function updateTreeIfChanged( ctg:string, aI, nI ) { - // Update all children (nodes) of a hierarchy root. - // This is a brute force solution, since any mismatch causes an update of the whole tree. - // ToDo: Add or delete a single child as required. - // ToDo: Update the smallest possible subtree in case addition or deletion of a single child is not sufficient. - - function newIds(h) { - // new and updated hierarchy entries must have a new id (server does not support revisions for hierarchies): - h.children.forEach( (ch)=>{ - ch.id = genID('N-'); - newIds(ch) - }) - } - function treeChanged(a,n) { - // Equal hierarchies? - // All children (nodes in SpecIF terms) on all levels must have the same sequence. - return nodesChanged(a.children,n.children) - - function nodesChanged(aL,nL) { -// console.debug( 'nodesChanged',aL,nL ) - if( (!aL || aL.length<1) && (!nL || nL.length<1) ) return false; // no update needed - if( aL.length!=nL.length ) return true; // update! - for( let i=nL.length-1; i>-1; i-- ) { - // compare the references only, as the hierarchy ids can change: - if( !aL[i] || aL[i].ref!=nL[i].ref ) return true; - if( nodesChanged(aL[i].children,nL[i].children) ) return true - }; - return false - } - } + private equalDT(r: DataType, n: DataType): boolean { + // return true, if reference and new dataType are equal: + if (r.type != n.type) return false; + switch (r.type) { + case TypeEnum.XsDouble: + if (r.fractionDigits != n.fractionDigits) return false; + // no break + case TypeEnum.XsInteger: + return r.minInclusive == n.minInclusive && r.maxInclusive == n.maxInclusive; + case TypeEnum.XsString: + case TypeEnum.XHTML: + return r.maxLength == n.maxLength; + case TypeEnum.XsEnumeration: + // Perhaps we must also look at the title .. + // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType + if (r.values.length != n.values.length) return false; + // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType + for (var i = n.values.length - 1; i > -1; i--) + // assuming that the titles/values don't matter: + // @ts-ignore - values is optional for a dataType, but not for an enumerated dataType + if (indexById(r.values, n.values[i].id) < 0) return false; + // the list of enumerated values *is* equal, + // finally check for the multiple flag: + return (r.multiple && n.multiple || !r.multiple && !n.multiple); + default: + return true; + }; + } + private equalPC(r: PropertyClass, n: PropertyClass): boolean { + // return true, if reference and new propertyClass are equal: + return r.dataType == n.dataType + && r.title == n.title + && (r.multiple && n.multiple || !r.multiple && !n.multiple); + } + private equalRC(r: ResourceClass, n: ResourceClass): boolean { + // return true, if reference and new resourceClass are equal: + if (!r.isHeading && n.isHeading || r.isHeading && !n.isHeading) return false; + return r.title == n.title + && this.eqL(r.propertyClasses, n.propertyClasses) + // && this.eqL( r.instantiation, n.instantiation ) + // --> the instantiation setting of the reference shall prevail + } + private equalSC(r: StatementClass, n: StatementClass): boolean { + // return true, if reference and new statementClass are equal: + return r.title == n.title + && eqSCL(r.propertyClasses, n.propertyClasses) + && eqSCL(r.subjectClasses, n.subjectClasses) + && eqSCL(r.objectClasses, n.objectClasses) + && eqSCL(r.instantiation, n.instantiation); - // Note: 'updateTreeIfChanged' is called for instance of ALL types, even though only a hierarchy has children. - // In case of a resource or statement, the tree operations are skipped: - if( ctg == 'hierarchy' && treeChanged(aI,nI) ) { - message.show( i18n.MsgOutlineAdded, {severity:'info', duration:CONFIG.messageDisplayTimeShort} ); - // self.deleteContent('hierarchy',aI.children); // can be be prohibited by removing the permission, but it is easily forgotten to change the role ... - newIds(nI); - server.project(app.cache.selectedProject.data).specification(nI).createChildren() - .done( ()=>{ - if( --pend<1 ) updateNext( ctg ) - }) - .fail( uDO.reject ) - } else { - // no hierarchy (tree) has been changed, so no update: - if( --pend<1 ) updateNext( ctg ) - } - } - } */ - }; + function eqSCL(rL: any, nL: any): boolean { +// console.debug('eqSCL',rL,nL); + // return true, if both lists have equal members, + // in this case we allow also less specified statementClasses + // (for example, when a statement is created from an Excel sheet): + if (!Array.isArray(nL)) return true; + // no or empty lists are allowed and considerated equal: + let rArr = Array.isArray(rL) && rL.length > 0, + nArr = Array.isArray(nL) && nL.length > 0; + if (!rArr && nArr + || rL.length != nL.length) return false; + // the sequence may differ: + for (var i = rL.length - 1; i > -1; i--) + if (nL.indexOf(rL[i]) < 0) return false; + return true; + } + } - self.createContent = ( ctg:string, item:Item ):Promise =>{ - // item can be a js-object or a list of js-objects - // ctg is a member of [dataType, resourceClass, statementClass, propertyClass, resource, statement, hierarchy] - // ... not all of them may be implemented, so far. - // cache the value before sending it to the server, as the result is not received after sending (except for 'resource' and 'statement') - return new Promise( - (resolve) => { - // (resolve,reject)=>{ -// console.debug('createContent', ctg, item ); - /* switch( ctg ) { - // case 'resource': - // case 'statement': - // case 'hierarchy': - // case 'node': - // no break - default: - // if current user can create an item, he has the other permissions, as well: - // addPermissions( item ); - // item.createdAt = new Date().toISOString(); - // item.createdBy = item.changedBy; */ - self.data.cache( ctg, item ) - // }; - resolve(item); - } - ); - }; - self.readContent = ( ctg:string, item:Item[]|Item|string, opts?:any ):Promise=>{ - // ctg is a member of [dataType, resourceClass, statementClass, resource, statement, hierarchy] + private equalKey(r: KeyObject, n: KeyObject): boolean { + // Return true if both keys are equivalent; + // this applies if only an id is given or a key with id and revision: + return Lib.itemIdOf(r) == Lib.itemIdOf(n) + && r.revision == n.revision; + } + private equalR(r: Resource, n: Resource): boolean { + // return true, if reference and new resource are equal. + // ToDo: Consider, if model-elements are considered equal, + // only if they have the same title *and* class, + // ToDo: Also, if a property with title CONFIG.propClassType has the same value? +// console.debug('equalR',r,n,resClassTitleOf(r,dta),resClassTitleOf(n,dta)); - if (!opts) opts = { reload: false, timelag: 10 }; - // override 'reload' as long as there is no server and we know that the resource is found in the cache: - opts.reload = false; + // Sort out most cases with minimal computing; + // assuming that the types have already been consolidated: + let dta = this.data; + if (r.title != n.title || resClassTitleOf(r, dta) != resClassTitleOf(n, dta)) + return false; - return new Promise((resolve, reject) => { - if (!opts.reload) { - // return the cached object asynchronously: - // delay the answer a little, so that the caller can properly process a batch: - setTimeout(() =>{ - resolve(self.data.readCache(ctg, item)); - }, opts.timelag); - } - else - // try to get the items from the server, but meanwhile: - reject({ status: 745, statusText: "No server available" }) - }); - }; - self.updateContent = ( ctg:string, item:Item[]|Item ):Promise =>{ - // ctg is a member of [resource, statement, hierarchy], 'null' is returned in all other cases. - function updateCh( itm:Item ):void { - itm.changedAt = new Date().toISOString(); - itm.changedBy = app.me.userName; - } + // Here, both resources have equal titles and class-titles: - return new Promise( - (resolve) => { - switch( ctg ) { - case 'node': - throw Error("Nodes can only be created or deleted"); - // case 'resource': - // case 'statement': - // case 'hierarchy': - // no break - default: -// console.debug('updateContent - cache', ctg ); - if( Array.isArray(item) ) - item.forEach( updateCh ) - else - updateCh(item); - self.data.cache( ctg, item ) + // Only a genuine title will be considered truly equal, but not a default title + // being equal to the content of property CONFIG.propClassType is not considered equal + // (for example BPMN endEvents which don't have a genuine title): + let typ = valByTitle(r, CONFIG.propClassType, dta), + rgT = RE.splitNamespace.exec(typ); + // rgT[2] contains the type without namespace (works also, if there is no namespace). + return (!rgT || rgT[2] != r.title); + } + private equalS(r: Statement, n: Statement): boolean { + // return true, if reference and new statement are equal: + // Model-elements are only equal, if they have the same class. + // ToDo: Also, if they have the same class title? + // ToDo: Also, if a property with title CONFIG.propClassType has the same value? + return r['class'] == n['class'] + && this.equalKey(r.subject, n.subject) + && this.equalKey(r.object, n.object); + } + private equalF(r: any, n: any): boolean { + // return true, if reference and new file are equal: + return r.id == n.id + && r.title == n.title + && r.type == n.type; + } + private eqL(rL: any[], nL: any[]): boolean { + // return true, if both lists have equal members: + // no or empty lists are allowed and considerated equal: + let rArr = Array.isArray(rL) && rL.length > 0, + nArr = Array.isArray(nL) && nL.length > 0; + if (!rArr && !nArr) return true; + if (!rArr && nArr + || rArr && !nArr + || rL.length != nL.length) return false; + // the sequence may differ: + for (var i = rL.length - 1; i > -1; i--) + if (nL.indexOf(rL[i]) < 0) return false; + return true; + } + private compatibleDT(refC: DataType, newC: DataType): boolean { + // return this.typeIsCompatible("dataType", refC, newC).status == 0; + switch (refC.type) { + case TypeEnum.XsBoolean: + case TypeEnum.XsDateTime: + return true; + case TypeEnum.XHTML: + case TypeEnum.XsString: +// console.debug( refC.maxLength>newC.maxLength-1 ); + if (refC.maxLength == undefined) + return true; + if (newC.maxLength == undefined || refC.maxLength < newC.maxLength) { + Lib.logMsg({ status: 951, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }); + return false;; }; - resolve(); - } - ); - }; - self.deleteContent = ( ctg:string, item:Item ):Promise =>{ - // ctg is a member of [dataType, resourceClass, statementClass, propertyClass, resource, statement, hierarchy] - /* function isInUse( ctg, itm ) { - function dTIsInUse( L, dT ) { - let i=null; - for( var e=L.length-1;e>-1;e-- ) { - i = L[e].propertyClasses?indexBy(L[e].propertyClasses,'dataType',dT.id):-1; -// console.debug('dTIsInUse',dT,L,e,i); - if( i>-1 ) return true - }; - return false - } - function aCIsInUse( ctg, sT ) { - let c = ctg.substr(0,ctg.length-4), // xyzType --> xyz, xyzClass ?? - L = cacheOf(c), - i = indexBy(L,ctg,sT.id); -// console.debug('aCIsInUse',sT,c,L,i); - // ToDo: In project.html, the resource cache is empty, but the resourceClass may be in use, anyways. - // Similarly with statements. - return ( i>-1 ) - } - function pCIsInUse( L, pT ) { - if( L==undefined ) return false; // can't be in use, if the list is not (yet) defined/present. - let i=null; - // ToDo: In project.html, the resource cache is empty, but the property class may be in use, anyways. - // Also a deleted resource may have used the propertyClass. - // As it stores only the newest types, the ReqIF Server will refuse to delete the type. - // In case of PouchDB, all revisions of classes/types are stored, so it is sufficient to check whether there are currently some elements using the type. - // Similarly with statements. - for( var e=L.length-1;e>-1;e-- ) { - i = L[e].properties?indexBy(L[e].properties,'class',pT.id):-1; -// console.debug('pCIsInUse property class',pT,L,e,i); - if( i>-1 ) return true - }; - return false - } -// console.debug('isInUse',ctg,item); - switch( ctg ) { - case 'dataType': return dTIsInUse(self.data.allClasses,itm); - case 'resourceClass': - case 'statementClass': return aCIsInUse(ctg,itm); - case 'class': return pCIsInUse(self.data.resources,itm) - || pCIsInUse(self.data.hierarchies,itm) - || pCIsInUse(self.data.statements,itm); + return true; + case TypeEnum.XsDouble: + // to be compatible, the new 'fractionDigits' must be lower or equal: + if (refC.fractionDigits < newC.fractionDigits) { + Lib.logMsg({ status: 952, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }); + return false; }; - return false - } */ - -// console.debug('deleteContent',ctg,item); - return new Promise( - (resolve, reject) => { - // Do not try to delete types which are in use; - switch( ctg ) { - /* case 'class': - case 'dataType': - case 'resourceClass': - case 'statementClass': if( Array.isArray(item) ) return null; // not yet supported - if( isInUse(ctg,item) ) { - dDO.reject({status:972, statusText:i18n.Err400TypeIsInUse}); - return dDO - }; - // no break; */ - case "resource": - case "statement": - case "node": -// console.debug('deleteContent',ctg,item); - if( self.data.uncache( ctg, item )<0 ) reject({status:999,statusText:ctg+' '+item.id+' not found and thus not deleted.'}); - break; - default: - reject({status:999,statusText:'Category '+ctg+' is unknown; item '+item.id+' could not be deleted.'}); + // else: go on ... + case TypeEnum.XsInteger: + // to be compatible, the new 'maxInclusive' must be lower or equal and the new 'minInclusive' must be higher or equal: +// console.debug( refC.maxInclusivenewC.minInclusive ); + if (refC.maxInclusive < newC.maxInclusive || refC.minInclusive > newC.minInclusive) { + Lib.logMsg({ status: 953, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }); + return false; }; - resolve(); + return true; + case TypeEnum.XsEnumeration: + // to be compatible, every value of the new 'enumeration' must be present in the present one: + // ToDo: Add a new enum value to an existing enum dataType. + var idx: number; + for (var v = newC.values.length - 1; v > -1; v--) { + idx = indexById(refC.values, newC.values[v].id); + // the id must be present: + if (idx < 0) { + Lib.logMsg({ status: 954, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }); + return false; + }; + // ... and the values must be equal: + if (refC.values[idx].value != newC.values[v].value) { + Lib.logMsg({ status: 955, statusText: "new dataType '" + newC.id + "' of type '" + newC.type + "' is incompatible" }); + return false; + }; + }; + return true; + }; + // should never arrive here ... as every branch in every case above has a return. + throw Error("Invalid data type."); + } + private compatiblePC(refC: PropertyClass, newC: PropertyClass): boolean { + /* // A resourceClass or statementClass is incompatible, if it has an equally-named property class with a different dataType + // A resourceClass or statementClass is compatible, if all equally-named propertyClasses have the same dataType + if (!newC.propertyClasses || !newC.propertyClasses.length) + return { status: 0 }; + // else: The new type has at least one property. + + if (!refC.propertyClasses || refC.propertyClasses.length < newC.propertyClasses.length) + return { status: 963, statusText: "new resourceClass or statementClass '" + newC.id + "' is incompatible (additional propertyClasses)" }; + // else: The new type has no more properties than the reference + + var idx: number, + nPC: PropertyClass; + for (var a = newC.propertyClasses.length - 1; a > -1; a--) { + nPC = newC.propertyClasses[a]; + if (nPC.id) { + // If an id exists, it must be equal to one of refC's propertyClasses: + idx = indexById(refC.propertyClasses, nPC.id) + } else { + // If there is no id, the type is new and there are no referencing elements, yet. + // So it does not matter. + // But there must be a property class with the same name: + idx = indexByTitle(refC.propertyClasses, nPC.title) + }; + if (idx < 0) { + // The property class in the new data is not found in the existing (reference) data: + if (!opts || !opts.mode || ["match", "include"].indexOf(opts.mode) > -1) + // the property class is expected and thus an error is signalled: + return { status: 964, statusText: "new resourceClass or statementClass '" + newC.id + "' is incompatible (additional propertyClass)" } + else + // cases 'extend' and 'ignore'; + // either the property will be created later on, or it will be ignored; + // we are checking only in a first pass. + continue; + }; + // else: the property class is present; in this case and in all modes the dataTypes must be equal: + if (refC.propertyClasses[idx].dataType != nPC.dataType) { + return { status: 965, statusText: "new resourceClass or statementClass '" + newC.id + "' is incompatible (different dataType)" } } - ); - }; + }; + return { status: 0 }; */ + if (this.equalPC(refC, newC)) + return true; + // else: + Lib.logMsg({ status: 956, statusText: "new propertyClass '" + newC.id + "' is incompatible" }); + return false; + } + private compatiblePCReferences(refC: ResourceClass | StatementClass, newC: ResourceClass | StatementClass, opts?:any): boolean { + if (!opts || !opts.mode) opts = { mode: "match" }; // most restrictive by default + if (Array.isArray(refC.propertyClasses) && Array.isArray(newC.propertyClasses)) { + switch (opts.mode) { + case "include": + return !(refC.propertyClasses.length < newC.propertyClasses.length || missingProperty(refC, newC)); + case "match": + default: + return !(refC.propertyClasses.length != newC.propertyClasses.length || missingProperty(refC, newC)); + }; + }; + switch (opts.mode) { + case "include": + // Also OK, if the new class doesn't reference any propertyClass, + // it is irrelevant whether the reference class references any or not: + return !Array.isArray(newC.propertyClasses) || newC.propertyClasses.length < 1; + case "match": + default: + return !Array.isArray(refC.propertyClasses) && !Array.isArray(newC.propertyClasses); + }; - self.createResource = ( rC:ResourceClass ):Promise =>{ - // Create an empty form (resource instance) for the resource class rC: - // Note: ES6 promises are used; - // see https://codeburst.io/a-simple-guide-to-es6-promises-d71bacd2e13a - // also https://javascript.info/promise-chaining - return new Promise( - (resolve, reject) => { - // Get the class's permissions. So far, it's property permissions are not loaded ... - var res:Resource; - - self.readContent( 'resourceClass', rC, {reload:true} ) - .then( - (rCL:ResourceClass[])=>{ -// console.debug('#1',rC); - // return an empty resource instance of the given type: - res = { - id: genID('R-'), - class: rCL[0].id, - title: '', - permissions: rCL[0].permissions||{cre:true,rea:true,upd:true,del:true}, - properties: [] - }; - return self.readContent( 'propertyClass', rC.propertyClasses, {reload:true} ) - } - ) - .then( - (pCL:PropertyClass[])=>{ -// console.debug('#2',pCL); - res.properties = forAll( pCL, createProp ); - resolve( res ) - } - ) - .catch( reject ); - } - ); - }; - self.readStatementsOf = ( res:Resource, opts?:any ):Statement[] =>{ - // Get the statements of a resource ... there are 2 use-cases: - // - All statements between resources appearing in a hierarchy shall be shown for navigation; - // it is possible that a resource is deleted (from all hierarchies), but not it's statements. - // --> set 'showComments' to false - // - All comments referring to the selected resource shall be shown; - // the resource is found in the cache, but the comment is not. - // --> set 'showComments' to true - // - It is assumed that the hierarchies contain only model-elements shown on a visible diagram, - // so only stetements are returned only for visible resources. - // - In addirion, only statements are returned which are shown on a visible diagram. - // (perhaps both checks are not necessary, as visible statements only referto vosible resources ...) + function missingProperty(rC:any, nC:any):boolean { + for (var i = nC.propertyClasses.length - 1; i > -1; i--) + if (rC.propertyClasses.indexOf(nC.propertyClasses[i]) < 0) + return true; + return false; + } + } + private compatibleRC(refC: ResourceClass, newC: ResourceClass, opts?:any): boolean { + if (this.compatiblePCReferences(refC, newC, opts)) + return true; + // else: + Lib.logMsg({ status: 963, statusText: "new resourceClass or statementClass '" + newC.id + "' is incompatible; propertyClasses don't match" }); + return false; + } + private compatibleSC(refC: StatementClass, newC: StatementClass, opts?:any): boolean { + // To be compatible, all sourceTypes of newC must be contained in the sourceTypes of refC; + // no sourceTypes means that all resourceClasses are permissible as subject. + // ... and similarly for the targetTypes: + if (refC.subjectClasses && !newC.subjectClasses + || refC.subjectClasses && newC.subjectClasses && !Lib.containsById(refC.subjectClasses, newC.subjectClasses)) { + Lib.logMsg({ status: 961, statusText: "new statementClass '" + newC.id + "' is incompatible (subjectClasses)" }); + return false; + }; + if (refC.objectClasses && !newC.objectClasses + || refC.objectClasses && newC.objectClasses && !Lib.containsById(refC.objectClasses, newC.objectClasses)) { + Lib.logMsg({ status: 962, statusText: "new statementClass '" + newC.id + "' is incompatible (objectClasses)" }); + return false; + }; + // else: so far everything is OK, but go on checking ... (no break!) + if (this.compatiblePCReferences(refC, newC, opts)) + return true; + // else: + Lib.logMsg({ status: 963, statusText: "new resourceClass or statementClass '" + newC.id + "' is incompatible; propertyClasses don't match" }); + return false; + } + private substituteDT(prj: SpecIF, r: DataType, n: DataType,): void { + // For all propertyClasses, substitute new by the original dataType: + this.substituteProp(prj.propertyClasses, 'dataType', r.id, n.id); + } + private substitutePC(prj: SpecIF, r: ResourceClass, n: ResourceClass, ): void { + // For all resourceClasses, substitute new by the original propertyClass: + this.substituteLe(prj.resourceClasses, 'propertyClasses', r.id, n.id); + // Also substitute the resource properties' class: + prj.resources.forEach((res) => { + this.substituteProp(res.properties, 'class', r.id, n.id); + }); + // The same with the statementClasses: + this.substituteLe(prj.statementClasses, 'propertyClasses', r.id, n.id); + if (Array.isArray(prj.statements)) + prj.statements.forEach((sta) => { + this.substituteProp(sta.properties, 'class', r.id, n.id) + }); + } + private substituteRC(prj: SpecIF, r: ResourceClass, n: ResourceClass): void { + // Substitute new by original resourceClass: + this.substituteLe(prj.statementClasses, 'subjectClasses', r.id, n.id); + this.substituteLe(prj.statementClasses, 'objectClasses', r.id, n.id); + this.substituteProp(prj.resources, 'class', r.id, n.id); + } + private substituteSC(prj: SpecIF, r: StatementClass, n: StatementClass): void { + // Substitute new by original statementClass: + this.substituteProp(prj.statements, 'class', r.id, n.id); + } + private substituteR(prj: SpecIF, r: Resource, n: Resource, opts?: any): void { + // Substitute resource n by r in all references of n, + // where r is always an element of this.data. + // But: Rescue any property of n, if undefined for r. +// console.debug('substituteR',r,n,prj.statements); + + if (opts && opts.rescueProperties) { + // Rescue any property value of n, + // if the corresponding property of the adopted resource r is undefined or empty; + // looking at the property types, which ones are in common: + n.properties.forEach((nP) => { + if (Lib.hasContent(nP.value)) { + // check whether existing resource has similar property; + // a property is similar, if it has the same title, + // where the title may be defined with the property class. + let pT = propTitleOf(nP, prj), + rP = propByTitle(r, pT, this.data); +// console.debug('substituteR 3a',nP,pT,rP,Lib.hasContent(valByTitle( r, pT, this.data ))); + if (!Lib.hasContent(valByTitle(r, pT, this.data)) + // dataTypes must be compatible: + && this.compatibleDT(dataTypeOf(this.data, rP['class']), dataTypeOf(prj, nP['class']))) { + // && this.typeIsCompatible( 'dataType', dataTypeOf(this.data,rP['class']), dataTypeOf(prj,nP['class']) ).status==0 ) { + rP.value = nP.value; + }; + }; + }); + }; + // In the rare case that the ids are identical, there is no need to update the references: + if (r.id == n.id) return; + + // 1. Memorize the replaced id, if not yet listed: + if (!Array.isArray(r.alternativeIds)) r.alternativeIds = []; + Lib.cacheE(r.alternativeIds, n.id); - if (typeof (opts) != 'object') opts = {}; - let sCL: StatementClass[]; - return new Promise( - (resolve, reject) => { - self.readContent('statementClass', 'all') - .then( - (sCs: StatementClass[]) => { - sCL = sCs; - return self.readContent('statement', 'all'); - } - ) - .then( - (sL:Statement[]) => { - // make a list of shows statements for all diagrams shown in the hierarchy: - let showsL = sL.filter((s) => { return staClassTitleOf(s) == CONFIG.staClassShows && isReferencedByHierarchy(itemIdOf(s.subject))}); - // filter all statements involving res as subject or object: - resolve( - sL.filter( - (s) => { - let sC: StatementClass = itemById(sCL,s['class'] as string); - return (res.id == itemIdOf(s.subject) || res.id == itemIdOf(s.object)) - // statement must be visible on a diagram referenced in a hierarchy - // or be a shows statement itself. - // ToDo: - Some Archimate relations are implicit (not shown on a diagram) and are unduly suppressed, here) - && (opts.dontCheckStatementVisibility - // Accept manually created relations (including those imported via Excel): - || !sC.instantiation || sC.instantiation.indexOf(Instantiation.User)>-1 - || indexBy(showsL, "object", s.id) > -1 - || titleOf(sC) == CONFIG.staClassShows) - // AND fulfill certain conditions: - && ( - // related subject and object must be referenced in the tree to be navigable, - // also, the statement must not be declared 'hidden': - !opts.showComments - // cheap tests first: - && titleOf(sC) != CONFIG.staClassCommentRefersTo - && CONFIG.hiddenStatements.indexOf(s.title) < 0 - && isReferencedByHierarchy(itemIdOf(s.subject)) - && isReferencedByHierarchy(itemIdOf(s.object)) - // In case of a comment, the comment itself is not referenced in the tree: - || opts.showComments - && titleOf(sC) == CONFIG.staClassCommentRefersTo - && isReferencedByHierarchy(itemIdOf(s.object)) - ) - } - ) - ); - } - ) - .catch( reject ); - } + // 2. Replace the references in all statements: + prj.statements.forEach((st: Statement) => { + if (this.equalKey(st.object, n)) { if (st.object.id) { st.object.id = Lib.itemIdOf(r) } else { st.object = Lib.itemIdOf(r) } }; + if (this.equalKey(st.subject, n)) { if (st.subject.id) { st.subject.id = Lib.itemIdOf(r) } else { st.subject = Lib.itemIdOf(r) } } + // ToDo: Is the substitution is too simple, if a key is used? + }); + + // 3. Replace the references in all hierarchies: + this.substituteRef(prj.hierarchies, r.id, n.id); + + // 4. Make sure all statementClasses allowing n.class also allow r.class (the class of the adopted resource): + prj.statementClasses.forEach((sC: StatementClass) => { + if (Array.isArray(sC.subjectClasses) && sC.subjectClasses.indexOf(n['class']) > -1) Lib.cacheE(sC.subjectClasses, r['class']); + if (Array.isArray(sC.objectClasses) && sC.objectClasses.indexOf(n['class']) > -1) Lib.cacheE(sC.objectClasses, r['class']); + }); + } + private substituteProp(L, propN: string, rAV: string, dAV: string): void { + // replace ids of the duplicate item by the id of the original one; + // this applies to the property 'propN' of each member of the list L: + if (Array.isArray(L)) + L.forEach((e) => { if (e[propN] == dAV) e[propN] = rAV }); + } + private substituteLe(L, propN: string, rAV: string, dAV: string): void { + // Replace the duplicate id by the id of the original item; + // so replace dAV by rAV in the list named 'propN' + // (for example: in L[i][propN] (which is a list as well), replace dAV by rAV): + let idx: number; + if (Array.isArray(L)) + L.forEach((e) => { + // e is a resourceClass or statementClass: + if (Array.isArray(e[propN])) { + idx = e[propN].indexOf(dAV); + if (idx > -1) { + // dAV is an element of e[propN] + // - replace dAV with rAV + // - in case rAV is already member of the list, just remove dAV + if (e[propN].indexOf(rAV) > -1) + e[propN].splice(idx, 1) + else + e[propN].splice(idx, 1, rAV); + }; + }; + }); + } + private substituteRef(L, rId: string, dId: string): void { + // For all hierarchies, replace any reference to dId by rId; + // eliminate double entries in the same folder (together with the children): + Lib.iterateNodes( + L, + // replace resource id: + (nd) => { if (nd.resource == dId) { nd.resource = rId }; return true }, + // eliminate duplicates within a folder (assuming that it will not make sense to show the same resource twice in a folder; + // for example it is avoided that the same diagram is shown twice if it has been imported twice: + (ndL) => { for (var i = ndL.length - 1; i > 0; i--) { if (indexBy(ndL.slice(0, i), 'resource', ndL[i].resource) > -1) { ndL.splice(i, 1) } } } ); + // ToDo: Make it work, if keys are used as a reference. + } + abort(): void { + console.info('abort specif'); + // server.abort(); + this.abortFlag = true; }; +} +/*/////////////////////////////////////////////////////// +interface IProject { + hierarchies: SpecifNode[]; // listed specifications (aka hierarchies, outlines) of the project. +// exp: boolean; + exporting: boolean; + abortFlag: boolean; + init: Function; + isLoaded: Function; + create: Function; + update: Function; + createContent: Function; + readContent: Function; + updateContent: Function; + deleteContent: Function; + createResource: Function; + readStatementsOf: Function; + createFolderWithResourcesByType: Function; + createFolderWithGlossary: Function; + deduplicate: Function; + exportFormatClicked: Function; + exportOptionsClicked: Function; + chooseExportOptions: Function; + chooseFormatAndExport: Function; + exportAs: Function; + abort: Function; +} + * This funtion formerly had the role of CProject, there are perhaps some algorithms which can be used again in future: +function Project(): IProject { + // Constructor for a project containing SpecIF data. + var self: any = {}, + // loading = false, // true: data is being gathered from the server. + fileName: string; - // Select format and options with a modal dialog, then export the data: - self.exportFormatClicked = ():void =>{ - // Display options depending on selected format: - // In case of ReqIF OOXML and ePub, let the user choose the language, if there are more than one: - document.getElementById( "expOptions" ).innerHTML = self.exportOptions( radioValue( i18n.LblFormat ) ); + self.updateMeta = ( prj )=>{ + if( !prj ) return; + // update only the provided properties: + for( var p in prj ) self[p] = prj[p]; + // Update the meta-data (header): + // return server.project(self).update() + }; + self.read = ( prj, opts )=>{ + // Assemble the data of the project from all documents in a document database: + switch( typeof(opts) ) { + case 'boolean': + // for backward compatibility: + opts = {reload: opts, loadAllSpecs: false, loadObjects: false, loadRelations: false}; + break; + case 'object': + // normal case (as designed): + // if( typeof opts.reload!='boolean' ) opts.reload = false; + break; + default: + opts = {reload: false} + }; +// console.debug( 'cache.read', opts, self.data.id, prj ); + + var pDO = $.Deferred(); + // Read from cache in certain cases: + if( self.data.isLoaded && !opts.reload && ( !prj || prj.id==self.data.id ) ) { + // return the loaded project: + pDO.resolve( self ); + return pDO + }; + // else + return null + }; + // var updateModes = ["adopt","match","extend","ignore"]; + self.update = (newD, opts:any): JQueryDeferred => { +// console.debug('update',newD,opts); + // Use jQuery instead of ECMA Promises for the time being, because of progress notification. + var uDO = $.Deferred(); -// console.debug('exportFormatClicked',radioValue( i18n.LblFormat )); - }; - self.exportOptionsClicked = ():void =>{ - // Obtain selected options: - // add 'zero with space' (​) to make the label = div-id unique: - self.data.title = textValue( '​'+i18n.LblProjectName ); - fileName = textValue( '​'+i18n.LblFileName ); + newD = new CSpecIF(newD); // transform to internal data structure -// console.debug('exportOptionsClicked',self.data.title,fileName); - }; - self.exportOptions = (fmt)=>{ - const exportOptionsClicked = 'app.cache.selectedProject.exportOptionsClicked()'; - var pnl = '
' - // + "

"+i18n.LblOptions+"

" - // add 'zero with space' (​) to make the label = div-id unique: - + textField( '​'+i18n.LblProjectName, self.data.title, {typ:'line', handle:exportOptionsClicked} ) - + textField( '​'+i18n.LblFileName, self.data.title, {typ:'line', handle:exportOptionsClicked} ); - switch( fmt ) { - case 'epub': - case 'oxml': - pnl += checkboxField( - // i18n.LblOptions, - i18n.modelElements, - [ - { title: i18n.withOtherProperties, id: 'withOtherProperties', checked: false }, - { title: i18n.withStatements, id: 'withStatements', checked: false } - ], - { handle: exportOptionsClicked } - ); + switch( opts.mode ) { + case 'update': + // updateWithLastChanged( newD, opts ); + // break; + case 'adopt': + adopt(newD, opts); + break; + default: + uDO.reject({status:999,statusText:'Invalid update mode specified'}); }; - pnl += '
'; -// console.debug('exportOptions',fmt,pnl); - return pnl; - }; - self.chooseFormatAndExport = ()=>{ - if( self.exporting ) return; + return uDO; - const exportFormatClicked = 'app.cache.selectedProject.exportFormatClicked()'; - // @ts-ignore - BootstrapDialog() is loaded at runtime - new BootstrapDialog({ - title: i18n.LblExport+": '"+self.data.title+"'", - type: 'type-primary', - /* // @ts-ignore - BootstrapDialog() is loaded at runtime - size: BootstrapDialog.SIZE_WIDE, */ - message: ()=>{ - var form = '
' - // + '
' - + '
' - + '
' - // + "

"+i18n.LblFormat+"

" - + "

"+i18n.MsgExport+"

" - + radioField( - i18n.LblFormat, - [ - { title: 'SpecIF v'+CONFIG.specifVersion, id: 'specif', checked: true }, - { title: 'HTML with embedded SpecIF v'+CONFIG.specifVersion, id: 'html' }, - { title: 'ReqIF v1.2', id: 'reqif' }, - // { title: 'RDF', id: 'rdf' }, - { title: 'Turtle (experimental)', id: 'turtle' }, - { title: 'ePub v2', id: 'epub' }, - { title: 'MS WORD® (Open XML)', id: 'oxml' } - ], - {handle:exportFormatClicked} // options depend on format - ) - + '
' - + '
' - // + '
' - + '
' - + self.exportOptions( 'specif' ) // parameter must correspond to the checked option above - + '
' - + '
'; - return $( form ) - }, - buttons: [ - { label: i18n.BtnCancel, - action: (thisDlg)=>{ - thisDlg.close() - } - }, - { label: i18n.BtnExport, - cssClass: 'btn-success', - action: (thisDlg)=>{ - // Get index of option: - app.busy.set(); - message.show( i18n.MsgBrowserSaving, {severity:'success', duration:CONFIG.messageDisplayTimeShort} ); -// console.debug('options',checkboxValues( i18n.LblOptions )); - let options = { - format: radioValue( i18n.LblFormat ), - fileName: fileName - }; - // further options according to the checkboxes: - checkboxValues( i18n.modelElements ).forEach( - (op)=>{ - options[op] = true - } - ); - self.exportAs( options ) - .then( - // app.busy.reset, --> doesn't work for some reason, 'this' within reset() is undefined ... - function() {app.busy.reset()}, - handleError - ); - thisDlg.close() - } - } - ] - }) - .open(); + /* function updateWithLastChanged( nD, opts ) { + console.debug('update project',nD,opts); + // Update a loaded project with data of the new: + // - Types with the same id must be compatible + // - New types will be added + // - Instances with newer changedAt replace older ones + // - Both the id and alternativeIds are used to associate existing and new instances - // --- - function handleError(xhr: xhrMessage): void { - self.exporting = false; - app.busy.reset(); - message.show( xhr ); + // In a first pass check, if there is any incompatible type making an update impossible: + rc = classesAreCompatible('dataType',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + rc = classesAreCompatible('propertyClass',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + rc = classesAreCompatible('resourceClass',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + rc = classesAreCompatible('statementClass',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + console.info("All existing types are compatible with '"+newD.title+"'"); } - }; - self.exportAs = (opts?: any): Promise => { - if (!opts) opts = {}; - if (!opts.format) opts.format = 'specif'; - // in certain cases, try to export files with the same name in PNG format, as well. - // - ole: often, preview images are supplied in PNG format; - // - svg: for generation of DOC or ePub, equivalent images in PNG-format are needed. - // if( typeof(opts.preferPng)!='boolean' ) opts.preferPng = true; ... is the default - // if( !opts.alternatePngFor ) opts.alternatePngFor = ['svg','ole']; ... not yet supported - return new Promise((resolve, reject) => { + // newD is new data in 'internal' data structure + // add new elements + // update elements with the same id + // exception: since types cannot be updated, return with error in case newD contains incompatible types + // There are four modes with respect to the types: + // - "match": if a type in newD with the same id is already present and it differs, quit with error-code. + // This is the minimum condition and true for all of the following modes, as well. + // - "deduplicate": if an identical type in newD with a different id is found, take the existing one + // and update the instances of the suppressed class. + // - "extend": in addition to "deduplicate", combine similar types. E.g. combine integer types and take the overall value range + // or add additional propertyClasses to a resourceClass. + // - "ignore": new propertyClasses and all their instances are ignored + mode = mode || 'deduplicate'; +// console.debug('cache.update',newD,mode); + var rc = {}, + uDO = $.Deferred(); + // newD = self.set( newD ); // transform to internal data structure + if( !newD ) { + uDO.reject({ + status: 995, + statusText: i18n.MsgImportFailed + }); + return uDO + }; + + // In a first pass check, if there is any incompatible type making an update impossible: + rc = classesAreCompatible('dataType',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + rc = classesAreCompatible('resourceClass',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + rc = classesAreCompatible('statementClass',mode); + if( rc.status>0 ) { + uDO.reject( rc ); + return uDO + }; + console.info("All existing types are compatible with '"+newD.title+"'"); + + // In a second pass, start with creating any type which does not yet exist. + // Start with the datatypes; the next steps will be chained by function updateNext: + var pend=0; + addNewTypes('dataType'); - if (self.exporting) { - // prohibit multiple entry - reject({ status: 999, statusText: "Export in process, please wait a little while" }); - } - else { - if (self.data.exp) { // check permission - self.exporting = true; // set status to prohibit multiple entry + return uDO - switch (opts.format) { - // case 'rdf': - case 'turtle': - case 'reqif': - case 'html': - case 'specif': - storeAs(opts); - break; - case 'epub': - case 'oxml': - publish(opts); - }; + function classesAreCompatible( ctg:string, mode ) { + let tL = standardTypes.listName.get(ctg), + aL = self.data[tL], + nL = newD[tL]; + // true, if every element in nL is compatibly present in aL or if it can be added: + let j:number, rC; + for( var i=nL.length-1;i>-1;i-- ) { + for( j=aL.length-1;j>-1;j-- ) { +// console.debug('classesAreCompatible',aL[j],nL[i]); + // if a single element is incompatible the lists are incompatible: + rC = typeIsCompatible(ctg,aL[j],nL[i],mode); + // on first error occurring, quit with return code: + if( rC.status>0 ) return rC } - else { - reject({ status: 999, statusText: "No permission to export" }); - }; }; - return; - - function transform2image(dta, fn: Function): void { - let pend = 0, - re = /]+?)(\/>|>)/, - reT = /type="[^"]+"/, - replaced = false; - if (Array.isArray(dta.files)) - // Transform any special file format to an image: - dta.files.forEach((f, i, L) => { - let nFileName = f.title.fileName() + '.svg', - reD = new RegExp('data="' + f.title + '"'); - switch (f.type) { - case 'application/bpmn+xml': - pend++; - - // Replace the file reference names and types: - dta.resources.forEach((res) => { - if (Array.isArray(res.properties)) - res.properties.forEach((prp) => { - prp.value = prp.value.replace(re, ($0, $1, $2) => { -// console.debug('#a', $0, $1, $2); - replaced = false; - if ($1) $1 = $1.replace(reD, ($4) => { -// console.debug('#b', $4, nFileName); - replaced = true; - return 'data="' + nFileName + '"' - }); - if (replaced) $1 = $1.replace(reT, () => { - return 'type="image/svg+xml"' - }); - return ' { - bpmn2svg(b) - .then( - (result) => { - // replace BPMN by SVG: - L.splice(i, 1, { - // blob: new Blob([result.svg], { type: "image/svg+xml; charset=utf-8" }), - blob: new Blob([result.svg], { type: "image/svg+xml" }), - id: 'F-' + simpleHash(f.title), - title: nFileName, - type: 'image/svg+xml', - changedAt: f.changedAt - }); -// console.debug('SVG',result.svg,L); - if (--pend < 1) - // Finally publish in the desired format: - fn(); - }, - (err) => { - console.error('BPMN-Viewer could not deliver SVG', err); - reject(); - } - ); - }, 0); - }; - }); - // In case there is nothing to transform, we start right away: - if (pend < 1) - // publish in the desired format: - fn(); + return {status:0} + } + function updateNext(ctg:string) { + // chains the updating of types and elements in asynchronous operation: + console.info('Finished updating:',ctg); + // having finished with elements of category 'ctg', start next step: + switch( ctg ) { + case 'dataType': addNewTypes( 'resourceClass' ); break; + case 'resourceClass': addNewTypes( 'statementClass' ); break; + case 'statementClass': updateIfChanged( 'file' ); break; + case 'file': updateIfChanged( 'resource' ); break; + case 'resource': updateIfChanged( 'statement' ); break; + case 'statement': updateIfChanged( 'hierarchy' ); break; + case 'hierarchy': + uDO.notify(i18n.MsgProjectUpdated,100); + console.info('Project successfully updated'); + uDO.resolve(); + break; + default: uDO.reject() //should never arrive here } - function publish(opts: any): void { - if (!opts || ['epub', 'oxml'].indexOf(opts.format) < 0) { - // programming error! - reject(); - return; - }; + } + function addNewTypes( ctg:string ) { + // Is commonly used for resource and statement classes with their propertyClasses. + let rL, nL, rT; + switch( ctg ) { + case 'dataType': rL = self.data.dataTypes; nL = newD.dataTypes; break; + case 'resourceClass': rL = self.data.resourceClasses; nL = newD.resourceClasses; break; + case 'statementClass': rL = self.data.statementClasses; nL = newD.statementClasses; break; + default: return null //should never arrive here + }; + nL.forEach( (nT)=>{ + rT = itemById(rL,nT.id); + if( rT ) { + // a type with the same id exists. + // ToDo: Add a new enum value to an existing enum dataType (server does not allow it yet) - // ToDo: Get the newest data from the server. - // console.debug( "publish", opts ); + // Add a new property class to an existing type: + switch( mode ) { + case 'match': + // Reference and new data DO match (as checked, before) + // ... so nothing needs to be done, here. + // no break + case 'ignore': + // later on, only properties for which the user has update permission will be considered, + // ... so nothing needs to be done here, either. + break; + case 'extend': + // add all missing propertyClasses: + // ToDo: Is it possible that the user does not have read permission for a property class ?? + // Then, if it is tried to create the supposedly missing property class, an error occurs. + // But currently all *types* are visible for everybody, so there is no problem. + if( nT.propertyClasses && nT.propertyClasses.length>0 ) { + // must create missing propertyClasses one by one in ascending sequence, + // because a newly added property class can be specified as predecessor: + addNewPC( rT, nT.propertyClasses, 0 ) + } + } + } else { + // else: the type does not exist and will be created, therefore: + pend++; + console.info('Creating type',nT.title); + self.createContent(nT.category,nT) + .done(()=>{ + if( --pend<1 ) updateNext( ctg ) + }) + .fail( uDO.reject ) + } + }); + // if no type needs to be created, continue with the next: + if(pend<1) updateNext( ctg ); + return - // If a hidden property is defined with value, it is suppressed only if it has this value; - // if the value is undefined, the property is suppressed in all cases. - opts.hiddenProperties = [ - { title: CONFIG.propClassType, value: CONFIG.resClassFolder }, - { title: CONFIG.propClassType, value: CONFIG.resClassOutline } - ]; + function addNewPC( r, nPCs, idx ) { + // r: existing (=reference) type with its propertyClasses + // nPCs: new list of propertyClasses + // idx: current index of nPCs + if( nPCs[idx].id?itemById( r.propertyClasses, nPCs[idx].id ):itemByName( r.propertyClasses, nPCs[idx].title ) ) { + // not missing, so try next: + if( ++idx0 ) + nPCs[idx].predecessor = nPCs[idx-1].id; - let data = self.data.toExt(opts), - localOpts = { - // Values of declared stereotypeProperties get enclosed by double-angle quotation mark '«' and '»' - titleProperties: CONFIG.titleProperties.concat(CONFIG.headingProperties), - descriptionProperties: CONFIG.descProperties, - stereotypeProperties: CONFIG.stereotypeProperties, - lookup: i18n.lookup, - showEmptyProperties: CONFIG.showEmptyProperties, - imgExtensions: CONFIG.imgExtensions, - applExtensions: CONFIG.applExtensions, - // hasContent: hasContent, - propertiesLabel: opts.withOtherProperties ? 'SpecIF:Properties' : undefined, - statementsLabel: opts.withStatements ? 'SpecIF:Statements' : undefined, - fileName: opts.fileName, - colorAccent1: '0071B9', // adesso blue - done: () => { app.cache.selectedProject.exporting = false; resolve() }, - fail: (xhr) => { app.cache.selectedProject.exporting = false; reject(xhr) } - }; + // add the new property class also to r: + let p = indexById( r.propertyClasses, nPCs[idx].predecessor ); + console.info('Creating property class', nPCs[idx].title); + // insert at the position similarly to the new type; + // if p==-1, then it will be inserted at the first position: + r.propertyClasses.splice( p+1, 0, nPCs[idx] ); + server.project({id:self.data.id}).allClasses({id:r.id}).class(nPCs[idx]).create() + .done( ()=>{ + // Type creation must be completed before starting to update the elements: + if( ++idx0 ) + self.updateContent(ctg,newD.files) + .done( ()=>{ + // Wait for all files to be loaded, so that resources will have higher revision numbers: + newD.files = []; + updateNext(ctg) + }) + .fail( uDO.reject ) + else + updateNext(ctg); + return; + case 'resource': itemL = newD.resources; uDO.notify(i18n.MsgLoadingObjects,50); break; + case 'statement': itemL = newD.statements; uDO.notify(i18n.MsgLoadingRelations,70); break; + case 'hierarchy': itemL = newD.hierarchies; uDO.notify(i18n.MsgLoadingHierarchies,80); break; + default: return null //should never arrive here + }; + itemL.forEach( (itm)=>{ + updateInstanceIfChanged(ctg,itm) + }); + // if list is empty, continue directly with the next item type: + if(pend<1) updateNext( ctg ) + return - transform2image( data, - () => { - switch (opts.format) { - case 'epub': - // @ts-ignore - toEpub() is loaded at runtime - toEpub(data, localOpts); + function contentChanged(ctg:string, r, n) { // ref and new resources +// console.debug('contentChanged',ctg, r, n); + // Is commonly used for resource, statement and hierarchy instances. + if( r['class']!=n['class'] ) return null; // fatal error, they must be equal! + + // Continue in case of resources and statements: + let i=null, rA=null, nA=null, rV=null, nV=null; + // 1) Are the property values equal? + // Skipped, if the new instance does not have any property (list is empty or not present). + // Statements and hierarchies often have no properties. + // Resources without properties are useless, as they do not carry any user payload (information). + // Note that the actual property list delivered by the server depends on the read privilege of the user. + // Only the properties, for which the current user has update privilege, will be compared. + // Use case: Update diagrams with model-elements only: + // Create a user with update privileges for resourceClass 'diagram' + // and property class 'title' of resourceClass 'model-element'. + // Then, only the diagrams and the title of the model-elements will be updated. + if( n.properties && n.properties.length>0 ) { + for( i=(r.properties?r.properties.length:0)-1;i>-1;i--) { + rA = r.properties[i]; +// console.debug( 'update?', r, n); + // no update, if the current user has no privilege: + if( !rA.upd ) continue; + // look for the corresponding property: + nA = itemBy( n.properties, 'class', rA['class'] ); + // no update, if there is no corresponding property in the new data: + if( !nA ) continue; + // in all other cases compare the value: + let oT = itemById( app.cache.selectedProject.data.resourceClasses, n['class'] ), // applies to both r and n + rDT = dataTypeOf( app.cache.selectedProject.data, rA['class'] ), + nDT = dataTypeOf( newD, nA['class'] ); + if( rDT.type!=nDT.type ) return null; // fatal error, they must be equal! + switch( nDT.type ) { + case 'xs:enumeration': + // value has a comma-separated list of value-IDs, + rV = enumValueOf(rDT,rA); + nV = enumValueOf(nDT,nA); +// console.debug('contentChanged','ENUM',rA,nA,rV!=nV); + if( rV!=nV ) return true; + break; + case 'xhtml': + // rV = toHex(stripCtrl(rA.value).reduceWhiteSpace()); + // nV = toHex(stripCtrl(fileRef.toServer(nA.value)).reduceWhiteSpace()); + // rV = stripCtrl(rA.value).reduceWhiteSpace(); + rV = rA.value; + // apply the same transformation to nV which has been applied to rV before storing: + // nV = stripCtrl(fileRef.toServer(nA.value)).reduceWhiteSpace(); + // nV = fileRef.toServer(nA.value); + nV = nA.value; +// console.debug('contentChanged','xhtml',rA,nA,rV!=nV); + if( rV!=nV ) return true; + // If a file is referenced, pretend that the resource has changed. + // Note that a resource always references a file having the next lower revision number than istself. + // It is possible that a file has been updated, so a referencing resource must be updated, as well. + // ToDo: Analyse whether a referenced file has really been updated. + if( RE.tagNestedObjects.test(nV) + || RE.tagSingleObject.test(nV) ) return true; break; - case 'oxml': - // @ts-ignore - toOxml() is loaded at runtime - toOxml(data, localOpts); - }; - // resolve() is called in the call-backs defined by opts + default: + if( rA.value!=nA.value ) return true + } } - ); - } - function storeAs(opts: any): void { - if (!opts || ['specif', 'html', 'reqif', 'turtle'].indexOf(opts.format) < 0) - // programming error! - throw Error("Invalid format specified on export"); - - // ToDo: Get the newest data from the server. -// console.debug( "storeAs", opts ); - - opts.allResources = false; // only resources referenced by a hierarchy. - // keep vocabulary terms: - opts.lookupTitles = false; - opts.lookupValues = false; - - switch (opts.format) { - case 'specif': - opts.allResources = true; // even, if not referenced by a hierarchy. - // no break - case 'html': - // export all languages: - opts.targetLanguage = undefined; - // keep all revisions: - opts.revisionDate = undefined; - break; - // case 'rdf': - case 'turtle': - case 'reqif': - // only single language is supported: - if (typeof (opts.targetLanguage) != 'string') opts.targetLanguage = browser.language; - // XHTML is supported: - opts.makeHTML = true; - opts.linkifyURLs = true; - opts.createHierarchyRootIfNotPresent = true; - // take newest revision: - opts.revisionDate = new Date().toISOString(); - break; - default: - reject(); - return; // should never arrive here }; - // console.debug( "storeAs", opts ); - let data = self.data.toExt(opts), - fName = opts.fileName || data.title; - - // A) Processing for 'html': - if (opts.format == 'html') { - // find the fully qualified path of the content delivery server to fetch the viewer modules: - opts.cdn = window.location.href.substr(0, window.location.href.lastIndexOf("/") + 1); - - transform2image( data, - () => { - toHtmlDoc(data, opts) - .then( - function (dta): void { - let blob = new Blob([dta], { type: "text/html; charset=utf-8" }); - // let blob = new Blob([dta], {type: "application/xhtml+xml; charset=utf-8"}); - // @ts-ignore - saveAs() is loaded at runtime - saveAs(blob, fName + '.specif.html'); - self.exporting = false; - resolve(); - } - ) - .catch( - function (xhr): void { - self.exporting = false; - reject(xhr); + // 2) Statements must have equal subjectClasses and objectClasses - with equal revisions? + if( ctg == 'statement' ) { + // if( n.subject.id!=r.subject.id || n.subject.revision!=r.subject.revision) return true; + // if( n.object.id!=r.object.id || n.object.revision!=r.object.revision) return true; + if( n.subject.id!=r.subject.id + || n.object.id!=r.object.id ) return true + }; + return false // ref and new are the same + } + function updateInstanceIfChanged(ctg:string,nI) { + // Update an element/item of the specified category, if changed. + pend++; + self.readContent(ctg,nI,true) // reload from the server to obtain most recent data + .done( (rI)=>{ + // compare actual and new item: +// console.debug('updateInstanceIfChanged',ctg,rI,nI); + // ToDo: Detect parallel changes and merge interactively ... + if( Date.parse(rI.changedAt){ + // in case the nI.properties are supplied in a different order: + nA = itemBy(nI.properties,'class',rA['class']); + if( nA ) { + nA.upd = rA.upd; + nA.del = rA.del } - ); + }); + console.info('Updating instance',nI.title); + // ToDo: Test whether only supplied properties are updated by the server; otherwise implement the behavior, here. + self.updateContent( ctg, nI ) + .done( updateTreeIfChanged( ctg, rI, nI ) ) // update the tree, if necessary. + .fail( uDO.reject ) + } else { + // no change, so continue directly: + updateTreeIfChanged( ctg, rI, nI ) // update the tree, if necessary. } - ); - return; - }; + }) + .fail( (xhr)=Y{ + switch( xhr.status ) { + case 403: + // This is a hack to circumvent a server limitation. + // In case the user is not admin, the server delivers 403, if a resource does not exist, + // whereas it delivers 404, if it is an admin. + // Thus: If 403 is delivered and the user has read access according to the resourceClass, + // do as if 404 had been delivered. + var pT = itemById(app.cache.selectedProject.data.allClasses,nI['class']); +// console.debug('403 instead of 404',nI,pT); + if( !pT.rea || !pT.cre ) { uDO.reject(xhr); return }; + // else the server should have delivered 404, so go on ... + case 404: +// console.debug('not found',xhr.status); + // no item with this id, so create a new one: + self.createContent(ctg,nI) + .done(()=>{ + if( --pend<1 ) updateNext( ctg ) + }) + .fail( uDO.reject ) + break; + default: + uDO.reject(xhr) + } + }) + } + function updateTreeIfChanged( ctg:string, aI, nI ) { + // Update all children (nodes) of a hierarchy root. + // This is a brute force solution, since any mismatch causes an update of the whole tree. + // ToDo: Add or delete a single child as required. + // ToDo: Update the smallest possible subtree in case addition or deletion of a single child is not sufficient. - // B) Processing for all formats except 'html': - // @ts-ignore - JSZip() is loaded at runtime - let zip = new JSZip(), - zName: string, - mimetype = "application/zip"; - - // Add the files to the ZIP container: - if (data.files) - data.files.forEach((f) => { - // console.debug('zip a file',f); - zip.file(f.title, f.blob); - delete f.blob; // the SpecIF data below shall not contain it ... - }); + function newIds(h) { + // new and updated hierarchy entries must have a new id (server does not support revisions for hierarchies): + h.children.forEach( (ch)=>{ + ch.id = Lib.genID('N-'); + newIds(ch) + }) + } + function treeChanged(a,n) { + // Equal hierarchies? + // All children (nodes in SpecIF terms) on all levels must have the same sequence. + return nodesChanged(a.children,n.children) - // Prepare the output data: - switch (opts.format) { - case 'specif': - fName += ".specif"; - zName = fName + '.zip'; - data = JSON.stringify(data); - break; - case 'reqif': - fName += ".reqif"; - zName = fName + 'z'; - mimetype = "application/reqif+zip"; - data = app.ioReqif.toReqif(data); - break; - case 'turtle': - fName += ".ttl"; - zName = fName + '.zip'; - // @ts-ignore - transformSpecifToTTL() is loaded at runtime - data = transformSpecifToTTL("https://specif.de/examples", data); - /* break; - case 'rdf': - if( !app.ioRdf ) { - reject({status:999,statusText:"ioRdf not loaded."}); - return; + function nodesChanged(aL,nL) { +// console.debug( 'nodesChanged',aL,nL ) + if( (!aL || aL.length<1) && (!nL || nL.length<1) ) return false; // no update needed + if( aL.length!=nL.length ) return true; // update! + for( let i=nL.length-1; i>-1; i-- ) { + // compare the references only, as the hierarchy ids can change: + if( !aL[i] || aL[i].ref!=nL[i].ref ) return true; + if( nodesChanged(aL[i].children,nL[i].children) ) return true }; - fName += ".rdf"; - data = app.ioRdf.toRdf( data ); */ - }; - let blob = new Blob([data], { type: "text/plain; charset=utf-8" }); - // Add the project: - zip.file(fName, blob); - blob = undefined; // free heap space - - // done, store the specif.zip: - zip.generateAsync({ - type: "blob", - compression: "DEFLATE", - compressionOptions: { level: 7 }, - mimeType: mimetype - }) - .then( - (blob: Blob) => { - // successfully generated: - // console.debug("storing ZIP of '"+fName+"'."); - // @ts-ignore - saveAs() is loaded at runtime - saveAs(blob, zName); - self.exporting = false; - resolve(); - }, - (xhr: xhrMessage) => { - // an error has occurred: - console.error("Cannot create ZIP of '" + fName + "'."); - self.exporting = false; - reject(xhr); + return false } - ); + } + + // Note: 'updateTreeIfChanged' is called for instance of ALL types, even though only a hierarchy has children. + // In case of a resource or statement, the tree operations are skipped: + if( ctg == 'hierarchy' && treeChanged(aI,nI) ) { + message.show( i18n.MsgOutlineAdded, {severity:'info', duration:CONFIG.messageDisplayTimeShort} ); + // self.deleteContent('hierarchy',aI.children); // can be be prohibited by removing the permission, but it is easily forgotten to change the role ... + newIds(nI); + server.project(app.cache.selectedProject.data).specification(nI).createChildren() + .done( ()=>{ + if( --pend<1 ) updateNext( ctg ) + }) + .fail( uDO.reject ) + } else { + // no hierarchy (tree) has been changed, so no update: + if( --pend<1 ) updateNext( ctg ) + } } - }); - }; - self.abort = ():void =>{ - console.info('abort specif'); - // server.abort(); - self.abortFlag = true; + } }; + self.init(); return self; ////////// @@ -2474,7 +2656,7 @@ function Project(): IProject { // get all resources of the specified type: qu is {type: class} // if( !reload ) { // collect all resources with the queried type: - var oL = forAll( self.data.resources, (o)=>{ return o['class']==qu.type?o:null } ), + var oL = Lib.forAll( self.data.resources, (o)=>{ return o['class']==qu.type?o:null } ), dO = $.Deferred(); dO.resolve( oL ); return dO @@ -2503,7 +2685,7 @@ function Project(): IProject { .done( (rsp)=>{ // continue caching, if the project hasn't been left, meanwhile: if( sp ) { // sp is null, if the project has been left. - cacheL( self.data.resources, rsp ); + Lib.cacheL( self.data.resources, rsp ); if( cI { + // Construct a representative of the selected project with cached data: + // ToDo: enforce CONFIG.maxItemsToCache + + /* var autoLoadId, // max 1 autoLoad chain + autoLoadCb; // callback function when the cache has been updated */ + + // initialization is at the end of this constructor. + self.init = (): boolean => { + // initialize/clear all variables: + self.data = new CCache({cacheInstances:true}); + self.projects = []; + self.selectedProject = undefined; + + /* autoLoadId = undefined; // stop any autoLoad chain + autoLoadCb = undefined; */ + + return true + }; + self.create = (prj: SpecIF, opts: any): JQueryDeferred => { + // in this implementation, delete existing projects to save memory space: + self.projects.length = 0; + self.data.clear(); + // append a project to the list: + self.projects.push(new CProject(prj,self.data)); + // self.projects.push(Project()); + self.selectedProject = self.projects[self.projects.length - 1]; + return self.selectedProject.create(prj, opts); + }; + /* self.update = (prj:SpecIF, opts:any ) => { + if (!prj) return; + // search the project and select it: + ... + // update: + ... + }; + // Periodically update the selected project with the current server state in a multi-user context: + self.startAutoLoad = ( cb )=>{ + // if( !self.cacheInstances ) return; + // console.info( 'startAutoLoad' ); + if( typeof(cb)=="function" ) { autoLoadCb = cb }; + autoLoadId = Lib.genID( 'aU-' ); + // get all resources from the server to fill the cache: + setTimeout( ()=>{ autoLoad(autoLoadId) }, 600 ) // start a little later ... + }; + self.stopAutoLoad = ()=>{ + // console.info('stopAutoLoad'); + autoLoadId = null; + loading = false + }; + self.loadInstances = ( cb )=>{ + // for the time being - until the synchronizing will be implemented: + // if( !self.cacheInstances ) return; + // load the instances of the selected hierarchy (spec) into the cache (but not the types): + // console.debug( 'self.loadInstances', self.selectedHierarchy, cb ); + if( self.selectedHierarchy ) { + loading = true; + // update all resources referenced by the selectedHierarchy: + loadObjsOf( self.selectedHierarchy ) + .done( ()=>{ + // loadRelsOf( self.selectedHierarchy ); + // update the hierarchy (outline). + // it is done after the resources to reflect any change in the hierarchy made during the loading. + self.readContent( 'hierarchy', self.selectedHierarchy, true ) // true: reload + // - call cb to refresh the app: + .done( ()=>{ + if( typeof(cb)=="function" ) cb(); + loading = false + }) + .fail( (xhr)=>{ + loading = false + }) + }) + .fail( (xhr)=>{ + loading = false + }) + } + }; + self.load = (opts)=>{ + var lDO = $.Deferred(); + + // load referenced resources and statements ... + if( opts.loadObjects ) { + if( opts.loadAllSpecs ) + loadAll( 'resource' ) + .done( ()=>{ + if( opts.loadRelations ) + return loadAll( 'statement' ) + .done( lDO.resolve ) + .fail( lDO.reject ); + // else + lDO.resolve() + }) + .fail( lDo.reject ) + else + loadObjsOf( self.selectedHierarchy ) + .done( ()=>{ + if( opts.loadRelations ) + return loadRelsOf( self.selectedHierarchy ) + .done( lDO.resolve ) + .fail( lDO.reject ); + // else + lDO.resolve() + }) + .fail( lDo.reject ); + return + } else { + lDO.resolve() + }; + return lDO + }; + */ + return self; +}); ////////////////////////// // global helper functions: -function itemIdOf( key:KeyObject|string ):string { +Lib.itemIdOf = ( key:KeyObject|string ):string =>{ // Return the id of the referenced item; the key can be // - a string with the requested id // - an pbject with id and a revision @@ -2657,21 +2966,21 @@ function isReferencedByHierarchy(rId: string, H?: SpecifNode[]): boolean { // checks whether a resource is referenced by the hierarchy: // ToDo: make it work with revisions. if( !H ) H = app.cache.selectedProject.data.hierarchies; - return iterateNodes( H, (nd)=>{ return nd.resource!=rId } ) + return Lib.iterateNodes( H, (nd)=>{ return nd.resource!=rId } ) } function collectResourcesByHierarchy(prj: SpecIF, H?: SpecifNode[] ):Resource[] { // collect all resources referenced by the given hierarchy: if( !prj ) prj = app.cache.selectedProject.data; if( !H ) H = prj.hierarchies; var rL:Resource[] = []; - iterateNodes( H, (nd)=>{ cacheE( rL, itemById(prj.resources,itemIdOf(nd.resource)) ); return true } ); + Lib.iterateNodes( H, (nd)=>{ Lib.cacheE( rL, itemById(prj.resources,Lib.itemIdOf(nd.resource)) ); return true } ); return rL; } function dataTypeOf(prj: SpecIF, pCid:string ):DataType { // given a propertyClass id, return it's dataType: if( typeof(pCid)=='string' && pCid.length>0 ) return itemById( prj.dataTypes, itemById( prj.propertyClasses, pCid ).dataType ) - // | get class + // | get propertyClass // get dataType // else: // may happen, if a resource does not have any properties and it's title or description is being used: @@ -2692,8 +3001,7 @@ function enumValueOf(dT: DataType, val: string, opts?: any): string { // If 'eV' is an id, replace it by the corresponding value, otherwise don't change: // For example, when an object is from a search hitlist or from a revision list, // the value ids of an ENUMERATION have already been replaced by the corresponding titles. - if( eV ) ct += (i==0?'':', ')+eV - else ct += (i==0?'':', ')+v + ct += (i == 0 ? '' : ', ') + (eV ? eV : v); }); return ct; } @@ -2711,7 +3019,7 @@ function visibleIdOf(r: Resource, prj?: SpecIF ):string|undefined { // Check the configured ids: if( CONFIG.idProperties.indexOf( vocabulary.property.specif( propTitleOf(r.properties[a],prj) ) )>-1 ) return r.properties[a].value - } + }; }; // return undefined } @@ -2742,12 +3050,12 @@ function languageValueOf( val, opts?:any ):string|undefined { // Return the value in the specified target language .. or the first value in the list by default. // 'val' can be a string or a multi-language object; // if targetLanguage is not defined, keep all language options: - if( typeof(val)=='string' || !(opts&&opts.targetLanguage) ) return val; + if( typeof(val)=='string' || !(opts&&opts.lookupLanguage) ) return val; // The value may be undefined: if( val==undefined ) return; if( !Array.isArray(val) ) { // neither a string nor an array is a programming error: - throw Error('Invalid value: ',val); + throw Error("Invalid value: '"+val+"'"); }; let lVs = val.filter( (v):boolean =>{ @@ -2758,7 +3066,7 @@ function languageValueOf( val, opts?:any ):string|undefined { // next try a little less stringently: lVs = val.filter( (v):boolean =>{ - return opts.targetLanguage.slice(0,2) == v.language.slice(0,2); + return opts && opts.targetLanguage && (opts.targetLanguage.slice(0,2) == v.language.slice(0,2)); }); // lVs should have none or one elements; any additional ones are simply ignored: if( lVs.length>0 ) return lVs[0].text; @@ -2766,15 +3074,15 @@ function languageValueOf( val, opts?:any ):string|undefined { // As a final resourt take the first element in the original list of values: return val[0].text; } -function hasContent( pV:string ):boolean { +Lib.hasContent = ( pV:string ):boolean =>{ // must be a string with the value of the selected language. if( typeof(pV)!="string" ) return false; - return stripHTML(pV).length>0 + return pV.stripHTML().length>0 || RE.tagSingleObject.test(pV) // covers nested object tags, as well || RE.tagImg.test(pV) || RE.tagA.test(pV) } -function iterateNodes(tree: SpecifNode[]|SpecifNode, eFn:Function, lFn?:Function): boolean { +Lib.iterateNodes = (tree: SpecifNode[]|SpecifNode, eFn:Function, lFn?:Function): boolean =>{ // Iterate a SpecIF hierarchy or a branch of a hierarchy. // Do NOT use with a tree for display (jqTree). // 1. Execute eFn for every node of the tree as long as eFn returns true; @@ -2786,14 +3094,14 @@ function iterateNodes(tree: SpecifNode[]|SpecifNode, eFn:Function, lFn?:Function let cont=true; if( Array.isArray( tree ) ) { for( var i=tree.length-1; cont&&(i>-1); i-- ) { - cont = !iterateNodes( tree[i], eFn, lFn ); + cont = !Lib.iterateNodes( tree[i], eFn, lFn ); }; if( typeof(lFn)=='function' ) lFn( tree ); } else { cont = eFn( tree ); if( cont && tree.nodes ) { - cont = !iterateNodes( tree.nodes, eFn, lFn ); + cont = !Lib.iterateNodes( tree.nodes, eFn, lFn ); }; }; return !cont; @@ -2840,41 +3148,41 @@ function propByTitle(itm: Resource, pN: string, dta:SpecIF): Property|undefined }; // return undefined } -function valByTitle(itm:Resource,pN:string,prj:SpecIF):string|undefined { +function valByTitle(itm:Resource,pN:string,dta:SpecIF):string|undefined { // Return the value of a resource's (or statement's) property with title pN: // ToDo: return the class's default value, if available. -// console.debug('valByTitle',prj,itm,pN); +// console.debug('valByTitle',dta,itm,pN); if( itm.properties ) { for( var i=itm.properties.length-1;i>-1;i-- ) { - if( (itm.properties[i].title || itemById( prj.propertyClasses, itm.properties[i]['class'] ).title)==pN ) + if( (itm.properties[i].title || itemById( dta.propertyClasses, itm.properties[i]['class'] ).title)==pN ) return itm.properties[i].value } }; // return undefined } -function titleIdx(pL: Property[], prj?: SpecIF): number { +function titleIdx(pL: Property[], dta?: SpecIF): number { // Find the index of the property to be used as title. // The result depends on the current user - only the properties with read permission are taken into consideration. // This works for title strings and multi-language title objects. // The first property which is found in the list of headings or titles is chosen: if( pL ) { - if( !prj ) prj = app.cache.selectedProject.data; + if( !dta ) dta = app.cache.selectedProject.data; let pt; for( var a=0,A=pL.length;a-1 ) return a; }; }; return -1; } -function elementTitleOf(el: Resource | Statement, opts?:any, prj?:SpecIF): string { +function elementTitleOf(el: Resource | Statement, opts?:any, dta?:SpecIF): string { // Get the title of a resource or a statement; // ... from the properties or a replacement value in case of default. // 'el' is an original element without 'classifyProps()'. if( typeof(el)!='object' ) return; - if( !prj ) prj = app.cache.selectedProject.data; + if( !dta ) dta = app.cache.selectedProject.data; // Lookup titles only in case of a resource serving as heading or in case of a statement: let localOpts; @@ -2885,8 +3193,9 @@ function elementTitleOf(el: Resource | Statement, opts?:any, prj?:SpecIF): strin else { // it is a resource localOpts = { + lookupLanguage: opts.lookupLanguage, targetLanguage: opts.targetLanguage, - lookupTitles: opts.lookupTitles && itemById( prj.resourceClasses, el['class'] ).isHeading + lookupTitles: opts.lookupTitles && itemById( dta.resourceClasses, el['class'] ).isHeading }; }; // Get the title from the properties or natively by default: @@ -2898,15 +3207,15 @@ function elementTitleOf(el: Resource | Statement, opts?:any, prj?:SpecIF): strin // it is a statement if( !ti ) // take the class' title by default: - ti = staClassTitleOf( el, prj, opts ); + ti = staClassTitleOf( el, dta, opts ); } else { // it is a resource - if( opts && opts.addIcon && CONFIG.addIconToInstance && prj && ti ) - ti = addIcon( ti, itemById( prj.resourceClasses, el['class'] ).icon ); + if( opts && opts.addIcon && CONFIG.addIconToInstance && dta && ti ) + ti = Lib.addIcon( ti, itemById( dta.resourceClasses, el['class'] ).icon ); }; // console.debug('elementTitleOf',el,opts,ti); - return typeof (ti) == 'string' ? stripHTML(ti) : ti; + return typeof (ti) == 'string' ? ti.stripHTML() : ti; function getTitle(pL: Property[], opts:any ): string { // if( !pL ) return; diff --git a/src/modules/filter.mod.ts b/src/modules/filter.mod.ts index fefbc87..5d4e96c 100644 --- a/src/modules/filter.mod.ts +++ b/src/modules/filter.mod.ts @@ -26,8 +26,8 @@ options: [ {title:'Word Beginnings', id:'wordBeginnings', checked:false}, {title:'Whole Words', id:'wholeWords', checked:false}, - {title:'Case Sensitive', id:'caseSensitive', checked:true}, - {title:'Exclude Enums', id:'excludeEnums', checked:false} + {title:'Case Sensitive', id:'caseSensitive', checked:true} + // {title:'Exclude Enums', id:'excludeEnums', checked:false} ] },{ title: 'Resource Class', @@ -174,7 +174,7 @@ moduleManager.construct({ function handleError(xhr: xhrMessage): void { self.clear(); // This is a sub-module to specs, so use its return method: - stdError(xhr); + Lib.stdError(xhr); }; // standard module entry: @@ -186,6 +186,7 @@ moduleManager.construct({ pData.showLeft.reset(); $('#filterNotice').empty(); + displayOptions.lookupLanguage = true; displayOptions.targetLanguage = pData.targetLanguage; displayOptions.lookupTitles = true; displayOptions.lookupValues = true; @@ -331,13 +332,13 @@ moduleManager.construct({ switch( dT.type ) { case TypeEnum.XsEnumeration: // only if enumerated values are included in the search: - if( !isChecked( f.options, 'excludeEnums' )) { + // if( !isChecked( f.options, 'excludeEnums' )) { if( patt.test( enumValueOf(dT,prp.value,displayOptions) ) ) return true; - }; + // }; break; case TypeEnum.XHTML: case TypeEnum.XsString: - if (patt.test( stripHTML(languageValueOf(prp.value,displayOptions)) )) return true; + if (patt.test( languageValueOf(prp.value, displayOptions).stripHTML() )) return true; break; default: if( patt.test( languageValueOf(prp.value,displayOptions) )) return true; @@ -491,14 +492,14 @@ moduleManager.construct({ // console.debug( '$0,$1,$2',$0,$1,$2 ); // 1. mark the preceding text: - if( stripHTML($1).length>0 ) + if ($1.stripHTML().length>0 ) $1 = $1.replace( re, ($a)=>{ return ''+$a+'' }); markedText += $1+$2; // consume txt: return '' }); // 2. finally mark the remainder (the rest of the txt not consumed before): - if ( stripHTML(txt).length>0 ) + if ( txt.stripHTML().length>0 ) markedText += txt.replace( re, ($a)=>{ return ''+$a+'' }); return markedText } @@ -572,7 +573,7 @@ moduleManager.construct({ function allEnumValues(pC: PropertyClass, vL):IBox[] { var boxes = [], - dT = itemById(dta.dataTypes, pC.dataType); + dT = dta.get( "dataType", pC.dataType)[0]; // Look up the baseType and include all possible enumerated values: if (dT && Array.isArray(dT.values)) { dT.values.forEach( (v)=>{ @@ -627,14 +628,14 @@ moduleManager.construct({ // console.debug('addEnumValueFilters',def); // This is called per resourceClass. // Each ENUMERATION property gets a filter module: - var rC: ResourceClass = itemById(dta.resourceClasses, def.rCid), + var rC: ResourceClass = dta.get("resourceClass", def.rCid)[0], pC: PropertyClass; // console.debug( 'rC', def, rC ); rC.propertyClasses.forEach( (pcid)=>{ - pC = itemById( dta.propertyClasses, pcid ); + pC = dta.get( "propertyClass", pcid )[0]; // if( pcid==def.pCid && itemById( dta.dataTypes, pC.dataType ).type == 'xs:enumeration' ) { if( (def.pCid && pC.id==def.pCid ) // we can assume that def.pCid == 'xs:enumeration' - || (!def.pCid && itemById( dta.dataTypes, pC.dataType ).type==TypeEnum.XsEnumeration)) { + || (!def.pCid && dta.get( "dataType", pC.dataType )[0].type==TypeEnum.XsEnumeration)) { addEnumFilter( rC, pC, def.options ) }; }); @@ -660,8 +661,8 @@ moduleManager.construct({ options: [ { id: 'wordBeginnings', title: i18n.LblWordBeginnings, checked: pre&&pre.options.indexOf('wordBeginnings')>-1 }, { id: 'wholeWords', title: i18n.LblWholeWords, checked: pre&&pre.options.indexOf('wholeWords')>-1 }, - { id: 'caseSensitive', title: i18n.LblCaseSensitive, checked: pre&&pre.options.indexOf('caseSensitive')>-1 }, - { id: 'excludeEnums', title: i18n.LblExcludeEnums, checked: pre&&pre.options.indexOf('excludeEnums')>-1 } + { id: 'caseSensitive', title: i18n.LblCaseSensitive, checked: pre&&pre.options.indexOf('caseSensitive')>-1 } + // { id: 'excludeEnums', title: i18n.LblExcludeEnums, checked: pre&&pre.options.indexOf('excludeEnums')>-1 } ] }; // console.debug('addTextSearchFilter',flt); @@ -690,7 +691,7 @@ moduleManager.construct({ baseType: TypeEnum.XsEnumeration, options: [] }; - dta.resourceClasses.forEach( ( rC )=>{ + dta.get("resourceClass","all").forEach( ( rC )=>{ if( CONFIG.excludedFromTypeFiltering.indexOf( rC.title )>-1 ) return; // skip var box = { diff --git a/src/modules/helper.ts b/src/modules/helper.ts index f648a0b..3e14062 100644 --- a/src/modules/helper.ts +++ b/src/modules/helper.ts @@ -10,6 +10,7 @@ - Do NOT minify this module with the Google Closure Compiler. At least the RegExp in jsIdOf() will be modified to yield wrong results, e.g. falsely replaces 'u' by '_'. */ +const Lib: any = {}; interface IFieldOptions { tagPos?: string; // 'left', 'none' typ?: string; // 'line', 'area' for textField @@ -22,7 +23,7 @@ function textField(tag: string, val: string, opts?: IFieldOptions): string { if (!opts) opts = {} as IFieldOptions; if (typeof (opts.tagPos) != 'string') opts.tagPos = 'left'; - val = typeof(val)=='string'? noCode( val ) : ''; + val = typeof(val)=='string'? Lib.noCode( val ) : ''; let fn = (typeof (opts.handle) == 'string' && opts.handle.length > 0)? ' oninput="' + opts.handle + '"' : '', sH = simpleHash(tag), @@ -98,7 +99,7 @@ function textValue( tag:string ):string { // get the input value: try { // @ts-ignore - .value is in fact accessible - return noCode(document.getElementById('field' + simpleHash(tag)).value) || ''; + return Lib.noCode(document.getElementById('field' + simpleHash(tag)).value) || ''; } catch(e) { return ''; } @@ -250,7 +251,10 @@ class xhrMessage { } } // standard error handler: -function stdError(xhr: xhrMessage, cb?:Function): void { +Lib.logMsg = (xhr: xhrMessage): void =>{ + console.log(xhr.statusText + " (" + xhr.status + (xhr.responseType == 'text' ? "): " + xhr.responseText : ")")); +} +Lib.stdError = (xhr: xhrMessage, cb?:Function): void =>{ // console.debug('stdError',xhr); // clone, as xhr.responseText ist read-only: let xhrCl = new xhrMessage(xhr.status, xhr.statusText, xhr.responseType, xhr.responseType=='text'? xhr.responseText : ''); @@ -297,7 +301,7 @@ function stdError(xhr: xhrMessage, cb?:Function): void { message.show( xhr ); }; // log original values: - console.log( xhr.statusText + " (" + xhr.status + (xhr.responseType=='text'?"): "+xhr.responseText : ")") ); + Lib.logMsg(xhr); if( typeof(cb)=='function' ) cb(); }; // standard message box: @@ -467,7 +471,7 @@ function itemBy(L:any[], p:string, s:string ):any { if( L[i][p]==s ) return L[i]; // return list item }; } -function containsById(cL:any[], L: Item[] ):boolean { +Lib.containsById = (cL:any[], L: Item[] ):boolean =>{ if (!cL || !L) throw Error("Missing Array"); // return true, if all items in L are contained in cL (cachedList), // where L may be an array or a single item: @@ -478,8 +482,8 @@ function containsById(cL:any[], L: Item[] ):boolean { if ( indexById( cL, L[i].id )<0 ) return false; return true; } -} -function containsByTitle(cL:any[], L: Item[] ):boolean { +} +/* Lib.containsByTitle = (cL:any[], L: Item[] ):boolean =>{ if (!cL || !L) throw Error("Missing Array"); // return true, if all items in L are contained in cL (cachedList): return Array.isArray(L)?containsL( cL, L ):( indexByTitle( cL, L.title )>-1 ); @@ -489,25 +493,25 @@ function containsByTitle(cL:any[], L: Item[] ):boolean { if ( indexByTitle( cL, L[i].title )<0 ) return false; return true; } -} -function cmp( i:string, a:string ):number { +} */ +Lib.cmp = ( i:string, a:string ):number =>{ if( !i ) return -1; if( !a ) return 1; i = i.toLowerCase(); a = a.toLowerCase(); return i==a? 0 : (i{ L.sort( - (bim,bam)=>{ return cmp( bim.title, bam.title ) } + (bim,bam)=>{ return Lib.cmp( bim.title, bam.title ) } ); } -function sortBy( L:any[], fn:(arg0:object)=>string ):void { +Lib.sortBy = ( L:any[], fn:(arg0:object)=>string ):void =>{ L.sort( - (bim,bam)=>{ return cmp( fn(bim), fn(bam) ) } + (bim, bam) => { return Lib.cmp( fn(bim), fn(bam) ) } ); } -function forAll( L:any[], fn:(arg0:any)=>any ):Array { +Lib.forAll = ( L:any[], fn:(arg0:any)=>any ):Array =>{ // return a new list with the results from applying the specified function to all items of input list L; // differences when compared to Array.map(): // - tolerates missing L @@ -520,68 +524,44 @@ function forAll( L:any[], fn:(arg0:any)=>any ):Array { // Add a leading icon to a title: // use only for display, don't add to stored variables. -function addIcon(str: string, ic: string): string { +Lib.addIcon = (str: string, ic: string): string =>{ if (ic) return ic + ' ' + str; return str; } -function addE( ctg:string, id:string, pr? ):void { - // Add an element (e.g. class) to it's list, if not yet defined: - if( !pr ) pr = app.cache.selectedProject.data; - - // get the name of the list, e.g. 'dataType' -> 'dataTypes': - let lN:string = standardTypes.listNameOf(ctg); - // create it, if not yet available: - if (Array.isArray(pr[lN])) { - // add the type, but avoid duplicates: - if( indexById( pr[lN], id )<0 ) - pr[lN].unshift( standardTypes.get(ctg,id) ); - } - else { - pr[lN] = [ standardTypes.get(ctg,id) ]; - }; -} -function addPC( eC:object, id:string ):void { - // Add the propertyClass-id to an element class (eC), if not yet defined: - let lN = 'propertyClasses'; - if (Array.isArray(eC[lN])) { - // Avoid duplicates: - if( eC[lN].indexOf( id )<0 ) - eC[lN].unshift( id ); - } - else { - eC[lN] = [id]; - }; -} -function addP( el:object, prp:object ):void { - // Add the property to an element (el): - if (Array.isArray(el['properties'])) - el['properties'].unshift( prp ); - else - el['properties'] = [prp]; -} -function cacheE( L:Array, e:object ):number { // ( list, entry ) +Lib.cacheE = ( L:Array, e:object ):number =>{ // ( list, entry ) // add or update the item e in a list L: let n = typeof(e)=='object'? indexById( L, e.id ) : L.indexOf(e); - if( n<0 ) { L.push( e ); return L.length-1 }; // add, if not yet listed - L[n] = e; return n; // update otherwise + // add, if not yet listed: + if (n < 0) { + L.push(e); + return L.length - 1; + }; + // update, if newer: +// if ( L[n].changedAt && e.changedAt && new Date(L[n].changedAt), es:Array ):void { // ( list, entries ) +Lib.cacheL = ( L:Array, es:Array ):boolean =>{ // ( list, entries ) // add or update the items es in a list L: - es.forEach( (e)=>{ cacheE( L, e ) } ) + es.forEach((e) => { Lib.cacheE(L, e) }) + // this operation cannot fail: + return true; } -function uncacheE( L:Array, e:object ):number { // ( list, entry ) +Lib.uncacheE = ( L:Array, e:object ):number =>{ // ( list, entry ) // remove the item e from a list L: let n = typeof(e)=='object'? indexById( L, e.id ) : L.indexOf(e); if( n>-1 ) L.splice(n,1); // remove, if found return n; } -function uncacheL( L:Array, es:Array ):void { // ( list, entries ) +Lib.uncacheL = ( L:Array, es:Array ):boolean =>{ // ( list, entries ) // remove the items es from a list L: - es.forEach( (e)=>{ uncacheE( L, e ) } ); + let done = true; + es.forEach((e) => { done = done && Lib.uncacheE(L, e) > -1 }); + return done; } // http://stackoverflow.com/questions/10726909/random-alpha-numeric-string-in-javascript -function genID(pfx:string):string { +Lib.genID = (pfx:string):string =>{ if( !pfx || pfx.length<1 ) { pfx = 'ID_' }; let re = /^[A-Za-z_]/; if( !re.test(pfx) ) { pfx = '_'+pfx }; // prefix must begin with a letter or '_' @@ -610,12 +590,16 @@ interface String { specifIdOf: Function; linkifyURLs: Function; ctrl2HTML: Function; + stripHTML: Function; + stripCtrl: Function; makeHTML: Function; escapeRE: Function; escapeXML: Function; escapeHTML: Function; escapeHTMLTags: Function; escapeHTMLEntities: Function; + unescapeHTMLTags: Function; + unescapeHTMLEntities: Function; fileName: Function; fileExt: Function; } @@ -627,12 +611,6 @@ String.prototype.jsIdOf = function():string { String.prototype.specifIdOf = function():string { return this.replace( /[^_0-9a-zA-Z]/g, '_' ); }; -// Make a very simple hash code from a string: -// http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ -function simpleHash(str: string): string { - for (var r = 0, i = 0; i < str.length; i++) r = (r << 5) - r + str.charCodeAt(i), r &= r; - return r -}; /* function truncate(l:number):string { var t = this.substring(0,l-1); @@ -648,27 +626,36 @@ String.prototype.log = function(m:string):string { console.debug( m, this ); return this }; */ -function stripCtrl(str:string):string { +String.prototype.stripHTML = function():string { + // strip html, but don't use a regex to impede cross-site-scripting (XSS) attacks: + return $("").html(this).text().trim() || ''; +}; +/* + * Returns the text from a HTML string + * see: https://ourcodeworld.com/articles/read/376/how-to-strip-html-from-a-string-extract-only-text-content-in-javascript + * + * @param {html} String The html string to strip + * +function stripHtml(html){ + // Create a new div element + var temp = document.createElement("div"); + // Set the HTML content with the providen + temp.innerHTML = html; + // Retrieve the text property of the element (cross-browser support) + return temp.textContent || temp.innerText || ""; +} */ +String.prototype.stripCtrl = function():string { // Remove js/json control characters from HTML-Text or other: - return str.replace( /\b|\f|\n|\r|\t|\v/g, '' ); + return this.replace( /\b|\f|\n|\r|\t|\v/g, '' ); } String.prototype.ctrl2HTML = function():string { // Convert js/json control characters (new line) to HTML-tags and remove the others: return this.replace( /\r|\f/g, '' ) .replace( /�{0,3}a;/gi, '' ) - .replace(/\t/g, '    ' ) + .replace(/\t/g, '    ' ) // nbsp .replace( /\n/g, '
' ) .replace( /�{0,3}d;/gi, '
' ); }; -function toHTML(str:string):string { -// Escape HTML characters and convert js/json control characters (new line etc.) to HTML-tags: - return str.escapeHTML().ctrl2HTML() -} -// https://stackoverflow.com/questions/15458876/check-if-a-string-is-html-or-not -function isHTML(str:string):boolean { - let doc = new DOMParser().parseFromString(str, "text/html"); - return Array.from(doc.body.childNodes).some(node => node.nodeType==1) -} String.prototype.makeHTML = function(opts?:any):string { // Note: HTML embedded in markdown is not supported, because isHTML() will return 'true'. if (typeof (opts) == 'object' && opts.makeHTML) { @@ -714,7 +701,16 @@ String.prototype.xmlChar2utf8 = function():string { }) } */ -function escapeInnerHtml( str:string ):string { +Lib.toHTML = (str: string): string => { + // Escape HTML characters and convert js/json control characters (new line etc.) to HTML-tags: + return str.escapeHTML().ctrl2HTML() +} +// https://stackoverflow.com/questions/15458876/check-if-a-string-is-html-or-not +Lib.isHTML = (str: string): boolean => { + let doc = new DOMParser().parseFromString(str, "text/html"); + return Array.from(doc.body.childNodes).some(node => node.nodeType == 1) +} +Lib.escapeInnerHtml = ( str:string ):string =>{ // escape text except for HTML tags: var out = ""; @@ -729,15 +725,6 @@ function escapeInnerHtml( str:string ):string { // escape the inner text and keep the tag: out += $1.escapeXML() + $2 + $3 + $4; - /* // @ts-ignore - $0 is never read, but must be specified anyways - str = str.replace( RE.innerTag, function($0,$1,$2,$3) { - // $1: inner text (before the next tag) - // $2: start of opening tag '<' or closing tag '' or '/>' - - // escape the inner text and keep the tag: - out += $1.escapeXML() + $2 + $3; */ - // consume the matched piece of str: return ''; }); @@ -764,9 +751,9 @@ String.prototype.escapeHTML = function():string { }; String.prototype.unescapeHTMLTags = function():string { // Unescape known HTML-tags: - if( isHTML(this as string) ) return this as string; + if( Lib.isHTML(this as string) ) return this as string; // @ts-ignore - $0 is never read, but must be specified anyways - return noCode(this.replace(RE.escapedHtmlTag, ($0,$1,$2,$3)=>{ + return Lib.noCode(this.replace(RE.escapedHtmlTag, ($0,$1,$2,$3)=>{ return '<'+$1+$2+$3+'>'; })); }; @@ -774,7 +761,7 @@ String.prototype.unescapeHTMLTags = function():string { String.prototype.unescapeHTMLEntities = function():string { // unescape HTML encoded entities (characters): var el = document.createElement('div'); - return noCode(this.replace(/\&#?x?[0-9a-z]+;/gi, (enc)=>{ + return Lib.noCode(this.replace(/\&#?x?[0-9a-z]+;/gi, (enc)=>{ el.innerHTML = enc; return el.innerText; })); @@ -782,26 +769,8 @@ String.prototype.unescapeHTMLEntities = function():string { /*// better: https://stackoverflow.com/a/34064434/5445 but strips HTML tags. String.prototype.unescapeHTMLEntities = function() { var doc = new DOMParser().parseFromString(input, "text/html"); - return noCode( doc.documentElement.textContent ) + return Lib.noCode( doc.documentElement.textContent ) };*/ -function stripHTML(str:string):string { - // strip html, but don't use a regex to impede cross-site-scripting (XSS) attacks: - return $("").html( str ).text().trim() || ''; -}; -/** - * Returns the text from a HTML string - * see: https://ourcodeworld.com/articles/read/376/how-to-strip-html-from-a-string-extract-only-text-content-in-javascript - * - * @param {html} String The html string to strip - * -function stripHtml(html){ - // Create a new div element - var temp = document.createElement("div"); - // Set the HTML content with the providen - temp.innerHTML = html; - // Retrieve the text property of the element (cross-browser support) - return temp.textContent || temp.innerText || ""; -} */ // Add a link to an isolated URL: String.prototype.linkifyURLs = function( opts?:any ):string { @@ -813,7 +782,7 @@ String.prototype.linkifyURLs = function( opts?:any ):string { // all links which do not start with "http" are considered local by most browsers: if( !$2.startsWith('http') ) $2 = 'https://'+$2; // starts with "www." then according to RE.URI /* // we must encode the URI, but to avoid that an already encoded URI is corrupted, we first decode it - // under the assumption that a decoding a non-encoded URI does not cause a change. + // under the assumption that decoding a non-encoded URI does not cause a change. // This does not work if a non-encoded URI contains '%'. return $1+''+(opts&&opts.label? opts.label:$3+($4||'')+$5)+''+$9 */ return $1+''+(opts&&opts.label? opts.label:$3+($4||'')+$5)+''+$9; @@ -830,12 +799,12 @@ String.prototype.fileName = function():string { // return the filename without extension: return this.substring( 0, this.lastIndexOf('.') ) }; -function trimJson(str:string):string { +Lib.trimJson = (str:string):string =>{ // trim all characters outside the outer curly brackets, which may include the UTF-8 byte-order-mask: return str.substring( str.indexOf('{'), str.lastIndexOf('}')+1 ) }; -/* +/* String.prototype.removeBOM = function():string { // remove the byte order mask from a UTF-8 coded string // ToDo: Any whitespace between BOM and JSON is not taken care of. @@ -849,9 +818,9 @@ function toHex(str) { hex += nV.length>1?''+nV:'0'+nV }; return hex; -};*/ +}; */ -function ab2str(buf):string { +Lib.ab2str = (buf): string =>{ // Convert arrayBuffer to string: // UTF-8 character table: http://www.i18nqa.com/debug/utf8-debug.html // or: https://bueltge.de/wp-content/download/wk/utf-8_kodierungen.pdf @@ -869,7 +838,7 @@ function ab2str(buf):string { return String.fromCharCode.apply(null, new Uint8Array(buf)); }; */ } -function str2ab(str:string) { +Lib.str2ab = (str:string) =>{ // Convert string to arrayBuffer: // try { let encoder = new TextEncoder(); @@ -887,7 +856,7 @@ function str2ab(str:string) { // see: https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications // see: https://blog.logrocket.com/programmatic-file-downloads-in-the-browser-9a5186298d5c/ // see: https://css-tricks.com/lodge/svg/09-svg-data-uris/ -function blob2dataURL(file, fn: Function, timelag?: number): void { +Lib.blob2dataURL = (file, fn: Function, timelag?: number): void =>{ if( !file || !file.blob ) return; const reader = new FileReader(); reader.addEventListener('loadend', (e)=>{ fn(e.target.result,file.title,file.type) }); @@ -898,18 +867,18 @@ function blob2dataURL(file, fn: Function, timelag?: number): void { else reader.readAsDataURL(file.blob); } -function blob2text(file,fn:Function,timelag?:number):void { - if( !file || !file.blob ) return; +Lib.blob2text = (file, fn: Function, timelag?: number): void => { + if (!file || !file.blob) return; const reader = new FileReader(); - reader.addEventListener('loadend', (e)=>{ fn(e.target.result,file.title,file.type) }); - if( typeof(timelag)=='number' && timelag>0 ) - setTimeout( ()=>{ + reader.addEventListener('loadend', (e) => { fn(e.target.result, file.title, file.type) }); + if (typeof (timelag) == 'number' && timelag > 0) + setTimeout(() => { reader.readAsText(file.blob); - }, timelag ); + }, timelag); else reader.readAsText(file.blob); -} -function uriBack2slash(str:string):string { +}; +Lib.uriBack2slash = (str:string):string =>{ return str.replace( /<(?:object[^>]+?data=|img[^>]+?href=)"([^"]+)"[^>]*?\/?>/g, ($0)=>{ return $0.replace( /(?:data=|href=)"([^"]+)"/g, @@ -924,7 +893,7 @@ function uriBack2slash(str:string):string { // not good enough, but better than nothing: // see https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet // do not implement as chainable function, because a string object is created. -function noCode( s:string ):string { +Lib.noCode = ( s:string ):string =>{ if( s ) { // just suppress the whole content, if there are inacceptable/evil tags or properties, do NOT try to repair it. // @@ -939,13 +908,13 @@ function noCode( s:string ):string { console.log("'"+s+"' is considered harmful ("+c+") and has been suppressed"); } } -function cleanValue(o: string | ValueElement[] ):string|ValueElement[] { +Lib.cleanValue = (o: string | ValueElement[]): string | ValueElement[] => { // remove potential malicious code from a value which may be supplied in several languages: - if( typeof(o)=='string' ) return noCode( o ); - if( Array.isArray(o) ) return forAll( o, ( val )=>{ val.text = noCode(val.text); return val } ); + if( typeof(o)=='string' ) return Lib.noCode( o ); + if( Array.isArray(o) ) return Lib.forAll( o, ( val )=>{ val.text = Lib.noCode(val.text); return val } ); return ''; // unexpected input (programming error with all likelihood } -function attachment2mediaType( fname:string ):string|undefined { +Lib.attachment2mediaType = ( fname:string ):string|undefined =>{ let t = fname.fileExt(); // get the extension excluding '.' if( t ) { // the sequence of mediaTypes in xTypes corresponds to the sequence of extensions in xExtensions: @@ -958,7 +927,7 @@ function attachment2mediaType( fname:string ):string|undefined { }; // return undefined; } -function localDateTime(iso:string):string { +Lib.localDateTime = (iso:string):string =>{ if( typeof(iso)=='string' ) { // ToDo: calculate offset of time-zone ... or use one of the libraries .. if( iso.length>15 ) return (iso.substr(0,10)+' '+iso.substr(11,5)+'h'); @@ -967,17 +936,24 @@ function localDateTime(iso:string):string { return ''; } -function simpleClone( o ) { +// Make a very simple hash code from a string: +// http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ +function simpleHash(str: string): string { + for (var r = 0, i = 0; i < str.length; i++) r = (r << 5) - r + str.charCodeAt(i), r &= r; + return r +}; +function simpleClone( o ) { // "deep" clone; // does only work, if none of the property values are functions: function cloneProp(p) { return ( typeof(p) == 'object' )? simpleClone(p) : p; } if( typeof(o)=='object' ) { - if( Array.isArray(o) ) - var n=[]; + var n: any; + if (Array.isArray(o)) + n=[]; else - var n={}; + n={}; for( var p in o ) { if( Array.isArray(o[p]) ) { n[p] = []; @@ -1118,7 +1094,7 @@ function clearUrlParams():void { // console.debug( 'clearUrlParams', path ); history.pushState('','',path[path.length-1]); // last element is 'appname.html' without url parameters; } -function httpGet(params:any):void { +Lib.httpGet = (params:any):void =>{ // https://blog.garstasio.com/you-dont-need-jquery/ // https://www.sitepoint.com/guide-vanilla-ajax-without-jquery/ var xhr = new XMLHttpRequest(); diff --git a/src/modules/helperTree.ts b/src/modules/helperTree.ts index 746e5e6..03ff981 100644 --- a/src/modules/helperTree.ts +++ b/src/modules/helperTree.ts @@ -35,7 +35,8 @@ class Tree { this.domE = $(options.loc); this.domE.tree({ data: [], - // saveState: true, + // saveState: true, + // buttonLeft: false, // left alignment does not work yet for nodes without children dragAndDrop: options.dragAndDrop }); for (var e in options.eventHandlers) { diff --git a/src/modules/importAny.mod.ts b/src/modules/importAny.mod.ts index 782fb83..ecb1fb9 100644 --- a/src/modules/importAny.mod.ts +++ b/src/modules/importAny.mod.ts @@ -46,7 +46,7 @@ moduleManager.construct({ desc:'Specification Integration Facility', label:'SpecIF', help: i18n.MsgImportSpecif, - opts: { mediaTypeOf: attachment2mediaType } + opts: { mediaTypeOf: Lib.attachment2mediaType } },{ id:'archimate', name:'ioArchimate', @@ -54,7 +54,7 @@ moduleManager.construct({ label:'Archimate®', // help: i18n.MsgImportArchimate, help: "Experimental: Import an Archimate Open Exchange file (*.xml) and add the diagrams (*.png or *.svg) to their respective resources using the 'edit' function.", - opts: { mediaTypeOf: attachment2mediaType } + opts: { mediaTypeOf: Lib.attachment2mediaType } },{ id:'bpmn', name:'ioBpmn', @@ -67,7 +67,7 @@ moduleManager.construct({ desc:'Requirement Interchange Format', label:'ReqIF', help: "Experimental: "+i18n.MsgImportReqif, - opts: { dontCheck: ["statement.subject","statement.object"], multipleMode:"update", mediaTypeOf: attachment2mediaType } + opts: { dontCheck: ["statement.subject","statement.object"], multipleMode:"update", mediaTypeOf: Lib.attachment2mediaType } /* },{ id: 'rdf', name: 'ioRdf', @@ -100,24 +100,6 @@ moduleManager.construct({ importing = false, cacheLoaded = false, allValid = false; - - function terminateWithSuccess():void { - message.show( i18n.lookup( 'MsgImportSuccessful', self.file.name ), {severity:"success",duration:CONFIG.messageDisplayTimeShort} ); - setTimeout( function() { - self.clear(); - if( urlP ) delete urlP[CONFIG.keyImport]; - // change view to browse the content: - moduleManager.show({ view: '#'+(urlP&&urlP[CONFIG.keyView] || CONFIG.specifications), urlParams:urlP }) - }, - CONFIG.showTimelag - ); - } - function handleError(xhr:xhrMessage):void { -// console.debug( 'handleError', xhr ); - self.clear(); - stdError(xhr); - self.show(); - } self.clear = function():void { $('input[type=file]').val( '' ); // otherwise choosing the same file twice does not create a change event in Chrome @@ -195,7 +177,7 @@ moduleManager.construct({ return true; }; // The module entry; - // called by the modules view management: + // called by the moduleManager: self.show = function( opts:any ):void { if( !opts ) opts = {}; // console.debug( 'import.show', opts ); @@ -241,7 +223,7 @@ moduleManager.construct({ // Assume it is an absolute or relative URL; // must be either from the same URL or CORS-enabled. // Import the file: - httpGet({ + Lib.httpGet({ // force a reload through cache-busting: url: urlP[CONFIG.keyImport] + '?' + Date.now().toString(), responseType: 'arraybuffer', @@ -277,7 +259,8 @@ moduleManager.construct({ // app[s.name].init( self.format.opts ); if( typeof(app[s.name].toSpecif)=='function' && typeof(app[s.name].verify)=='function' ) { str += ''; - } else { + } + else { str += ''; }; }; @@ -418,6 +401,24 @@ moduleManager.construct({ rdr.readAsArrayBuffer( f ); } }; + + function terminateWithSuccess(): void { + message.show(i18n.lookup('MsgImportSuccessful', self.file.name), { severity: "success", duration: CONFIG.messageDisplayTimeShort }); + setTimeout(function () { + self.clear(); + if (urlP) delete urlP[CONFIG.keyImport]; + // change view to browse the content: + moduleManager.show({ view: '#' + (urlP && urlP[CONFIG.keyView] || CONFIG.specifications), urlParams: urlP }) + }, + CONFIG.showTimelag + ); + } + function handleError(xhr: xhrMessage): void { + // console.debug( 'handleError', xhr ); + self.clear(); + Lib.stdError(xhr); + self.show(); + } // ToDo: construct an object ... var resQ:SpecIF[] = [], resIdx = 0; @@ -446,7 +447,7 @@ moduleManager.construct({ } function handle( dta:SpecIF, idx:number ):void { // console.debug('handleResult',simpleClone(dta),idx); - specif.check( dta, self.format.opts ) + check( dta, self.format.opts ) .then( (dta:SpecIF)=>{ /* // First check if there is a project with the same id: function sameId() { @@ -486,7 +487,7 @@ moduleManager.construct({ // save according to the selected mode: switch( mode.id ) { case 'clone': - dta.id = genID('P-'); + dta.id = Lib.genID('P-'); // no break case 'replace': setProgress('Creating project',20); @@ -525,7 +526,7 @@ moduleManager.construct({ switch( opts.mode ) { /* case 'clone': - dta.id = genID('P-'); + dta.id = Lib.genID('P-'); // no break */ case 'create': case 'replace': @@ -538,6 +539,9 @@ moduleManager.construct({ .fail( handleError ); break; /* case 'update': + opts.deduplicate = true; + opts.addGlossary = true; + opts.collectProcesses = false; app.cache.update(dta, opts) .progress(setProgress) .done(handleNext) @@ -547,7 +551,7 @@ moduleManager.construct({ opts.deduplicate = true; opts.addGlossary = true; opts.collectProcesses = true; - app.cache.selectedProject.update( dta, opts ) + app.cache.selectedProject.adopt( dta, opts ) .progress( setProgress ) .done( handleNext ) .fail( handleError ) @@ -558,6 +562,91 @@ moduleManager.construct({ ); }; }; + function check(spD: SpecIF, opts?: any): Promise { + // Check the SpecIF data for schema compliance and consistency; + // no data of app.cache is modified: + return new Promise( + (resolve, reject) => { + let checker: any; + + if (typeof (spD) == 'object') { + // 1a. Get the "official" routine for checking schema and constraints + // - where already loaded checking routines are replaced by the newly loaded ones + // - use $.ajax() with options since it is more flexible than $.getScript + // - the first (relative) URL is for debugging within a local clone of Github + // - both of the other (absolute) URLs are for a production environment + $.ajax({ + dataType: "script", + cache: true, + url: (spD['$schema'] && spD['$schema'].indexOf('v1.0') < 0 ? + (window.location.href.startsWith('file:/') ? '../../SpecIF/check/CCheck.min.js' + : 'https://specif.de/v' + /\/(?:v|specif-)([0-9]+\.[0-9]+)\//.exec(spD['$schema'])[1] + '/CCheck.min.js') + : 'https://specif.de/v1.0/CCheck.min.js') // older versions are covered by v1.0/check.js + }) + .done(() => { + // 2. Get the specified schema file: + Lib.httpGet({ + // @ts-ignore - 'specifVersion' is defined for versions <1.0 + url: (spD['$schema'] || 'https://specif.de/v' + spD.specifVersion + '/schema'), + responseType: 'arraybuffer', + withCredentials: false, + done: handleResult, + fail: handleError + }); + // 1b. Instantiate checker: + // @ts-ignore - 'CCheck' has just been loaded dynamically: + checker = new CCheck(); + }) + .fail(handleError); + } + else { + reject({ status: 999, statusText: 'No SpecIF data to check' }); + }; + return; + + function handleResult(xhr: XMLHttpRequest) { + // @ts-ignore - checkSchema() and checkConstraints() are defined in check.js loaded at runtime + if (typeof (checker.checkSchema) == 'function' && typeof (checker.checkConstraints) == 'function') { +// console.debug('schema', xhr); + // 1. check data against schema: + // @ts-ignore - checkSchema() is defined in check.js loaded at runtime + let rc: xhrMessage = checker.checkSchema(spD, { schema: JSON.parse(Lib.ab2str(xhr.response)) }); + if (rc.status == 0) { + // 2. Check further constraints: + // @ts-ignore - checkConstraints() is defined in check.js loaded at runtime + rc = checker.checkConstraints(spD, opts); + if (rc.status == 0) { + resolve(spD); + } + else { + reject(rc); + }; + } + else { + // older versions of the checking routine don't set the responseType: + if (typeof (rc.responseText) == 'string' && rc.responseText.length > 0) + rc.responseType = 'text'; + reject(rc); + }; + } + else { + reject({ status: 999, statusText: 'Standard routines checkSchema and checkConstraints are not available.' }); + } + } + function handleError(xhr: xhrMessage) { + switch (xhr.status) { + case 404: + // @ts-ignore - 'specifVersion' is defined for versions <1.0 + let v = spD.specifVersion ? 'version ' + spD.specifVersion : 'with Schema ' + spD['$schema']; + xhr = { status: 903, statusText: 'SpecIF ' + v + ' is not supported by the program!' }; + // no break + default: + reject(xhr); + }; + } + } + ); + } function setProgress(msg:string,perc:number):void { $('#progress .progress-bar').css( 'width', perc+'%' ).html(msg) } diff --git a/src/modules/ioBpmn.mod.ts b/src/modules/ioBpmn.mod.ts index df2265f..38a0f3f 100644 --- a/src/modules/ioBpmn.mod.ts +++ b/src/modules/ioBpmn.mod.ts @@ -63,26 +63,28 @@ moduleManager.construct({ bDO.notify('Transforming BPMN to SpecIF',10); // @ts-ignore - BPMN2Specif() is loaded at runtime - data = BPMN2Specif( ab2str(buf), - { - fileName: fName, - fileDate: fDate, - titleLength: CONFIG.textThreshold, - descriptionLength: CONFIG.maxStringLength, - strGlossaryType: CONFIG.resClassGlossary, - strGlossaryFolder: CONFIG.resClassGlossary, - strActorFolder: "FMC:Actors", - strStateFolder: "FMC:States", - strEventFolder: "FMC:Events", - // strCollectionFolder: "SpecIF:Collections", - // strAnnotationFolder: "SpecIF:Annotations", - strRoleType: CONFIG.resClassRole, - strConditionType: CONFIG.resClassCondition, - strBusinessProcessType: CONFIG.resClassProcess, - strBusinessProcessesType: CONFIG.resClassProcesses, - strBusinessProcessFolder: CONFIG.resClassProcesses, - isIE: false - }); + data = BPMN2Specif( + Lib.ab2str(buf), + { + fileName: fName, + fileDate: fDate, + titleLength: CONFIG.textThreshold, + descriptionLength: CONFIG.maxStringLength, + strGlossaryType: CONFIG.resClassGlossary, + strGlossaryFolder: CONFIG.resClassGlossary, + strActorFolder: "FMC:Actors", + strStateFolder: "FMC:States", + strEventFolder: "FMC:Events", + // strCollectionFolder: "SpecIF:Collections", + // strAnnotationFolder: "SpecIF:Annotations", + strRoleType: CONFIG.resClassRole, + strConditionType: CONFIG.resClassCondition, + strBusinessProcessType: CONFIG.resClassProcess, + strBusinessProcessesType: CONFIG.resClassProcesses, + strBusinessProcessFolder: CONFIG.resClassProcesses, + isIE: false + } + ); // console.debug('input.prjName', self.parent.projectName, data ); if( typeof(data)=='object' && data.id ) bDO.resolve( data ) diff --git a/src/modules/ioReqif.mod.ts b/src/modules/ioReqif.mod.ts index 34495d2..4d774d4 100644 --- a/src/modules/ioReqif.mod.ts +++ b/src/modules/ioReqif.mod.ts @@ -154,7 +154,7 @@ moduleManager.construct({ // Selected file is not zipped - it is expected to be ReqIF data in XML format. // Check if data is valid XML: - let str = ab2str(buf); + let str = Lib.ab2str(buf); if( validateXML(str) ) { // @ts-ignore - transformReqif2Specif() is loaded at runtime var data = transformReqif2Specif( str, {translateTitle2Specif:vocabulary.property.specif} ); @@ -344,7 +344,7 @@ moduleManager.construct({ for( j=L[i].properties.length-1;j>-1;j-- ) { prp = L[i].properties[j]; // check only the property with the specified class: - if( prp['class']==id && isHTML(prp.value) ) return true; + if( prp['class']==id && Lib.isHTML(prp.value) ) return true; }; }; return false; @@ -391,7 +391,7 @@ moduleManager.construct({ + ''+date+'' + '' + '1.0' - + ''+(pr.tool || '')+'' + + ''+(pr.generator || '')+'' + ''+pr.title+'' + '' + '' @@ -599,10 +599,10 @@ moduleManager.construct({ return xml; function dateTime( e ):string { - return e.changedAt || pr.changedAt || date + return e.changedAt || pr.createdAt || date } function commonAttsOf( e ):string { - return 'IDENTIFIER="' + e.id + '" LONG-NAME="' + (e.title ? stripHTML(e.title).escapeXML() : '') + '" DESC="' + (e.description ? stripHTML(e.description).escapeXML():'')+'" LAST-CHANGE="'+dateTime(e)+'"' + return 'IDENTIFIER="' + e.id + '" LONG-NAME="' + (e.title ? e.title.stripHTML().escapeXML() : '') + '" DESC="' + (e.description ? e.description.stripHTML().escapeXML():'')+'" LAST-CHANGE="'+dateTime(e)+'"' } function attrTypesOf( eC ):string { // eC: resourceClass or statementClass @@ -683,7 +683,7 @@ moduleManager.construct({ + '' break; case 'xs:string': - xml += '' + xml += '' + 'PC-'+adId+'' + '' break; @@ -715,7 +715,7 @@ moduleManager.construct({ let hasDiv = RE_hasDiv.test(prp.value), txt = // escape text except for HTML tags: - escapeInnerHtml(prp.value) + Lib.escapeInnerHtml(prp.value) // ReqIF does not support the class attribute: .replace( RE_class, function() { return ''; diff --git a/src/modules/ioSpecif.mod.ts b/src/modules/ioSpecif.mod.ts index 83eb869..daacc81 100644 --- a/src/modules/ioSpecif.mod.ts +++ b/src/modules/ioSpecif.mod.ts @@ -91,7 +91,7 @@ moduleManager.construct({ // Please note: // - the file may have a UTF-8 BOM // - all property values are encoded as string, even if boolean, integer or double. - data = JSON.parse( trimJson(dta) ); + data = JSON.parse( Lib.trimJson(dta) ); data.files = []; // SpecIF data is valid. @@ -150,7 +150,7 @@ moduleManager.construct({ try { // Cut-off UTF-8 byte-order-mask ( 3 bytes xEF xBB xBF ) at the beginning of the file, if present. // The resulting data before parsing must be a JSON string enclosed in curly brackets "{" and "}". - var data = JSON.parse( trimJson(ab2str(buf)) ); + var data = JSON.parse( Lib.trimJson(Lib.ab2str(buf)) ); zDO.resolve( data ); } catch (err) { zDO.reject( errInvalidJson ); diff --git a/src/modules/ioXls.mod.ts b/src/modules/ioXls.mod.ts index b686f2a..e28f277 100644 --- a/src/modules/ioXls.mod.ts +++ b/src/modules/ioXls.mod.ts @@ -72,166 +72,166 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { "use strict"; // requires sheetjs - interface ICell { - t: string; - w: string; - v: string|number|boolean|Date; - } - class Coord { - col: number; - row: number; - constructor(addr: string) { - // create a coordinate from a cell name: 'C4' becomes {col:3,row:4} - let res = addr.match(/([A-Z]+)([0-9]+)/); // res[1] is column name, res[2] is line index - if ( !Array.isArray(res) || !res[1] || !res[2] ) - throw Error("Incomplete input data: Cell without address!"); - this.col = colIdx(res[1]); - this.row = parseInt(res[2]); - } - } - class Worksheet { - name: string; - data: any; - resClass: string; - hid: string; - range: string; - firstCell: Coord; - lastCell: Coord; - constructor(wsN: string) { - this.name = wsN; // the name of the selected sheet (first has index '0'!) - this.data = wb.Sheets[wsN], - this.resClass = resClassId( pN + '-' + wsN ); - - // ToDo: Check if the type name does not yet exist. Should not occur as Excel does not allow equal sheet names in a file. - this.hid = 'H-' + simpleHash(pN + wsN) // the hierarchy ID of the folder carrying all resources of the selected sheet - this.range = this.data["!ref"]; // e.g. A1:C25 - if (this.range) { - // only if the sheet has content: - this.firstCell = new Coord(this.range.split(":")[0]); - this.lastCell = new Coord(this.range.split(":")[1]); - } - else - throw Error("Incomplete input data: Worksheet without range!"); - } - } - class BaseTypes { - dataTypes: DataType[]; - propertyClasses: PropertyClass[]; - resourceClasses: ResourceClass[]; - statementClasses: StatementClass[]; - resources: Resource[]; - statements: Statement[]; - hierarchies: SpecifNode[]; - constructor() { - this.dataTypes = [ - standardTypes.get("dataType", "DT-ShortString") as DataType, - standardTypes.get("dataType", "DT-Text") as DataType, - standardTypes.get("dataType", "DT-DateTime") as DataType, - standardTypes.get("dataType", "DT-Boolean") as DataType, - standardTypes.get("dataType", "DT-Integer") as DataType, - standardTypes.get("dataType", "DT-Real") as DataType - ]; - this.propertyClasses = [ - standardTypes.get("propertyClass", "PC-Name") as PropertyClass, - standardTypes.get("propertyClass", "PC-Description") as PropertyClass, - standardTypes.get("propertyClass", "PC-Type") as PropertyClass - ]; - this.resourceClasses = [ - standardTypes.get("resourceClass", "RC-Folder") as ResourceClass - ]; - // user-created instances are not checked for visibility: - this.resourceClasses[0].instantiation = [Instantiation.User]; - this.statementClasses = []; - this.resources = []; - this.statements = []; - this.hierarchies = []; - } - } - function dataTypeId( str:string ):string { - // must be able to find it just knowing the ws-name and the column index: - return 'DT-' + simpleHash(str); - } - class PropClass { - id: string; - title: string; - dataType: string; - changedAt: string; - constructor (nm: string, ti: string, dT: string) { - this.id = propClassId(nm); - // ti = vocabulary.property.specif(ti); has already be translated before calling - this.title = ti; - this.dataType = 'DT-' + dT; // like baseTypes[i].id - this.changedAt = chAt; - } - } - function propClassId( str:string ):string { - // must be able to find it just knowing the ws-name and the column index: - return 'PC-' + simpleHash(str); + interface ICell { + t: string; + w: string; + v: string|number|boolean|Date; + } + class Coord { + col: number; + row: number; + constructor(addr: string) { + // create a coordinate from a cell name: 'C4' becomes {col:3,row:4} + let res = addr.match(/([A-Z]+)([0-9]+)/); // res[1] is column name, res[2] is line index + if ( !Array.isArray(res) || !res[1] || !res[2] ) + throw Error("Incomplete input data: Cell without address!"); + this.col = colIdx(res[1]); + this.row = parseInt(res[2]); } - class ResClass { - id: string; - title: string; - description: string; - icon?: string; - instantiation: Instantiation[]; - propertyClasses: PropertyClass[]; - changedAt: string; - constructor(nm: string, ti: string) { - this.id = nm; - this.title = vocabulary.resource.specif(ti); - let ic = CONFIG.icons.get(this.title); - if (ic) this.icon = ic; - this.description = 'For resources specified per line of an excel sheet'; - this.instantiation = [Instantiation.User]; // user-created instances are not checked for visibility - this.propertyClasses = []; - this.changedAt = chAt; + } + class Worksheet { + name: string; + data: any; + resClass: string; + hid: string; + range: string; + firstCell: Coord; + lastCell: Coord; + constructor(wsN: string) { + this.name = wsN; // the name of the selected sheet (first has index '0'!) + this.data = wb.Sheets[wsN], + this.resClass = resClassId( pN + '-' + wsN ); + + // ToDo: Check if the type name does not yet exist. Should not occur as Excel does not allow equal sheet names in a file. + this.hid = 'H-' + simpleHash(pN + wsN) // the hierarchy ID of the folder carrying all resources of the selected sheet + this.range = this.data["!ref"]; // e.g. A1:C25 + if (this.range) { + // only if the sheet has content: + this.firstCell = new Coord(this.range.split(":")[0]); + this.lastCell = new Coord(this.range.split(":")[1]); } + else + throw Error("Incomplete input data: Worksheet without range!"); } - function resClassId( str:string ):string { - return 'RC-' + simpleHash(str); - } - class StaClass { - id: string; - title: string; - description: string; - instantiation: Instantiation[]; - changedAt: string; - constructor(ti: string) { - this.title = vocabulary.statement.specif(ti); - this.id = staClassId(this.title); - this.description = 'For statements created by columns whose title is declared as a statement'; - this.instantiation = [Instantiation.User]; // user-created instances are not checked for visibility - // No subjectClasses or objectClasses means all are allowed. - // Cannot specify any, as we don't know the resourceClasses. - this.changedAt = chAt; - } + } + class BaseTypes { + dataTypes: DataType[]; + propertyClasses: PropertyClass[]; + resourceClasses: ResourceClass[]; + statementClasses: StatementClass[]; + resources: Resource[]; + statements: Statement[]; + hierarchies: SpecifNode[]; + constructor() { + this.dataTypes = [ + standardTypes.get("dataType", "DT-ShortString") as DataType, + standardTypes.get("dataType", "DT-Text") as DataType, + standardTypes.get("dataType", "DT-DateTime") as DataType, + standardTypes.get("dataType", "DT-Boolean") as DataType, + standardTypes.get("dataType", "DT-Integer") as DataType, + standardTypes.get("dataType", "DT-Real") as DataType + ]; + this.propertyClasses = [ + standardTypes.get("propertyClass", "PC-Name") as PropertyClass, + standardTypes.get("propertyClass", "PC-Description") as PropertyClass, + standardTypes.get("propertyClass", "PC-Type") as PropertyClass + ]; + this.resourceClasses = [ + standardTypes.get("resourceClass", "RC-Folder") as ResourceClass + ]; + // user-created instances are not checked for visibility: + this.resourceClasses[0].instantiation = [Instantiation.User]; + this.statementClasses = []; + this.resources = []; + this.statements = []; + this.hierarchies = []; } - function staClassId( str:string ):string { - return 'SC-' + simpleHash(str); + } + function dataTypeId( str:string ):string { + // must be able to find it just knowing the ws-name and the column index: + return 'DT-' + simpleHash(str); + } + class PropClass { + id: string; + title: string; + dataType: string; + changedAt: string; + constructor (nm: string, ti: string, dT: string) { + this.id = propClassId(nm); + // ti = vocabulary.property.specif(ti); has already be translated before calling + this.title = ti; + this.dataType = 'DT-' + dT; // like baseTypes[i].id + this.changedAt = chAt; } - function colName( colI:number ):string { - // get the column name from an index: 4->'D', 27->'AA' - function cName( idx:number, res:string):string { - if( idx<1 ) return res; - let r = idx % 26, - f = (idx-r)/26; - res = String.fromCharCode(r+64) + res; - return cName(f, res); - } - return cName(colI,''); + } + function propClassId( str:string ):string { + // must be able to find it just knowing the ws-name and the column index: + return 'PC-' + simpleHash(str); + } + class ResClass { + id: string; + title: string; + description: string; + icon?: string; + instantiation: Instantiation[]; + propertyClasses: PropertyClass[]; + changedAt: string; + constructor(nm: string, ti: string) { + this.id = nm; + this.title = vocabulary.resource.specif(ti); + let ic = CONFIG.icons.get(this.title); + if (ic) this.icon = ic; + this.description = 'For resources specified per line of an excel sheet'; + this.instantiation = [Instantiation.User]; // user-created instances are not checked for visibility + this.propertyClasses = []; + this.changedAt = chAt; } - function cellName(col: number, row: number): string { - return colName(col) + row; + } + function resClassId( str:string ):string { + return 'RC-' + simpleHash(str); + } + class StaClass { + id: string; + title: string; + description: string; + instantiation: Instantiation[]; + changedAt: string; + constructor(ti: string) { + this.title = vocabulary.statement.specif(ti); + this.id = staClassId(this.title); + this.description = 'For statements created by columns whose title is declared as a statement'; + this.instantiation = [Instantiation.User]; // user-created instances are not checked for visibility + // No subjectClasses or objectClasses means all are allowed. + // Cannot specify any, as we don't know the resourceClasses. + this.changedAt = chAt; } - function colIdx(colN: string): number { - // transform column name to column index, e.g. 'C'->3 or 'AB'->28 - let idx = 0, f = 1; - for (var i = colN.length - 1; i > -1; i--) { - idx += f * (colN.charCodeAt(i) - 64); - f *= 26; - }; - return idx; + } + function staClassId( str:string ):string { + return 'SC-' + simpleHash(str); + } + function colName( colI:number ):string { + // get the column name from an index: 4->'D', 27->'AA' + function cName( idx:number, res:string):string { + if( idx<1 ) return res; + let r = idx % 26, + f = (idx-r)/26; + res = String.fromCharCode(r+64) + res; + return cName(f, res); } + return cName(colI,''); + } + function cellName(col: number, row: number): string { + return colName(col) + row; + } + function colIdx(colN: string): number { + // transform column name to column index, e.g. 'C'->3 or 'AB'->28 + let idx = 0, f = 1; + for (var i = colN.length - 1; i > -1; i--) { + idx += f * (colN.charCodeAt(i) - 64); + f *= 26; + }; + return idx; + } function collectMetaData(ws:Worksheet):void { if (!ws) return; @@ -316,13 +316,13 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { for ( a=res.properties.length-1; a>-1; a--) { pC = itemById(specifData.propertyClasses as Item[], res.properties[a]['class'] as string); if ( pC && CONFIG.titleProperties.indexOf(pC.title) > -1 ) - return stripHTML(res.properties[a].value); + return res.properties[a].value.stripHTML(); }; // then try to find a property with title listed in CONFIG.idProperties: for ( a=res.properties.length-1; a>-1; a--) { pC = itemById(specifData.propertyClasses as Item[], res.properties[a]['class'] as string); if (pC && CONFIG.idProperties.indexOf(pC.title) > -1 ) - return stripHTML(res.properties[a].value); + return res.properties[a].value.stripHTML(); }; }; return ''; @@ -388,7 +388,8 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { obL:string[], oInner:string[]; for (c = ws.firstCell.col, C = ws.lastCell.col + 1; c < C; c++) { // an attribute per column ... - pTi = ws.data[cellName(c, ws.firstCell.row)].v; // column title in the first line + cell = ws.data[cellName(c, ws.firstCell.row)]; // column title in the first row + pTi = cell ? (cell.v as string) : ''; // skip the column, if it has no title (value in the first row): if ( !pTi ) continue; @@ -405,7 +406,9 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { val = getVal({ type: pC.type }, cell); // @ts-ignore - check is defined in this case if (pC.check(val)) { + // @ts-ignore - name is defined in this case res[pC.name] = val; + // @ts-ignore - name is defined in this case console.info(ws.name + ", row " + row + ": '"+pTi+"' with value '" + val + "' has been mapped to the native property '" + pC.name + "'"); } else @@ -513,7 +516,7 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { } else { // No id specified, so a random value must be generated. // No chance to update the element later on! - res.id = genID('R-'); + res.id = Lib.genID('R-'); }; res.title = titleFromProps( res ); // accept only resources with title: @@ -606,7 +609,7 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { pC = getPropClass(c); // .. and create the propertyClass: if( pC ) { - cacheE( specifData.propertyClasses, pC ); // add it to propertyClasses, avoid duplicates + Lib.cacheE( specifData.propertyClasses, pC ); // add it to propertyClasses, avoid duplicates pCs.push(pC.id); // add the key to the resourceClass' propertyClasses }; }; @@ -720,7 +723,7 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { // 3.1 Create a resourceClass per XLS-sheet: // The resourceClass' title is taken from the worksheet name, project name or a default is applied: - var rC = new ResClass(ws.resClass, inBracketsOf(ws.name) || inBracketsOf(pN) || CONFIG.resClassXlsRow ); + var rC = new ResClass(ws.resClass, inBracketsAtEnd(ws.name) || inBracketsAtEnd(pN) || CONFIG.resClassXlsRow ); // Add a property class for each column using the names specified in line 1: rC.propertyClasses = getPropClasses( ws ); // console.debug('rC',rC); @@ -796,10 +799,11 @@ function xslx2specif(buf: ArrayBuffer, pN:string, chAt:string):SpecIF { function isFalse(str: string): boolean { return CONFIG.valuesFalse.indexOf(str.toLowerCase().trim()) > -1; } - function inBracketsOf(str:string):string { - // Extract resourceClass in [square brackets] or (round brackets): + function inBracketsAtEnd(str:string):string|undefined { + // Extract resourceClass in (round brackets) or [square brackets]: // let resL = /\s*(?:\(|\[)([a-zA-Z0-9:_\-].+?)(?:\)|\])$/.exec( pN ); - let resL = RE.inBrackets.exec(str); - if (Array.isArray(resL) && resL.length > 1) return resL[1]; + let resL = RE.inBracketsAtEnd.exec(str); + if (Array.isArray(resL) && resL.length > 1) + return resL[1] || resL[2]; } }); diff --git a/src/modules/reports.mod.ts b/src/modules/reports.mod.ts index daa133c..91c8e34 100644 --- a/src/modules/reports.mod.ts +++ b/src/modules/reports.mod.ts @@ -54,7 +54,7 @@ moduleManager.construct({ self.hide(); self.clear(); // This is a sub-module to specs, so use its return method: - stdError(xhr,pData.returnToCaller) + Lib.stdError(xhr,pData.returnToCaller) } function showNotice(txt) { $('#'+CONFIG.reports).html('
'+txt+'
'); @@ -69,6 +69,7 @@ moduleManager.construct({ pData.showLeft.reset(); // Language options have been selected at project level: + opts.lookupLanguage = true; opts.targetLanguage = pData.targetLanguage; opts.lookupTitles = true; opts.lookupValues = true; @@ -92,7 +93,7 @@ moduleManager.construct({ // but not navigation in the browser history: if( !opts || !opts.urlParams ) setUrlParams({ - project: dta.id, + project: prj.id, view: self.view.substr(1) // remove leading hash }); @@ -109,7 +110,7 @@ moduleManager.construct({ scaleMax: 0, datasets: [] }; - pr.resourceClasses.forEach( function( rC ) { + pr.resourceClasses.forEach( ( rC ) =>{ // Add a counter for each resourceClass if( CONFIG.excludedFromTypeFiltering.indexOf(rC.title)<0 ) rCR.datasets.push({ @@ -133,7 +134,7 @@ moduleManager.construct({ scaleMax: 0, datasets: [] }; - pr.statementClasses.forEach( function( sC ) { + pr.statementClasses.forEach( ( sC ) =>{ // Add a counter for each resourceClass if( CONFIG.excludedFromTypeFiltering.indexOf(sC.title)<0 ) sCR.datasets.push({ @@ -152,7 +153,7 @@ moduleManager.construct({ // Look up the dataType and create a counter for all possible enumerated values: for( var d=0, D=prj.dataTypes.length; d{ // add a counter for resources whose properties have a certain value (one per enumerated value) rep.datasets.push({ label: i18n.lookup( languageValueOf( val.value, opts )), @@ -176,10 +177,10 @@ moduleManager.construct({ // Add a report with a counter per enumerated property of all resource types: let pC; - dta.resourceClasses.forEach( function(rC) { - rC.propertyClasses.forEach( function(id) { - pC = itemById( dta.propertyClasses, id ); - if( itemById( dta.dataTypes, pC.dataType ).type=='xs:enumeration' ) { + dta.get("resourceClass","all").forEach( (rC) =>{ + rC.propertyClasses.forEach( (id) =>{ + pC = dta.get("propertyClass", id )[0]; + if( dta.get("dataType", pC.dataType )[0].type=='xs:enumeration' ) { var aVR = { title: titleOf(rC,opts)+': '+titleOf(pC,opts), category: 'enumValue', @@ -234,12 +235,12 @@ moduleManager.construct({ if( j>-1 ) incVal( 0,j ); // b) The histograms of all enumerated properties: - let rC = itemById( dta.resourceClasses, rId ); + let rC = dta.get("resourceClass", rId )[0]; // there is a report for every enumerated resourceClass: let dT=null,oa=null,i=null,ct=null,pC; - rC.propertyClasses.forEach( function(pId) { - pC = itemById( dta.propertyClasses, pId ); - dT = itemById( dta.dataTypes, pC.dataType ); + rC.propertyClasses.forEach( (pId) =>{ + pC = dta.get("propertyClass", pId )[0]; + dT = dta.get("dataType", pC.dataType )[0]; if( dT.type!='xs:enumeration' ) return; // find the report panel: i = findPanel(self.list,rId,pId); @@ -252,7 +253,7 @@ moduleManager.construct({ // has a value: // console.debug( 'evalResource a', oa ); ct = oa.value.split(','); - ct.forEach( function(val) { + ct.forEach( (val) =>{ // find the bar which corresponds to the property values j = indexById( self.list[i].datasets, val.trim() ); // console.debug( 'evalResource z', ct, j ); @@ -270,7 +271,7 @@ moduleManager.construct({ // we must go through the tree because not all resources may be cached, // but we must avoid to evaluate every resource more than once: let pend=0, visitedR=[]; - pData.tree.iterate( function(nd) { + pData.tree.iterate( (nd) =>{ if( visitedR.indexOf(nd.ref)>-1 ) return; // not yet evaluated: pend++; @@ -303,12 +304,12 @@ moduleManager.construct({ function renderReports(list) { var rs = '
'; let lb; - list.forEach( function(li,i) { + list.forEach( (li,i) =>{ rs += '
' + '

'+li.title+'

' + '' + ''; - li.datasets.forEach( function(ds,s) { + li.datasets.forEach( (ds,s) =>{ lb = ds.count>0? ''+ds.label+'' : ds.label; rs += '' + '' @@ -329,7 +330,7 @@ moduleManager.construct({ // Determine panel height. // So far, all panels get the same size depending on the longest dataset. let maxSets = 0; - L.forEach( function(p) { + L.forEach( (p) =>{ maxSets = Math.max( maxSets, p.datasets.length ) }); return ( 3+maxSets*1.8+'em' ) diff --git a/src/modules/resourceEdit.mod.ts b/src/modules/resourceEdit.mod.ts index f8d7673..3ee2bf8 100644 --- a/src/modules/resourceEdit.mod.ts +++ b/src/modules/resourceEdit.mod.ts @@ -50,7 +50,7 @@ class DialogForm { }; setTextState(cPs.label, ok ? 'has-success' : 'has-error'); allOk = allOk && ok; - // console.debug( 'DialogForm.check: ', cPs, val ); +// console.debug( 'DialogForm.check: ', cPs, val ); }); return allOk; } @@ -64,7 +64,7 @@ moduleManager.construct({ let myName = self.loadAs, myFullName = 'app.'+myName, pData = self.parent, // the parent's data - cData:CSpecIF, // the cached data + cData:SpecIF, // the cached data opts:any, // the processing options toEdit:CResourceToShow; // the resource with classified properties to edit @@ -163,10 +163,10 @@ moduleManager.construct({ ]; editResource(r,opts); }, - stdError + Lib.stdError ); }, - stdError + Lib.stdError ); break; case 'clone': @@ -179,7 +179,7 @@ moduleManager.construct({ // create a clone to collect the changed values before committing: self.newRes = simpleClone(rL[0]); if( opts.mode=='clone' ) { - self.newRes.id = genID('R-'); + self.newRes.id = Lib.genID('R-'); opts.dialogTitle = i18n.MsgCloneResource, opts.msgBtns = [ msgBtns.cancel, @@ -195,7 +195,7 @@ moduleManager.construct({ }; editResource(self.newRes,opts) }, - stdError + Lib.stdError ); }; return; @@ -204,7 +204,7 @@ moduleManager.construct({ // Edit/update the resources properties: // console.debug( 'editResource', res, simpleClone(cData.resourceClasses) ); // complete and sort the properties according to their role (title, descriptions, ..): - toEdit = new CResourceToShow( res, cData ); + toEdit = new CResourceToShow( res ); let ti = i18n.lookup(CONFIG.propClassTitle); // @ts-ignore - BootstrapDialog() is loaded at runtime new BootstrapDialog({ @@ -240,11 +240,12 @@ moduleManager.construct({ function editP(p) { // Return a form element for a property; // works only, if all propertyClasses and dataTypes are always cached: - let pC = itemById( cData.propertyClasses, p['class'] ), + let pC = cData.get("propertyClass", p['class'] )[0], // title and description may not have a propertyClass (e.g. Tutorial 2 "Related terms"): - dT = pC? itemById( cData.dataTypes, pC.dataType ) : undefined, + dT = pC? cData.get("dataType", pC.dataType )[0] : undefined, opts = { lookupTitles: true, + lookupLanguage: true, targetLanguage: browser.language, imgClass: 'forImagePreview' }, @@ -257,7 +258,8 @@ moduleManager.construct({ if (propTitleOf(p, cData) == CONFIG.propClassDiagram) { // it is a diagram reference (works only with XHTML-fields): return renderDiagram(p, opts) - } else { + } + else { // add parameters to check this input field: self.dialogForm.addField(ti, dT); // it is a text; @@ -278,11 +280,12 @@ moduleManager.construct({ case 'xs:enumeration': // no input checking needed: let separatedValues = p.value.split(','), - vals = forAll( dT.values, (v)=>{ return {title:i18n.lookup(languageValueOf(v.value,opts)),id:v.id,checked:separatedValues.indexOf(v.id)>-1} }); + vals = Lib.forAll( dT.values, (v)=>{ return {title:i18n.lookup(languageValueOf(v.value,opts)),id:v.id,checked:separatedValues.indexOf(v.id)>-1} }); // console.debug('xs:enumeration',ti,p,pC,separatedValues,vals); if( typeof(pC.multiple)=='boolean'? pC.multiple : dT.multiple ) { return checkboxField( ti, vals ); - } else { + } + else { return radioField( ti, vals ); }; case 'xs:boolean': @@ -334,12 +337,12 @@ moduleManager.construct({ function selectResClass( opts ) { // Let the user choose the class of the resource to be created later on: return new Promise((resolve, reject) => { - app.cache.selectedProject.readContent( 'resourceClass', forAll( opts.eligibleResourceClasses, (rCId)=>{return {id:rCId}} )) + app.cache.selectedProject.readContent( 'resourceClass', Lib.forAll( opts.eligibleResourceClasses, (rCId)=>{return {id:rCId}} )) .then( (rCL)=>{ if( rCL.length>0 ) { // store a clone and get the title to display: - let resClasses = forAll( simpleClone( rCL ), (rC)=>{ rC.title=titleOf(rC,{lookupTitles:true}); return rC } ); + let resClasses = Lib.forAll( simpleClone( rCL ), (rC)=>{ rC.title=titleOf(rC,{lookupTitles:true}); return rC } ); if( resClasses.length>1 ) { // open a modal dialog to let the user select the class for the resource to create: resClasses[0].checked = true; // default selection @@ -373,11 +376,13 @@ moduleManager.construct({ }] }) .open(); - } else { + } + else { // exactly on class, so we can continue immediately: resolve( resClasses[0] ); }; - } else { + } + else { // ToDo: Don't enable the 'create resource' button, if there are no eligible resourceClasses .. reject({status:999,statusText:"No resource class defined for manual creation of a resource."}); }; @@ -449,7 +454,7 @@ moduleManager.construct({ toEdit.title.value = getP( toEdit.title ); // In any case, update the elements native title: - self.newRes.title = stripHTML(toEdit.title.value); + self.newRes.title = toEdit.title.value.stripHTML(); // If the title property doesn't have a class, // it has been added by new CResourceToShow() and there is no need to create it; // in this case the title will only be seen in the element's title: @@ -476,7 +481,7 @@ moduleManager.construct({ p.value = getP( p ); delete p.title; - let pV = stripHTML(p.value); + let pV = p.value.stripHTML(); if( pV ) { // update the elements native description: self.newRes.description = pV @@ -490,7 +495,8 @@ moduleManager.construct({ else self.newRes.properties = [ p ]; }; - } else { + } + else { // delete it: delete self.newRes.description; }; @@ -504,13 +510,14 @@ moduleManager.construct({ // a property class must exist, // because new CResourceToShow() puts only existing properties to 'other': if( p['class'] ) { - if( hasContent(p.value) ) { + if( Lib.hasContent(p.value) ) { if( Array.isArray( self.newRes.properties ) ) self.newRes.properties.push( p ); else self.newRes.properties = [ p ]; }; - } else { + } + else { console.error('Cannot save edited property',p,' because it has no class'); }; }); @@ -518,35 +525,29 @@ moduleManager.construct({ self.newRes.changedAt = chD; // console.debug( 'save', self.newRes ); + app.cache.selectedProject.updateContent('resource', self.newRes) + .then(finalize, Lib.stdError); switch( mode ) { - case 'update': - app.cache.selectedProject.updateContent( 'resource', self.newRes ) - .then( finalize, stdError ); - break; + // case 'update': + // break; case 'insert': - app.cache.selectedProject.createContent( 'resource', self.newRes ) - .then( finalize, stdError ); pend++; - app.cache.selectedProject.createContent( 'node', {id:genID('N-'),resource:self.newRes.id,changedAt:chD} ) - .then( finalize, stdError ); + app.cache.selectedProject.createContent( 'node', {id:Lib.genID('N-'),resource:self.newRes.id,changedAt:chD} ) + .then( finalize, Lib.stdError ); break; case 'insertAfter': - app.cache.selectedProject.createContent( 'resource', self.newRes ) - .then( finalize, stdError ); pend++; - app.cache.selectedProject.createContent( 'node', {id:genID('N-'),resource:self.newRes.id,changedAt:chD,predecessor:opts.selNodeId} ) - .then( finalize, stdError ); + app.cache.selectedProject.createContent( 'node', {id:Lib.genID('N-'),resource:self.newRes.id,changedAt:chD,predecessor:opts.selNodeId} ) + .then( finalize, Lib.stdError ); break; case 'insertBelow': - app.cache.selectedProject.createContent( 'resource', self.newRes ) - .then( finalize, stdError ); pend++; - app.cache.selectedProject.createContent( 'node', {id:genID('N-'),resource:self.newRes.id,changedAt:chD,parent:opts.selNodeId} ) - .then( finalize, stdError ); + app.cache.selectedProject.createContent( 'node', {id:Lib.genID('N-'),resource:self.newRes.id,changedAt:chD,parent:opts.selNodeId} ) + .then( finalize, Lib.stdError ); }; // has no effect, if newFiles is empty: app.cache.selectedProject.createContent( 'file', self.newFiles ) - .then( finalize, stdError ); + .then( finalize, Lib.stdError ); return; function finalize() { @@ -554,6 +555,7 @@ moduleManager.construct({ // update the tree because the title may have changed: pData.updateTree({ lookupTitles: true, + lookupLanguage: true, targetLanguage: browser.language }); // get the selected node: @@ -587,11 +589,12 @@ moduleManager.construct({ // ToDo: Works only, if all propertyClasses are always cached: const opts = { lookupTitles: true, + lookupLanguage: true, targetLanguage: browser.language }; - let pC = itemById( cData.propertyClasses, p['class'] ), + let pC = cData.get("propertyClass", p['class'] )[0], // title and description may not have a propertyClass (e.g. Tutorial 2 "Related terms"): - dT = pC? itemById( cData.dataTypes, pC.dataType ) : undefined; + dT = pC? cData.get("dataType", pC.dataType )[0] : undefined; switch( dT? dT.type : "xs:string" ) { case 'xs:integer': case 'xs:double': diff --git a/src/modules/resourceLink.mod.ts b/src/modules/resourceLink.mod.ts index 93642d8..447684c 100644 --- a/src/modules/resourceLink.mod.ts +++ b/src/modules/resourceLink.mod.ts @@ -60,7 +60,7 @@ moduleManager.construct({ selRes = rL[0]; createStatement( opts ) }, - stdError + Lib.stdError ); // console.debug('resourceLink.show',opts); @@ -77,7 +77,7 @@ moduleManager.construct({ // 1. get the eligible statementClasses and all referenced resources in parallel and then create the desired statement: self.eligibleSCL.length=0; opts.eligibleStatementClasses.subjectClasses.concat(opts.eligibleStatementClasses.objectClasses).forEach( (sCId)=>{ - cacheE( self.eligibleSCL, sCId ) // avoid duplicates + Lib.cacheE( self.eligibleSCL, sCId ) // avoid duplicates }); app.cache.selectedProject.readContent( 'statementClass', self.eligibleSCL ) .then( @@ -85,7 +85,7 @@ moduleManager.construct({ self.eligibleSCL = list; // now self.eligibleSCL contains the full statementClasses chooseResourceToLink() }, - stdError + Lib.stdError ); // 2. collect all statements of the originally selected resource to exclude them from selection: @@ -95,14 +95,14 @@ moduleManager.construct({ self.selResStatements = list; chooseResourceToLink() }, - stdError + Lib.stdError ); // 3. collect all referenced resources avoiding duplicates: self.allResources.length=0; - iterateNodes( cData.hierarchies, + Lib.iterateNodes( cData.hierarchies, (nd:SpecifNode)=>{ - cacheE( self.allResources, nd.resource ); + Lib.cacheE( self.allResources, nd.resource ); // self.allResources contains the resource ids return true // iterate the whole tree } @@ -112,7 +112,7 @@ moduleManager.construct({ (list:Resource[])=>{ // Sort the resources: - sortBy( + Lib.sortBy( list, (el)=>{ return elementTitleOf(el,opts,cData) } ); @@ -120,7 +120,7 @@ moduleManager.construct({ // now self.allResources contains the full resources chooseResourceToLink() }, - stdError + Lib.stdError ); return @@ -129,7 +129,7 @@ moduleManager.construct({ if( --pend<1 ) { // all parallel requests are done, // store a clone and get the title to display: - let staClasses = forAll( + let staClasses = Lib.forAll( self.eligibleSCL, (sC)=>{ return {title:titleOf(sC,{lookupTitles:true}),description:languageValueOf(sC.description,opts)}} ); @@ -174,7 +174,7 @@ moduleManager.construct({ ()=>{ pData.doRefresh({forced:true}) }, - stdError + Lib.stdError ); thisDlg.close() } @@ -188,7 +188,7 @@ moduleManager.construct({ ()=>{ pData.doRefresh({forced:true}) }, - stdError + Lib.stdError ); thisDlg.close() } @@ -319,7 +319,7 @@ moduleManager.construct({ self.saveStatement = (dir):void =>{ // console.debug('saveStatement',selRes, self.selectedStatementClass, self.selectedCandidate.resource,dir.secondAs); return app.cache.selectedProject.createContent( 'statement', { - id:genID('S-'), + id: Lib.genID('S-'), class: self.selectedStatementClass.id, subject: ( dir.secondAs=='object'? selRes.id : self.selectedCandidate.resource.id ), object: ( dir.secondAs=='object'? self.selectedCandidate.resource.id : selRes.id ), diff --git a/src/modules/specifications.mod.ts b/src/modules/specifications.mod.ts index 114af3a..2ea98b1 100644 --- a/src/modules/specifications.mod.ts +++ b/src/modules/specifications.mod.ts @@ -34,11 +34,14 @@ class CPropertyToShow implements Property { } isVisible(opts: any): boolean { return (CONFIG.hiddenProperties.indexOf(this.title)<0 // not listed as hidden - && (CONFIG.showEmptyProperties || hasContent(languageValueOf(this.value, opts)))) + && (CONFIG.showEmptyProperties || Lib.hasContent(languageValueOf(this.value, opts)))) } get( opts?: any): string { if (typeof (opts) != 'object') opts = {}; + opts.lookupLanguage = true; if (typeof (opts.dynLinks) != 'boolean') opts.dynLinks = false; + if (!Array.isArray(opts.titlelLinkTargets)) opts.titleLinkTargets = CONFIG.modelElementClasses.concat(CONFIG.diagramClasses); + if (typeof (opts.clickableElements) != 'boolean') opts.clickableElements = false; if (typeof (opts.linkifyURLs) != 'boolean') opts.linkifyURLs = false; // some environments escape the tags on export, e.g. camunda / in|flux: @@ -68,7 +71,7 @@ class CPropertyToShow implements Property { ct = this.titleLinks(ct, opts); break; case TypeEnum.XsDateTime: - ct = localDateTime(this.value); + ct = Lib.localDateTime(this.value); break; case TypeEnum.XsEnumeration: // Usually 'value' has a comma-separated list of value-IDs, @@ -99,8 +102,8 @@ class CPropertyToShow implements Property { // @ts-ignore - $0 is never read, but must be specified anyways return str.replace(RE.titleLink, ($0, $1) => { return $1 }); - /* let date1 = new Date(); - let n1 = date1.getTime(); */ + /* let date1 = new Date(); + let n1 = date1.getTime(); */ // else, find all dynamic link patterns in the current property and replace them by a link, if possible: let replaced = false; @@ -112,19 +115,22 @@ class CPropertyToShow implements Property { replaced = true; // disregard links being too short: if ($1.length < CONFIG.dynLinkMinLength) return $1; - let m = $1.toLowerCase(), cO = null, ti: string, target = null, notFound = true; + let m = $1.toLowerCase(), cR: Resource, ti: string, rC:ResourceClass, target: Resource; // is ti a title of any resource? app.specs.tree.iterate((nd: jqTreeNode) => { - cO = itemById(app.cache.selectedProject.data.resources, nd.ref); + cR = itemById(app.cache.selectedProject.data.resources, nd.ref); // avoid self-reflection: - // if(ob.id==cO.id) return true; - ti = elementTitleOf(cO, opts); - // if the dynLink content equals a resource's title, remember the first occurrence: - if (notFound && ti && m == ti.toLowerCase()) { - notFound = false; - target = cO; - }; - return notFound // go into depth (return true) only if not yet found + // if(ob.id==cR.id) return true; + ti = elementTitleOf(cR, opts); + if (!ti || m != ti.toLowerCase()) return true; // continue searching + + // disregard link targets which aren't diagrams nor model elements: + rC = itemById(app.cache.selectedProject.data.resourceClasses, cR['class']); + if (opts.titleLinkTargets.indexOf(rC.title) < 0) return true; // continue searching + + // the dynLink content equals a resource's title, remember the first occurrence: + target = cR; + return false; // found, stop searching! }); // replace it with a link in case of a match: if (target) @@ -135,9 +141,9 @@ class CPropertyToShow implements Property { ) } while (replaced); - /* let date2 = new Date(); - let n2 = date2.getTime(); - console.info( 'dynamic linking in ', n2-n1,'ms' ) */ + /* let date2 = new Date(); + let n2 = date2.getTime(); + console.info( 'dynamic linking in ', n2-n1,'ms' ) */ return str; function lnk(r: Resource, t: string): string { @@ -153,8 +159,8 @@ class CPropertyToShow implements Property { - an external hyperlink is to be included */ if (typeof (opts) != 'object') opts = {}; - if (opts.projId == undefined) opts.projId = app.cache.selectedProject.data.id; - // if( opts.rev==undefined ) opts.rev = 0; + // if (opts.projId == undefined) opts.projId = app.cache.selectedProject.id; + // if( opts.rev==undefined ) opts.rev = 0; if (opts.imgClass == undefined) opts.imgClass = 'forImage' // regular size /* function addFilePath( u ) { @@ -218,11 +224,11 @@ class CPropertyToShow implements Property { h2 = getPrpVal("height", $2), d = $4 || u1; // If there is no description, use the name of the link object - // console.debug('fileRef.toGUI nestedObject: ', $0,'|', $1,'|', $2,'|', $3,'|', $4,'||', u1,'|', t1,'|', w1, h1,'|', u2,'|', t2,'|', w2, h2,'|', d ); - if (!u1) console.warn('no file found in', $0); - if (!u2) console.warn('no image found in', $0); - // u1 = addFilePath(u1); - // u2 = addFilePath(u2); +// console.debug('fileRef.toGUI nestedObject: ', $0,'|', $1,'|', $2,'|', $3,'|', $4,'||', u1,'|', t1,'|', w1, h1,'|', u2,'|', t2,'|', w2, h2,'|', d ); + if (!u1) console.warn('no file found in '+$0); + if (!u2) console.warn('no image found in '+$0); + // u1 = addFilePath(u1); + // u2 = addFilePath(u2); let f1 = new CFileWithContent(itemByTitle(app.cache.selectedProject.data.files, u1)), f2 = new CFileWithContent(itemByTitle(app.cache.selectedProject.data.files, u2)); @@ -266,7 +272,7 @@ class CPropertyToShow implements Property { }; } ); - // console.debug('fileRef.toGUI 1: ', txt); +// console.debug('fileRef.toGUI 1: ', txt); // 2. transform a single object to link+object resp. link+image: txt = txt.replace(RE.tagSingleObject, // comprehensive tag or tag pair @@ -282,7 +288,7 @@ class CPropertyToShow implements Property { w1 = getPrpVal("width", $1), h1 = getPrpVal("height", $1); - let e = u1.fileExt(); + let e = u1? u1.fileExt() : undefined; if (!e) return $0 // no change, if no extension found // $3 is the description between the tags : @@ -292,16 +298,18 @@ class CPropertyToShow implements Property { // console.debug('fileRef.toGUI singleObject: ', $0,'|', $1,'|', $2,'|', $3,'||', u1,'|', t1 ); // u1 = addFilePath(u1); - if (!u1) console.info('no image found'); + if (!u1) console.warn('no image or link found in '+$0); let f1 = new CFileWithContent(itemByTitle(app.cache.selectedProject.data.files, u1)); + // sometimes the application files (BPMN or other) have been replaced by images; // this is for example the case for *.specif.html files: - if (!f1.hasContent() && CONFIG.applExtensions.indexOf(e) > -1) { + if (!f1.hasContent() && u1 && CONFIG.applExtensions.indexOf(e) > -1) { for (var i = 0, I = CONFIG.imgExtensions.length; !f1 && i < I; i++) { u1 = u1.fileName() + '.' + CONFIG.imgExtensions[i]; f1 = new CFileWithContent(itemByTitle(app.cache.selectedProject.data.files, u1)); }; }; + // ... cannot happen any more now, is still here for compatibility with older files only. if (CONFIG.imgExtensions.indexOf(e) > -1 || CONFIG.applExtensions.indexOf(e) > -1) { // it is an image, show it: @@ -347,23 +355,28 @@ class CPropertyToShow implements Property { }; } else { - switch (e) { + /* switch (e) { case 'ole': // It is an ole-file, so add a preview image; // in case there is no preview image, the browser will display d holding the description // IE: works, if preview is PNG, but a JPG is not displayed (perhaps because of wrong type ...) // But in case of IE it appears that even with correct type a JPG is not shown by an tag // ToDo: Check if there *is* a preview image and which type it has, use an tag. - hasImg = true; + if (f1.hasContent()) { + hasImg = true; // d = ''+d+''; - d = '' + d + ''; + d = '' + d + ''; + } + else { + d = '
File missing: ' + d + '
' + }; // ToDo: Offer a link for downloading the file break; - default: + default: */ // last resort is to take the filename: d = '' + d + ''; // ToDo: Offer a link for downloading the file - }; + // }; }; // finally add the link and an enclosing div for the formatting: @@ -378,14 +391,14 @@ class CPropertyToShow implements Property { return 'aBra§kadabra' + (repStrings.length - 1) + '§'; } ); - // console.debug('fileRef.toGUI 2: ', txt); +// console.debug('fileRef.toGUI 2: ', txt); // 3. process a single link: txt = txt.replace(RE.tagA, ($0, $1, $2) => { var u1 = getPrpVal('href', $1), - e = u1.fileExt(); - // console.debug( $1, $2, u1, e ); + e = u1? u1.fileExt() : undefined; +// console.debug( $1, $2, u1, e ); if (!e) return $0 // no change, if no extension found /* if( /(' + e + '') } ); - // console.debug('fileRef.toGUI 3: ', txt); +// console.debug('fileRef.toGUI 3: ', txt); // Now, at the end, replace the placeholders with the respective strings, txt = txt.replace(/aBra§kadabra([0-9]+)§/g, @@ -417,7 +430,7 @@ class CPropertyToShow implements Property { ($0, $1) => { return repStrings[$1] }); - // console.debug('fileRef.toGUI result: ', txt); +// console.debug('fileRef.toGUI result: ', txt); return txt } } @@ -433,11 +446,11 @@ class CResourceToShow { other: CPropertyToShow[]; changedAt: string; changedBy?: string; - constructor(el: Resource, pData?: CSpecIF) { + constructor(el: Resource) { // add missing (empty) properties and classify properties into title, descriptions and other; // for resources. // ToDo: Basically it can also be used for Statements ... - if (!pData) pData = app.cache.selectedProject.data; + let pData = app.cache.selectedProject.data; this.id = el.id; this['class'] = itemById(pData.resourceClasses, el['class']) as ResourceClass; this.isHeading = false; // will be set further down if appropriate @@ -499,7 +512,7 @@ class CResourceToShow { // Why create a description, if there is none ?? What is the use-case? // if (this.descriptions.length < 1) // this.descriptions.push( {title: CONFIG.propClassDesc, value: el.description || ''} ); - // console.debug( 'classifyProps 2', simpleClone(this) ); +// console.debug( 'classifyProps 2', simpleClone(this) ); } private normalizeProps(el: Resource, dta: CSpecIF): CPropertyToShow[] { // el: original instance (resource or statement) @@ -547,7 +560,7 @@ class CResourceToShow { nL.push(new CPropertyToShow(p)); } }); - // console.debug('normalizeProps result',simpleClone(nL)); +// console.debug('normalizeProps result',simpleClone(nL)); return nL; // normalized property list } isEqual(res: Resource): boolean { @@ -562,7 +575,7 @@ class CResourceToShow { // ToDo: Create a class for attributes .. cssCl = cssCl ? ' ' + cssCl : ''; if (typeof (val) == 'string') - val = noCode(val) + val = Lib.noCode(val) else val = ''; // assemble a label:value pair resp. a wide value field for display: @@ -581,31 +594,33 @@ class CResourceToShow { if (opts && opts.lookupTitles) ti = i18n.lookup(ti); // it is assumed that a heading never has an icon: - return '
' + (this.order ? this.order + nbsp : '') + ti + '
'; + return '
' + (this.order ? this.order + ' ' : '') + ti + '
'; }; // else: is not a heading: // take title and add icon, if configured: - return '
' + (CONFIG.addIconToInstance ? addIcon(ti, this['class'].icon) : ti) + '
'; + return '
' + (CONFIG.addIconToInstance ? Lib.addIcon(ti, this['class'].icon) : ti) + '
'; } renderChangeInfo(): string { if (!this.revision) return ''; // the view may be faster than the data, so avoid an error - var rChI = ''; + var chI = ''; switch (app.specs.selectedView()) { case '#' + CONFIG.objectRevisions: - rChI = this.renderAttr(i18n.LblRevision, this.revision, 'attribute-condensed'); - // no break + chI = this.renderAttr(i18n.LblRevision, this.revision, 'attribute-condensed'); + // no break case '#' + CONFIG.comments: - rChI += this.renderAttr(i18n.LblModifiedAt, localDateTime(this.changedAt), 'attribute-condensed') + chI += this.renderAttr(i18n.LblModifiedAt, Lib.localDateTime(this.changedAt), 'attribute-condensed') + this.renderAttr(i18n.LblModifiedBy, this.changedBy, 'attribute-condensed'); // default: no change info! }; - return rChI; + return chI; } listEntry(options?: any): string { if (!this.id) return '
' + i18n.MsgNoObject + '
'; // Create HTML for a list entry: var opts = options ? simpleClone(options) : {}; + opts.lookupLanguage = true; + opts.targetLanguage = browser.language; opts.dynLinks = opts.clickableElements = opts.linkifyURLs @@ -683,7 +698,7 @@ class CResourceToShow { // 2 The description properties: this.descriptions.forEach( function(prp) { // console.debug('details.descr',prp.value); - if( hasContent(prp.value) ) { + if( Lib.hasContent(prp.value) ) { var opts = { // dynLinks: [CONFIG.objectList, CONFIG.objectDetails].indexOf(app.specs.selectedView())>-1, dynLinks: true, @@ -737,9 +752,10 @@ class CResourceToShow { class CResourcesToShow { private opts = { lookupTitles: true, + lookupLanguage: true, targetLanguage: browser.language }; - values: any[]; + values: CResourceToShow[]; constructor() { this.values = []; @@ -855,7 +871,7 @@ class CFileWithContent implements IFileWithContent { // see: https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications // see: https://blog.logrocket.com/programmatic-file-downloads-in-the-browser-9a5186298d5c/ if (this.hasBlob()) - blob2dataURL(this, addL, opts.timelag); + Lib.blob2dataURL(this, addL, opts.timelag); else // assuming that dataURL has content: setTimeout(() => { addL(this.dataURL,this.title,this.type) }, opts.timelag); @@ -915,7 +931,7 @@ class CFileWithContent implements IFileWithContent { // end of renderImage() }; private showRaster(opts: any): void { - blob2dataURL(this, (r: string, fTi: string, fTy: string): void => { + Lib.blob2dataURL(this, (r: string, fTi: string, fTy: string): void => { // add image to DOM using an image-tag with data-URI: Array.from(document.getElementsByClassName(tagId(fTi)), (el) => { @@ -932,10 +948,10 @@ class CFileWithContent implements IFileWithContent { } private showSvg(opts: any): void { // Read and render SVG: - blob2text(this, displaySVGeverywhere, opts.timelag) + Lib.blob2text(this, displaySVGeverywhere, opts.timelag) return; - function itemBySimilarId(L: Item[], id: string): Item { + function itemBySimilarId(L: Item[], id: string): Item | undefined { // return the list element having an id similar to the specified one: id = id.trim(); for (var i = L.length - 1; i > -1; i--) @@ -944,7 +960,7 @@ class CFileWithContent implements IFileWithContent { if (L[i].id.indexOf(id) > -1) return L[i]; // return list item // return undefined } - function itemBySimilarTitle(L: Item[], ti: string): Item { + function itemBySimilarTitle(L: Item[], ti: string): Item|undefined { // return the list element having a title similar to the specified one: ti = ti.trim(); for (var i = L.length - 1; i > -1; i--) @@ -986,7 +1002,7 @@ class CFileWithContent implements IFileWithContent { pend++; // console.debug('SVG embedded file',mL[2],ef,pend); // transform file to data-URL and display, when done: - blob2dataURL(ef, (r: string, fTi: string): void => { + Lib.blob2dataURL(ef, (r: string, fTi: string): void => { dataURLs.push({ id: fTi, val: r @@ -1077,10 +1093,10 @@ class CFileWithContent implements IFileWithContent { // to avoid an endless recursive call, the property shall neither have dynLinks nor clickableElements dsc += d.get({ unescapeHTMLTags: true, makeHTML: true }) }); - if (stripHTML(stripCtrl(dsc))) { + if (dsc.stripCtrl().stripHTML()) { // Remove the dynamic linking pattern from the text: $("#details").html('' - + (CONFIG.addIconToInstance ? addIcon(ti, clsPrp['class'].icon) : ti) + + (CONFIG.addIconToInstance ? Lib.addIcon(ti, clsPrp['class'].icon) : ti) + '\n' + dsc); app.specs.showTree.set(false); @@ -1151,11 +1167,11 @@ class CFileWithContent implements IFileWithContent { } private showBpmn(opts: any): void { // Read and render BPMN: - blob2text(this, (t: string, fTi: string) => { + Lib.blob2text(this, (t: string, fTi: string) => { bpmn2svg(t) .then( (result) => { - // console.debug('SVG',result); +// console.debug('SVG',result); Array.from(document.getElementsByClassName(tagId(fTi)), (el) => { el.innerHTML = result.svg } ); @@ -1261,14 +1277,14 @@ moduleManager.construct({ document.getElementById(CONFIG.objectList).scrollTop = 0; self.refresh(); }, - stdError + Lib.stdError ); return; function toSpecIF(mNd: jqTreeNode, tgt: ITargetNode): INodeWithPosition { // transform from jqTree node to SpecIF node: var nd: INodeWithPosition = { - // id: genID('N-'), + // id: Lib.genID('N-'), id: mNd.id, resource: mNd.ref, changedAt: chd @@ -1296,17 +1312,19 @@ moduleManager.construct({ // (a) event.move_info.position=='position after': // The node is dropped between two nodes. moveNode( event.move_info.moved_node, {predecessor:event.move_info.target_node} ); - } else if( ire.test(event.move_info.position) ) { + } + else if (ire.test(event.move_info.position)) { // (b) event.move_info.position=='position inside': // The node is dropped on a target node without children or before the first node in a folder. moveNode( event.move_info.moved_node, {parent:event.move_info.target_node} ); - } else { + } + else { // (c) event.move_info.position=='position before': // The node is dropped before the first node in the tree: moveNode( event.move_info.moved_node, {parent:event.move_info.target_node.parent} ); }; }, - stdError + Lib.stdError ); } } @@ -1352,7 +1370,7 @@ moduleManager.construct({ case 201: return; // some calls end up in the fail trail, even though all went well. default: - stdError(xhr); + Lib.stdError(xhr); } } function setPermissions( nd ) { @@ -1390,7 +1408,8 @@ moduleManager.construct({ return false // no statement is available for this resource for which the user has creation rights }; self.staCre = mayHaveStatements( r ) - } else { + } + else { noPerms() } } */ @@ -1407,7 +1426,7 @@ moduleManager.construct({ let tr; // Replace the tree: if( Array.isArray( spc ) ) - tr = forAll( spc, toChild ); + tr = Lib.forAll( spc, toChild ); else tr = [toChild(spc)]; @@ -1429,7 +1448,7 @@ moduleManager.construct({ name: elementTitleOf(r,opts,pData), ref: iE.resource.id || iE.resource // for key (with revision) or for id (without revision) }; - oE.children = forAll( iE.nodes, toChild ); + oE.children = Lib.forAll( iE.nodes, toChild ); // if( typeof(iE.upd)=='boolean' ) oE.upd = iE.upd; if( iE.revision ) oE.revision = iE.revision; oE.changedAt = iE.changedAt; @@ -1441,11 +1460,10 @@ moduleManager.construct({ // called by the parent's view controller: self.show = function( opts:any ):void { // console.debug( CONFIG.specifications, 'show', opts ); - if( !(app.cache.selectedProject && app.cache.selectedProject.data && app.cache.selectedProject.data.id) ) { + if( !(app.cache.selectedProject && app.cache.selectedProject.isLoaded() ) ) throw Error("No selected project on entry of spec.show()"); - }; - $('#pageTitle').html( app.cache.selectedProject.data.title ); + $('#pageTitle').html( app.cache.selectedProject.title ); app.busy.set(); // $( self.view ).html( '
'+i18n.MsgInitialLoading+'
' ); // $('#specNotice').empty(); @@ -1455,6 +1473,7 @@ moduleManager.construct({ nd: jqTreeNode; // Select the language options at project level, also for subordinated views such as filter and reports: + opts.lookupLanguage = true; self.targetLanguage = opts.targetLanguage = browser.language; opts.lookupTitles = true; @@ -1462,17 +1481,17 @@ moduleManager.construct({ // - URL parameters are specified where the project is equal to the loaded one // - just a view is specifed without URL parameters (coming from another page) if( !fNd - || indexById( app.cache.selectedProject.data.resources, fNd.ref )<0 // condition is probably too weak - || uP && uP[CONFIG.keyProject] && uP[CONFIG.keyProject]!=app.cache.selectedProject.data.id ) + || !app.cache.selectedProject.data.has("resource", fNd.ref ) // condition is probably too weak + || uP && uP[CONFIG.keyProject] && uP[CONFIG.keyProject]!=app.cache.selectedProject.id ) self.tree.init(); // console.debug('show 1',uP,self.tree.selectedNode); // assuming that all initializing is completed (project and types are loaded), // get and show the specs: - if( app.cache.selectedProject.data.hierarchies && app.cache.selectedProject.data.hierarchies.length>0 ) { + if (app.cache.selectedProject.data.length("hierarchy")>0 ) { // ToDo: Get the hierarchies one by one, so that the first is shown as quickly as possible; // each might be coming from a different source (in future): - app.cache.selectedProject.readContent( 'hierarchy', app.cache.selectedProject.data.hierarchies, {reload:true} ) + app.cache.selectedProject.readContent( 'hierarchy', "all", {reload:true} ) .then( (rsp)=>{ // console.debug('load',rsp); @@ -1494,7 +1513,8 @@ moduleManager.construct({ if( !nd ) nd = self.tree.selectFirstNode(); if( nd ) { self.tree.openNode( nd ); - } else { + } + else { if( !self.resCre ) { // Warn, if tree is empty and there are no resource classes for user instantiation: message.show( i18n.MsgNoObjectTypeForManualCreation, {duration:CONFIG.messageDisplayTimeLong} ); @@ -1502,9 +1522,10 @@ moduleManager.construct({ }; }; }, - stdError + Lib.stdError ); - } else { + } + else { // the project has no spec: $( self.view ).html( '
'+i18n.MsgNoSpec+'
' ); app.busy.reset(); @@ -1561,7 +1582,8 @@ moduleManager.construct({ // changing the tree node triggers an event, by which 'self.refresh' will be called. self.tree.openNode( self.tree.selectedNode ); // opening a node triggers an event, by which 'self.refresh' will be called. - } else { + } + else { if( self.tree.selectedNode.children.length>0 ) { // console.debug('#2',rId,self.tree.selectedNode); // open the node if closed, close it if open: @@ -1579,7 +1601,7 @@ moduleManager.construct({ if( !cT || !rT ) return null; var newC = {}, - newId = genID('R-'); + newId = Lib.genID('R-'); app.cache.selectedProject.initResource( cT ) .done( function(rsp) { // returns an initialized resource of the requested type: @@ -1735,7 +1757,8 @@ moduleManager.construct({ renderNextResources, handleErr ); - } else { + } + else { handleErr( err ); }; } @@ -1789,7 +1812,7 @@ moduleManager.construct({ app.busy.reset(); } function handleErr(err):void { - stdError( err ); + Lib.stdError( err ); app.busy.reset(); } function actionBtns():string { @@ -1810,7 +1833,7 @@ moduleManager.construct({ // if( self.resCre && cacheData.selectedHierarchy.upd ) // ToDo: Respect the user's permission to change the hierarchy // ToDo: Don't allow creation of elements in automatically created branches like the glossary - if( self.resCre ) + if( self.resCre && (!selRes || selRes.isUserInstantiated()) ) rB += '' else @@ -1822,7 +1845,7 @@ moduleManager.construct({ // Add the clone button depending on the current user's permissions: // if( self.resCln && cacheData.selectedHierarchy.upd ) - if( self.resCre ) + if( self.resCre && selRes.isUserInstantiated() ) rB += '' else @@ -1981,14 +2004,14 @@ moduleManager.construct({ // it is necessary to provide "shows" statements also for statements. // ?? ToDo: delete the resource with all other references ... app.cache.selectedProject.deleteContent( "resource", {id:resId} ) - .catch( stdError ); + .catch( Lib.stdError ); // Delete all statements related to this resource: app.cache.selectedProject.readStatementsOf( {id:resId} ) .then( (staL)=>{ console.debug( 'delRes statements', staL); }, - stdError + Lib.stdError ); } */ function delNd(nd: jqTreeNode): void { @@ -2005,7 +2028,7 @@ moduleManager.construct({ ()=>{ // If a diagram has been deleted, build a new glossary with elements // which are shown by any of the remaining diagrams: - app.cache.selectedProject.createFolderWithGlossary( cacheData, {addGlossary:true} ) + app.cache.selectedProject.createFolderWithGlossary({addGlossary:true} ) .then( ()=>{ // undefined parameters will be replaced by default value: @@ -2015,10 +2038,10 @@ moduleManager.construct({ }); pData.doRefresh({forced:true}) }, - stdError + Lib.stdError ) }, - stdError + Lib.stdError ); } }; @@ -2147,7 +2170,7 @@ moduleManager.construct({ case 404: // related resource(s) not found, just ignore it break; default: - stdError(xhr); + Lib.stdError(xhr); } app.busy.reset(); } */ @@ -2158,20 +2181,20 @@ moduleManager.construct({ return; function handleErr(xhr: xhrMessage): void { - stdError(xhr); + Lib.stdError(xhr); app.busy.reset(); } function cacheMinRes(L:Resource[],r:Resource):void { // cache the minimal representation of a resource; // r may be a resource, a key pointing to a resource or a resource-id; // note that the sequence of items in L is always maintained: - cacheE( L, { id: itemIdOf(r), title: elementTitleOf( r, $.extend({},opts,{addIcon:true}), cacheData )}); + Lib.cacheE( L, { id: Lib.itemIdOf(r), title: elementTitleOf( r, $.extend({},opts,{addIcon:true}), cacheData )}); } function cacheMinSta(L:Statement[],s:Statement):void { // cache the minimal representation of a statement; // s is a statement: - cacheE(L, { id: s.id, title: staClassTitleOf(s, cacheData, opts), subject: itemIdOf(s.subject), object: itemIdOf(s.object)} ); - // cacheE(L, { id: s.id, title: elementTitleOf(s, opts, cacheData), subject: itemIdOf(s.subject), object: itemIdOf(s.object) }); + Lib.cacheE(L, { id: s.id, title: staClassTitleOf(s, cacheData, opts), subject: Lib.itemIdOf(s.subject), object: Lib.itemIdOf(s.object)} ); + // Lib.cacheE(L, { id: s.id, title: elementTitleOf(s, opts, cacheData), subject: Lib.itemIdOf(s.subject), object: Lib.itemIdOf(s.object) }); } function cacheNet(s:Statement):void { // skip hidden statements: @@ -2182,11 +2205,12 @@ moduleManager.construct({ // console.debug( 'cacheNet 1', s, simpleClone(net) ); // collect the related resources: - if( itemIdOf(s.subject) == nd.ref ) { + if( Lib.itemIdOf(s.subject) == nd.ref ) { // the selected node is a subject, so the related resource is an object, // list it, but only once: cacheMinRes( net.resources, s.object ); - } else { + } + else { // the related resource is a subject, // list it, but only once: cacheMinRes( net.resources, s.subject ); @@ -2281,7 +2305,7 @@ moduleManager.construct({ function notListed( L:Statement[],s,t ):boolean { for( var i=L.length-1;i>-1;i-- ) { - if( itemIdOf(L[i].subject)==s.id && itemIdOf(L[i].object)==t.id ) return false; + if( Lib.itemIdOf(L[i].subject)==s.id && Lib.itemIdOf(L[i].object)==t.id ) return false; }; return true; } @@ -2291,7 +2315,7 @@ moduleManager.construct({ // return false, if all resources 'and' visible statements have 'shows' statements for all diagrams (newer tranformators). // Corner case: No diagram at all returns true, also. let res: Resource, pV: string, isNotADiagram: boolean, noDiagramFound = true; - return iterateNodes(dta.hierarchies, + return Lib.iterateNodes(dta.hierarchies, (nd): boolean => { // get the referenced resource: res = itemById(dta.resources, nd.resource); @@ -2420,7 +2444,7 @@ moduleManager.construct({ sG.rGs.forEach( function(s) { relG.push({ id: s.id, - sId: itemIdOf(s.subject), + sId: Lib.itemIdOf(s.subject), sT: elementTitleWithIcon(s.subject,opts), computed: !s['class'] }); @@ -2451,7 +2475,7 @@ moduleManager.construct({ sG.rGt.forEach( function(s) { relG.push({ id: s.id, - tId: itemIdOf(s.object), + tId: Lib.itemIdOf(s.object), tT: elementTitleWithIcon(s.object,opts), computed: !s['class'] }); @@ -2478,7 +2502,8 @@ moduleManager.construct({ rT += '
'+lb+'
'; if( opts.fnDel ) rT += '
' - } else { + } + else { rT += '
'+i18n.MsgNoRelatedObjects+'
' }; rT += '
'; @@ -2521,7 +2546,7 @@ moduleManager.construct({ app.cache.selectedProject.deleteContent( 'statement', {id: sId} ) .then( pData.doRefresh({forced:true}), - stdError + Lib.stdError ); } else { diff --git a/src/modules/stdTypes.ts b/src/modules/stdTypes.ts index 2cf1467..a4e8ec3 100644 --- a/src/modules/stdTypes.ts +++ b/src/modules/stdTypes.ts @@ -1,9 +1,10 @@ /*! Standard type definitions with methods. Dependencies: - - (C)copyright 2010-2018 enso managers gmbh (http://www.enso-managers.de) + (C)copyright enso managers gmbh (http://www.enso-managers.de) License and terms of use: Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) Author: se@enso-managers.com, Berlin - We appreciate any correction, comment or contribution! + We appreciate any correction, comment or contribution via e-mail to maintenance@specif.de + .. or even better as Github issue (https://github.com/GfSE/SpecIF-Viewer/issues) */ class StandardTypes { @@ -107,9 +108,20 @@ class StandardTypes { propertyClasses: ["PC-Name","PC-Description","PC-Type"], changedAt: "2016-05-26T08:59:00+02:00" }]; + // The sequence is such that every list's elements have only references to list elements above: + listName = new Map([ + ['dataType', "dataTypes"], + ['propertyClass', "propertyClasses"], + ['resourceClass', "resourceClasses"], + ['statementClass', "statementClasses"], + ['file', "files"], + ['resource', "resources"], + ['statement', "statements"], + ['hierarchy', "hierarchies"] + ]) - get(ctg:string,id:string,chAt?:string):Item { - var item:Item = itemById( this[this.listNameOf(ctg)], id ); + get(ctg:string,id:string,chAt?:string):Item|undefined { + var item:Item = itemById( this[this.listName.get(ctg)], id ); if( item ) { // shield any subsequent change from the templates available here: item = simpleClone(item); @@ -117,28 +129,70 @@ class StandardTypes { return item; }; } + iterateLists(fn: Function): void { + for (var le of this.listName.keys()) + fn(le, this.listName.get(le)); + return this.listName.size; + } +/* getByTitle(ctg: string, ti: string, chAt?: string): Item | undefined { + var item: Item = itemByTitle(this[this.listName.get(ctg)], ti); + if (item) { + // shield any subsequent change from the templates available here: + item = simpleClone(item); + if (chAt) item.changedAt = chAt; + return item; + }; + } listNameOf(ctg:string):string { // Return the cache name for a given category: - switch(ctg) { - case 'dataType': return "dataTypes"; - case 'propertyClass': return "propertyClasses"; - case 'resourceClass': return "resourceClasses"; - case 'statementClass': return "statementClasses"; - case 'resource': return "resources"; - case 'statement': return "statements"; - case 'hierarchy': return "hierarchies"; - case 'file': return "files"; - }; + if (this.listName.has(ctg)) + return this.listName.get(ctg) as string; throw Error("Invalid category '"+ctg+"'"); - } + } */ }; - + +function addE(ctg: string, id: string, pr?): void { + // Add an element (e.g. class) to it's list, if not yet defined: + if (!pr) pr = app.cache.selectedProject.data; + + // get the name of the list, e.g. 'dataType' -> 'dataTypes': + let lN: string = standardTypes.listName.get(ctg); + // create it, if not yet available: + if (Array.isArray(pr[lN])) { + // add the type, but avoid duplicates: + if (indexById(pr[lN], id) < 0) + pr[lN].unshift(standardTypes.get(ctg, id)); + } + else { + pr[lN] = [standardTypes.get(ctg, id)]; + }; +} +function addPC(eC: object, id: string): void { + // Add the propertyClass-id to an element class (eC), if not yet defined: + let lN = 'propertyClasses'; + if (Array.isArray(eC[lN])) { + // Avoid duplicates: + if (eC[lN].indexOf(id) < 0) + eC[lN].unshift(id); + } + else { + eC[lN] = [id]; + }; +} +function addP(el: object, prp: object): void { + // Add the property to an element (el): + if (Array.isArray(el['properties'])) + el['properties'].unshift(prp); + else + el['properties'] = [prp]; +} + /* ToDo: REWORK FOR v0.10.8: // The standard types for comments: // A list with all data-, object- and relation-types needed for the comments according to specif schema. // For the time being, the addComment dialog (in specifications-*.html) is hard-coded for the current type definitions. function CommentTypes() { - let did = genID('DT-'), oid = genID('RC-'), rid = genID('SC-'); + let did = Lib.genID('DT-'), oid = Lib.genID('RC-'), rid = Lib.genID('SC-'); this.title = 'Types for comments'; this.specifVersion = '0.10.4'; this.dataTypes = [{ @@ -169,59 +223,6 @@ class StandardTypes { }] // console.debug('CommentTypes done') } - function GlossaryItems() { - var self=this; - let dTid = genID('DT-'), rCid = genID('RC-'), sCid = genID('SC-'), - pC1id = genID('PC-'), pC3id = genID('PC-'), rId - time = new Date().toISOString(); - self.title = 'Types and a folder instance for a glossary'; - self.specifVersion = '0.10.8'; - self.dataTypes = [{ - id: dTid, - title: CONFIG.dataTypeComment, - type: "xs:string", - maxLength: CONFIG.textThreshold, - changedAt: time - }]; - self.propertyClasses = [{ - id: pC1id, - title: CONFIG.attrTypeTitle, - dataType: dTid, // ID of the dataType defined before - changedAt: time - // }, { - // id: genID('PC-'), - // title: attrTypeText, - // dataType: dTid, - // changedAt: time - }, { - id: pC3id, - title: CONFIG.attrTypeType, - dataType: dTid, // ID of the dataType defined before - changedAt: time - }]; - self.resourceClasses = [{ - id: rCid, - title: CONFIG.spcTypeGlossary, - description: "Comment referring to a model element ('resource' in general).", - instantiation: ["auto"], - propertyClasses: [pC1id,pC3id], - changedAt: time - }]; - self.resources = [{ - id: genID('R-'), - title: i18n.lookup(CONFIG.spcTypeGlossary), - class: rCid, - properties: [{ - class: pC1id, - value: i18n.lookup(CONFIG.spcTypeGlossary) - }, { - class: pC3id, - value: CONFIG.spcTypeGlossary - }], - changedAt: time - }]; - return self - } // a constructor for standard types: function StdTypes( prj, types ) { @@ -236,9 +237,9 @@ class StandardTypes { // self.available = function() { // // Return true if all types are available. // // Must compare by unique name, because the id may vary. - // return containsByTitle( prj.dataTypes, types.dataTypes ) - // && containsByTitle( prj.resourceClasses, types.resourceClasses ) - // && containsByTitle( prj.statementClasses, types.statementClasses ) + // return Lib.containsByTitle( prj.dataTypes, types.dataTypes ) + // && Lib.containsByTitle( prj.resourceClasses, types.resourceClasses ) + // && Lib.containsByTitle( prj.statementClasses, types.statementClasses ) // }; self.add = function() { diff --git a/src/modules/toHtml.ts b/src/modules/toHtml.ts index 4729c79..894dd0c 100644 --- a/src/modules/toHtml.ts +++ b/src/modules/toHtml.ts @@ -60,7 +60,7 @@ function toHtmlDoc(pr: SpecIF, pars:any) { // console.warn(errT); // reject({ status: 999, statusText: errT }); */ - blob2dataURL(f, + Lib.blob2dataURL(f, (r: string) => { // perhaps there is a more elegant way to apply the type to the dataURL, // but it works: @@ -123,7 +123,7 @@ function toHtmlDoc(pr: SpecIF, pars:any) { + 'document.head.appendChild(link);' + '}' + 'let pend = 4;' - + 'getScript("https://code.jquery.com/jquery-3.5.1.min.js");' + + 'getScript("https://code.jquery.com/jquery-3.6.0.min.js");' + 'getScript(cdn+"config/definitions.js?" + Date.now().toString());' + 'getScript(cdn+"config/moduleManager.js?" + Date.now().toString());' + 'getScript(cdn+"embedded.js?" + Date.now().toString());' diff --git a/src/modules/xSpecif.ts b/src/modules/xSpecif.ts index 86767e1..e834200 100644 --- a/src/modules/xSpecif.ts +++ b/src/modules/xSpecif.ts @@ -1,5 +1,5 @@ -/*! Transformation Library for SpecIF data. - Dependencies: jQuery +/*! Transformation Library for SpecIF data for import to and export from the internal data structure. + Dependencies: jQuery 3.5+ (C)copyright enso managers gmbh (http://www.enso-managers.de) Author: se@enso-managers.de, Berlin License and terms of use: Apache 2.0 (http://www.apache.org/licenses/LICENSE-2.0) @@ -72,104 +72,58 @@ class CSpecifItemNames { } } class CSpecIF implements SpecIF { - // Internal representation of a SpecIF data-set/project. - // ToDo: Needs rework to separate the I/O transformation from the caching functions. - names: CSpecifItemNames; // SpecIF attribute names for all supported import versions - - id: string; - $schema: string; - title: string; - description: string; - generator: string; - generatorVersion: string; - rights: any; - myRole: RoleEnum; - cre: boolean; - upd: boolean; - del: boolean; - exp: boolean; - locked: boolean; // the server has locked the project ( readOnly ) - createdAt: string; + // Transform a SpecIF data-set of several versions to the internal representation of the SpecIF Viewer/Editor + // and also transform it back to a SpecIF data-set of the most recent version. + + id = ''; + $schema = ''; + title = ''; + description = ''; + generator = ''; + generatorVersion = ''; + rights: any = {}; + createdAt = ''; createdBy: CreatedBy; - dataTypes: DataType[]; - propertyClasses: PropertyClass[]; - resourceClasses: ResourceClass[]; - statementClasses: StatementClass[]; - resources: Resource[]; // list of resources as referenced by the hierarchies - statements: Statement[]; - hierarchies: SpecifNode[]; // listed specifications (aka hierarchies, outlines) of the project. - files: IFileWithContent[]; + dataTypes: DataType[] = []; + propertyClasses: PropertyClass[] = []; + resourceClasses: ResourceClass[] = []; + statementClasses: StatementClass[] = []; + files: IFileWithContent[] = []; + resources: Resource[] = []; // list of resources as referenced by the hierarchies + statements: Statement[] = []; + hierarchies: SpecifNode[] = []; // listed specifications (aka hierarchies, outlines) of the project. constructor(spD?:any) { - this.names = new CSpecifItemNames(); - if (spD && spD.id) { - this.toInt(spD); - } - else { - this.id = ''; - this.$schema = ''; - this.title = ''; - this.description = ''; - this.generator = ''; - this.generatorVersion = ''; - this.rights = {}; - this.myRole = RoleEnum.Reader; - this.cre = false; - this.upd = false; - this.del = false; - this.exp = false; - this.locked = false; // the server has locked the project ( readOnly ) - this.createdAt = ''; - this.createdBy = undefined; - - this.dataTypes = []; - this.propertyClasses = []; - this.resourceClasses = []; - this.statementClasses = []; - this.resources = []; // list of resources as referenced by the hierarchies - this.statements = []; - this.hierarchies = []; // listed specifications (aka hierarchies, outlines) of the project. - this.files = []; - }; + this.toInt(spD); } - isValid(): boolean { - return typeof(this.id)=='string' && this.id.length > 0; + isValid(spD?: any): boolean { + if (!spD) spD = this; + return typeof(spD.id)=='string' && spD.id.length > 0; } - setMeta(dta: SpecIF): void { - this.id = dta.id; - this.title = dta.title; - this.description = dta.description; - this.generator = dta.generator; - this.generatorVersion = dta.generatorVersion; - this.myRole = i18n.LblRoleProjectAdmin; - // this.cre = this.upd = this.del = this.exp = app.title!=i18n.LblReader; - this.cre = this.upd = this.del = app.title != i18n.LblReader; - this.exp = true; - this.locked = app.title == i18n.LblReader; - this.createdAt = dta.createdAt; - this.createdBy = dta.createdBy; - } toInt(spD: any):void { + if (!this.isValid(spD)) return; + // transform SpecIF to internal data; // no data of app.cache is modified. // It is assumed that spD has passed the schema and consistency check. // console.debug('set',simpleClone(spD)); - this.names = new CSpecifItemNames(spD.specifVersion); + let self = this, + names = new CSpecifItemNames(spD.specifVersion); // Differences when using forAll() instead of [..].map(): // - tolerates missing input list (not all are mandatory for SpecIF) // - suppresses undefined list items in the result, so in effect forAll() is a combination of .map() and .filter(). try { - this.dataTypes = forAll( spD.dataTypes, (e: any) => { return this.dT2int(e) }); - this.propertyClasses = forAll( spD.propertyClasses, (e: any) => { return this.pC2int(e) }); // starting v0.10.6 - this.resourceClasses = forAll( spD[this.names.rClasses], (e: any) => { return this.rC2int(e) }); - this.statementClasses = forAll( spD[this.names.sClasses], (e: any) => { return this.sC2int(e) }); - if (this.names.hClasses) - this.resourceClasses = this.resourceClasses.concat( forAll( spD[this.names.hClasses], (e: any) => { return this.hC2int(e) })); - this.resources = forAll( spD.resources, (e: any) => { return this.r2int(e) }); - this.statements = forAll( spD.statements, (e: any) => { return this.s2int(e) }); - this.hierarchies = forAll( spD.hierarchies, (e: any) => { return this.h2int(e) }); - this.files = forAll( spD.files, (e: any) => { return this.f2int(e) }) + this.dataTypes = Lib.forAll( spD.dataTypes, dT2int ); + this.propertyClasses = Lib.forAll(spD.propertyClasses, pC2int ); + this.resourceClasses = Lib.forAll( spD[names.rClasses], rC2int ); + this.statementClasses = Lib.forAll( spD[names.sClasses], sC2int ); + if (names.hClasses) + this.resourceClasses = this.resourceClasses.concat( Lib.forAll( spD[names.hClasses], hC2int )); + this.resources = Lib.forAll( spD.resources, r2int ); + this.statements = Lib.forAll( spD.statements, s2int ); + this.hierarchies = Lib.forAll( spD.hierarchies, h2int ); + this.files = Lib.forAll( spD.files, f2int ) } catch (e) { let txt = "Error when importing the project '" + spD.title + "'"; @@ -179,945 +133,816 @@ class CSpecIF implements SpecIF { }; // header information provided only in case of project creation, but not in case of project update: - if (spD.id) this.id = spD.id; - if (spD.title) this.title = spD.title; - if (spD.description) this.description = spD.description; if (spD.generator) this.generator = spD.generator; if (spD.generatorVersion) this.generatorVersion = spD.generatorVersion; if (spD.createdBy) this.createdBy = spD.createdBy; if (spD.createdAt) this.createdAt = spD.createdAt; + if (spD.description) this.description = spD.description; + if (spD.title) this.title = spD.title; + if (spD.id) this.id = spD.id; // console.debug('specif.toInt',simpleClone(this)); - } - private i2int(iE) { - // common for all items: - var oE: any = { - id: iE.id, - changedAt: iE.changedAt - }; - if (iE.description) oE.description = cleanValue(iE.description); - // revision is a number up until v0.10.6 and a string thereafter: - switch (typeof (iE.revision)) { - case 'number': - oE.revision = iE.revision.toString(); // for { - // 'v.title' until v0.10.6, 'v.value' thereafter; - // 'v.value' can be a string or a multilanguage object. - return { - id: v.id, - value: typeof (v.value) == 'string' || typeof (v.value) == 'object' ? v.value : v.title // works also for v.value=='' - } - }) - }; - // console.debug('dataType 2int',iE); - return oE - } - // a property class: - private pC2int(iE: PropertyClass): PropertyClass { - var oE: any = this.i2int(iE); - oE.title = cleanValue(iE.title); // an input file may have titles which are not from the SpecIF vocabulary. - if (iE.description) oE.description = cleanValue(iE.description); - if (iE.value) oE.value = cleanValue(iE.value); - oE.dataType = iE.dataType; - let dT: DataType = itemById(this.dataTypes, iE.dataType); - // console.debug('pC2int',iE,dT); - switch (dT.type) { - case 'xs:enumeration': - // include the property only, if it is different from the dataType's: - if (iE.multiple && !dT.multiple) oE.multiple = true - else if (iE.multiple == false && dT.multiple) oE.multiple = false - }; - // console.debug('propClass 2int',iE,oE); - return oE - } - // common for all instance classes: - private aC2int(iE) { - var oE: any = this.i2int(iE); - oE.title = cleanValue(iE.title); - if (iE['extends']) oE._extends = iE['extends']; // 'extends' is a reserved word starting with ES5 - if (iE.icon) oE.icon = iE.icon; - if (iE.creation) oE.instantiation = iE.creation; // deprecated, for compatibility - if (iE.instantiation) oE.instantiation = iE.instantiation; - if (oE.instantiation) { - let idx = oE.instantiation.indexOf('manual'); // deprecated - if (idx > -1) oE.instantiation.splice(idx, 1, 'user') - }; - // Up until v0.10.5, the pClasses themself are listed, starting v0.10.6 their ids are listed as a string. - if (Array.isArray(iE[this.names.pClasses]) && iE[this.names.pClasses].length > 0) - if (typeof (iE[this.names.pClasses][0]) == 'string') - // copy the list of pClasses' ids: - oE.propertyClasses = iE.propertyClasses - else { - // internally, the pClasses are stored like in v0.10.6. - oE.propertyClasses = []; - iE[this.names.pClasses].forEach((e: PropertyClass) => { - // Store the pClasses at the top level: - this.propertyClasses.push(this.pC2int(e)); - // Add to a list with pClass' ids, here: - oE.propertyClasses.push(e.id) - }) - } - else - oE.propertyClasses = []; - // console.debug('anyClass 2int',iE,oE); - return oE - } - // a resource class: - private rC2int(iE: ResourceClass): ResourceClass { - var oE: any = this.aC2int(iE); - - // If "iE.isHeading" is defined, use it: - if (typeof (iE.isHeading) == 'boolean') { - oE.isHeading = iE.isHeading; - return oE - }; - // else: take care of older data without "isHeading": - if (iE.title == 'SpecIF:Heading') { - oE.isHeading = true; - return oE - }; - // else: look for a property class being configured in CONFIG.headingProperties - let pC; - for (var a = oE.propertyClasses.length - 1; a > -1; a--) { - pC = oE.propertyClasses[a]; - // look up propertyClass starting v0.101.6: - if (typeof (pC) == 'string') pC = itemById(this.propertyClasses, pC); - if (CONFIG.headingProperties.indexOf(pC.title) > -1) { - oE.isHeading = true; - break - } - }; - // console.debug('resourceClass 2int',iE,oE); - return oE - } - // a statementClass: - private sC2int(iE): StatementClass { - var oE: StatementClass = this.aC2int(iE); - if (iE.isUndirected) oE.isUndirected = iE.isUndirected; - if (iE[this.names.subClasses]) oE.subjectClasses = iE[this.names.subClasses]; - if (iE[this.names.objClasses]) oE.objectClasses = iE[this.names.objClasses]; - // console.debug('statementClass 2int',iE,oE); - return oE - } - // a hierarchyClass: - private hC2int(iE) { - // hierarchyClasses (used up until v0.10.6) are stored as resourceClasses, - // later on, the hierarchy-roots will be stored as resources referenced by a node: - var oE = this.aC2int(iE); - oE.isHeading = true; - // console.debug('hierarchyClass 2int',iE,oE); - return oE - } - // a property: - private p2int(iE): Property { - var dT: DataType = dataTypeOf(this, iE[this.names.pClass]), - oE: Property = { - // no id - class: iE[this.names.pClass] - }; - if (iE.title) oE.title = cleanValue(iE.title); - if (iE.description) oE.description = cleanValue(iE.description); - - switch (dT.type) { - case 'xs:string': - case 'xhtml': - oE.value = cleanValue(iE.value); - oE.value = Array.isArray(oE.value) ? - // multiple languages: - forAll(oE.value, - (val) => { - val.text = uriBack2slash(val.text); - return val; - }) - // single language: - : uriBack2slash(oE.value); - break; - default: - // According to the schema, all property values are represented by a string - // and internally they are stored as string as well to avoid inaccuracies - // by multiple transformations: - oE.value = cleanValue(iE.value); - }; - // properties do not have their own revision and change info -// console.debug('propValue 2int',iE,pT,oE); - return oE - } - // common for all instances: - private a2int(iE): Instance { - var oE = this.i2int(iE); - // resources must have a title, but statements may come without: - if (iE.title) - oE.title = cleanValue(iE.title); - if (iE.properties && iE.properties.length > 0) - oE.properties = forAll( iE.properties, (e: any): Property => { return this.p2int(e) }); -// console.debug('a2int',iE,simpleClone(oE)); - return oE - } - // a resource: - private r2int(iE): Resource { - var oE: Resource = this.a2int(iE); - oE['class'] = iE[this.names.rClass]; -// console.debug('resource 2int',iE,simpleClone(oE)); - return oE - } - // a statement: - private s2int(iE): Statement { - var oE = this.a2int(iE); - oE['class'] = iE[this.names.sClass]; - // SpecIF allows subjects and objects with id alone or with a key (id+revision): - // keep original and normalize to id+revision for display: - // if( iE.isUndirected ) oE.isUndirected = iE.isUndirected; - oE.subject = iE.subject; - oE.object = iE.object; - - // special feature to import statements to complete, - // used for example by the XLS or ReqIF import: - if (iE.subjectToFind) oE.subjectToFind = iE.subjectToFind; - if (iE.objectToFind) oE.objectToFind = iE.objectToFind; -// console.debug('statement 2int',iE,oE); - return oE - } - // a hierarchy: - private h2int(eH: SpecifNode) { - // the properties are stored with a resource, while the hierarchy is stored as a node with reference to that resource: - if (this.names.hClasses) { - // up until v0.10.6, transform hierarchy root to a regular resource: - var iR = this.a2int(eH), - // ... and add a link to the hierarchy: - iH = { - id: 'N-' + iR.id, - resource: iR.id, - changedAt: eH.changedAt - }; - iR['class'] = eH[this.names.hClass]; - this.resources.push(iR); - - if (eH.revision) iH.revision = eH.revision.toString() - } - else { - // starting v0.10.8: - var iH = this.i2int(eH); - iH.resource = eH.resource - }; - - // SpecIF allows resource references with id alone or with a key (id+revision): - iH.nodes = forAll(eH.nodes, n2int); -// console.debug('hierarchy 2int',eH,iH); - return iH - - // a hierarchy node: - function n2int(eN) { - switch (typeof (eN.revision)) { - case 'number': - eN.revision = eN.revision.toString() - }; - forAll(eN.nodes, n2int); - return eN - } - } - // a file: - private f2int(iF) { - var oF = this.i2int(iF); - oF.title = iF.title ? iF.title.replace(/\\/g, '/') : iF.id; - // store the blob and it's type: - if (iF.blob) { - oF.type = iF.blob.type || iF.type || attachment2mediaType(iF.title); - oF.blob = iF.blob; - } - else if (iF.dataURL) { - oF.type = iF.type || attachment2mediaType(iF.title); - oF.dataURL = iF.dataURL; - } - else - oF.type = iF.type; - return oF - } - toExt( opts?: any ):SpecIF { - // transform self.data to SpecIF; - // if opts.targetLanguage has no value, all available languages are kept. - -// console.debug('toExt', this, opts ); - // transform internal data to SpecIF: - var spD: SpecIF = { - id: this.id, - title: languageValueOf(this.title, opts), - $schema: 'https://specif.de/v' + CONFIG.specifVersion + '/schema.json', - generator: app.title, - generatorVersion: CONFIG.appVersion, - createdAt: new Date().toISOString() - }; - - if (this.description) spD.description = languageValueOf(this.description, opts); - - if (this.rights && this.rights.title && this.rights.url) { - spD.rights = this.rights; - if (!this.type) spD.type = "dcterms:rights"; - } - else - spD.rights = { - title: "Creative Commons 4.0 CC BY-SA", - type: "dcterms:rights", - url: "https://creativecommons.org/licenses/by-sa/4.0/" - }; - - if (app.me && app.me.email) { - spD.createdBy = { - familyName: app.me.lastName, - givenName: app.me.firstName, - email: { type: "text/html", value: app.me.email } - }; - if (app.me.organization) - spD.createdBy.org = { organizationName: app.me.organization }; - } - else { - if (this.createdBy && this.createdBy.email && this.createdBy.email.value) { - spD.createdBy = { - familyName: this.createdBy.familyName, - givenName: this.createdBy.givenName, - email: { type: "text/html", value: this.createdBy.email.value } - }; - if (this.createdBy.org && this.createdBy.org.organizationName) - spD.createdBy.org = this.createdBy.org; - }; - // else: don't add createdBy without data - }; - - // Now start to assemble the SpecIF output: - if (this.dataTypes && this.dataTypes.length > 0) - spD.dataTypes = forAll(this.dataTypes, dT2ext); - if (this.propertyClasses && this.propertyClasses.length > 0) - spD.propertyClasses = forAll(this.propertyClasses, pC2ext); - spD.resourceClasses = forAll(this.resourceClasses, rC2ext); - spD.statementClasses = forAll(this.statementClasses, sC2ext); - spD.resources = forAll((opts.allResources ? this.resources : collectResourcesByHierarchy(this)), r2ext); - spD.statements = forAll(this.statements, s2ext); - spD.hierarchies = forAll(this.hierarchies, n2ext); - if (this.files && this.files.length > 0) - spD.files = forAll(this.files, f2ext); - - // Check whether all statements reference resources or statements, which are listed. - // Obviously this check can only be done at the end .. - let lenBefore: number; - do { - lenBefore = spD.statements.length; - spD.statements = spD.statements.filter( - (s) => { - return (indexById(spD.resources, itemIdOf(s.subject)) > -1 - || indexById(spD.statements, itemIdOf(s.subject)) > -1) - && (indexById(spD.resources, itemIdOf(s.object)) > -1 - || indexById(spD.statements, itemIdOf(s.object)) > -1) - } - ); - console.info("Suppressed " + (lenBefore - spD.statements.length) + " statements, because subject or object are not listed."); - } - while (spD.statements.length < lenBefore); - - // Add a resource as hierarchyRoot, if needed. - // It is assumed, - // - that in general SpecIF data do not have a hierarchy root with meta-data. - // - that ReqIF specifications (=hierarchyRoots) are transformed to regular resources on input. - function outlineTypeIsNotHidden(hPL?): boolean { - if (!hPL || hPL.length < 1) return true; - for (var i = hPL.length - 1; i > -1; i--) { - if (hPL[i].title == CONFIG.propClassType - && (typeof (hPL[i].value) != 'string' || hPL[i].value == CONFIG.resClassOutline)) - return false; - }; - return true; - } - if (opts.createHierarchyRootIfNotPresent && aHierarchyHasNoRoot(spD)) { - - console.info("Adding a hierarchyRoot"); - addE("resourceClass", "RC-HierarchyRoot", spD); - - // ToDo: Let the program derive the referenced class ids from the above - addE("propertyClass", "PC-Type", spD); - addE("propertyClass", "PC-Description", spD); - addE("propertyClass", "PC-Name", spD); - addE("dataType", "DT-ShortString", spD); - addE("dataType", "DT-Text", spD); - - var res = { - id: 'R-' + simpleHash(spD.id), - title: spD.title, - class: "RC-HierarchyRoot", - properties: [{ - class: "PC-Name", - value: spD.title - }], - changedAt: spD.createdAt - }; - // Add the resource type, if it is not hidden: - let rC = itemById(spD.resourceClasses, "RC-HierarchyRoot"); - if (outlineTypeIsNotHidden(opts.hiddenProperties)) { - addP(res, { - class: "PC-Type", - value: rC.title // should be CONFIG.resClassOutline - }); - }; - // Add a description property only if it has a value: - if (spD.description) - addP(res, { - class: "PC-Description", - value: spD.description - }); - spD.resources.push(r2ext(res)); - // create a new root instance: - spD.hierarchies = [{ - id: "H-" + res.id, - resource: res.id, - // .. and add the previous hierarchies as children: - nodes: spD.hierarchies, - changedAt: spD.changedAt - }]; - }; - - // ToDo: schema and consistency check (if we want to detect any programming errors) - // console.debug('specif.toExt exit',spD); - return spD - function aHierarchyHasNoRoot(dta: SpecIF): boolean { - for (var i = dta.hierarchies.length - 1; i > -1; i--) { - let hR = itemById(dta.resources, dta.hierarchies[i].resource); - if (!hR) { - throw Error("Hierarchy '", dta.hierarchies[i].id, "' is corrupt"); - }; - let prpV = valByTitle(hR, CONFIG.propClassType, dta), - hC = itemById(dta.resourceClasses, hR['class']); - // The type of the hierarchy root can be specified by a property titled CONFIG.propClassType - // or by the title of the resourceClass: - if ((!prpV || CONFIG.hierarchyRoots.indexOf(prpV) < 0) - && (!hC || CONFIG.hierarchyRoots.indexOf(hC.title) < 0)) - return true; - }; - return false; - } - // common for all items: - function i2ext(iE) { - var oE = { + function i2int(iE) { + // common for all items: + var oE: any = { id: iE.id, changedAt: iE.changedAt }; - // most items must have a title, but statements may come without: - if (iE.title) oE.title = titleOf(iE, opts); - if (iE.description) oE.description = languageValueOf(iE.description, opts); - if (iE.revision) oE.revision = iE.revision; + if (iE.description) oE.description = Lib.cleanValue(iE.description); + // revision is a number up until v0.10.6 and a string thereafter: + switch (typeof (iE.revision)) { + case 'number': + oE.revision = iE.revision.toString(); // for { return { id: val.id, value: languageValueOf(val.value, opts) } }) - else - oE.values = iE.values + if (iE.values) + oE.values = Lib.forAll(iE.values, (v): EnumeratedValue => { + // 'v.title' until v0.10.6, 'v.value' thereafter; + // 'v.value' can be a string or a multilanguage object. + return { + id: v.id, + value: typeof (v.value) == 'string' || typeof (v.value) == 'object' ? v.value : v.title // works also for v.value=='' + } + }) }; +// console.debug('dataType 2int',iE); return oE } // a property class: - function pC2ext(iE: PropertyClass) { - var oE: PropertyClass = i2ext(iE); - if (iE.value) oE.value = iE.value; // a default value + function pC2int(iE: PropertyClass): PropertyClass { + var oE: any = i2int(iE); + oE.title = Lib.cleanValue(iE.title); // an input file may have titles which are not from the SpecIF vocabulary. + if (iE.description) oE.description = Lib.cleanValue(iE.description); + if (iE.value) oE.value = Lib.cleanValue(iE.value); oE.dataType = iE.dataType; - let dT = itemById(spD.dataTypes, iE.dataType); + let dT: DataType = itemById(self.dataTypes, iE.dataType); +// console.debug('pC2int',iE,dT); switch (dT.type) { case 'xs:enumeration': - // With SpecIF, he 'multiple' property should be defined at dataType level - // and can be overridden at propertyType level. - // dT.multiple aTs.multiple aTs.multiple effect - // --------------------------------------------------------- - // undefined undefined undefined false - // false undefined undefined false - // true undefined undefined true - // undefined false undefined false - // false false undefined false - // true false false false - // undefined true true true - // false true true true - // true true undefined true - // Include the property only, if is different from the dataType's: + // include the property only, if it is different from the dataType's: if (iE.multiple && !dT.multiple) oE.multiple = true else if (iE.multiple == false && dT.multiple) oE.multiple = false }; +// console.debug('propClass 2int',iE,oE); return oE } // common for all instance classes: - function aC2ext(iE) { - var oE = i2ext(iE); + function aC2int(iE) { + var oE: any = i2int(iE); + oE.title = Lib.cleanValue(iE.title); + if (iE['extends']) oE._extends = iE['extends']; // 'extends' is a reserved word starting with ES5 if (iE.icon) oE.icon = iE.icon; + if (iE.creation) oE.instantiation = iE.creation; // deprecated, for compatibility if (iE.instantiation) oE.instantiation = iE.instantiation; - if (iE._extends) oE['extends'] = iE._extends; - if (iE.propertyClasses.length > 0) oE.propertyClasses = iE.propertyClasses; + if (oE.instantiation) { + let idx = oE.instantiation.indexOf('manual'); // deprecated + if (idx > -1) oE.instantiation.splice(idx, 1, 'user') + }; + // Up until v0.10.5, the pClasses themself are listed, starting v0.10.6 their ids are listed as a string. + if (Array.isArray(iE[names.pClasses]) && iE[names.pClasses].length > 0) + if (typeof (iE[names.pClasses][0]) == 'string') + // copy the list of pClasses' ids: + oE.propertyClasses = iE.propertyClasses + else { + // internally, the pClasses are stored like in v0.10.6. + oE.propertyClasses = []; + iE[names.pClasses].forEach((e: PropertyClass) => { + // Store the pClasses at the top level: + self.propertyClasses.push(pC2int(e)); + // Add to a list with pClass' ids, here: + oE.propertyClasses.push(e.id) + }) + } + else + oE.propertyClasses = []; +// console.debug('anyClass 2int',iE,oE); return oE } // a resource class: - function rC2ext(iE: ResourceClass) { - var oE: ResourceClass = aC2ext(iE); - // Include "isHeading" in SpecIF only if true: - if (iE.isHeading) oE.isHeading = true; + function rC2int(iE: ResourceClass): ResourceClass { + var oE: any = aC2int(iE); + + // If "iE.isHeading" is defined, use it: + if (typeof (iE.isHeading) == 'boolean') { + oE.isHeading = iE.isHeading; + return oE + }; + // else: take care of older data without "isHeading": + if (iE.title == 'SpecIF:Heading') { + oE.isHeading = true; + return oE + }; + // else: look for a property class being configured in CONFIG.headingProperties + let pC; + for (var a = oE.propertyClasses.length - 1; a > -1; a--) { + pC = oE.propertyClasses[a]; + // look up propertyClass starting v0.101.6: + if (typeof (pC) == 'string') pC = itemById(self.propertyClasses, pC); + if (CONFIG.headingProperties.indexOf(pC.title) > -1) { + oE.isHeading = true; + break + } + }; +// console.debug('resourceClass 2int',iE,oE); return oE } - // a statement class: - function sC2ext(iE: StatementClass) { - var oE: StatementClass = aC2ext(iE); + // a statementClass: + function sC2int(iE): StatementClass { + var oE: StatementClass = aC2int(iE); if (iE.isUndirected) oE.isUndirected = iE.isUndirected; - if (iE.subjectClasses && iE.subjectClasses.length > 0) oE.subjectClasses = iE.subjectClasses; - if (iE.objectClasses && iE.objectClasses.length > 0) oE.objectClasses = iE.objectClasses; + if (iE[names.subClasses]) oE.subjectClasses = iE[names.subClasses]; + if (iE[names.objClasses]) oE.objectClasses = iE[names.objClasses]; +// console.debug('statementClass 2int',iE,oE); + return oE + } + // a hierarchyClass: + function hC2int(iE) { + // hierarchyClasses (used up until v0.10.6) are stored as resourceClasses, + // later on, the hierarchy-roots will be stored as resources referenced by a node: + var oE = aC2int(iE); + oE.isHeading = true; +// console.debug('hierarchyClass 2int',iE,oE); return oE } // a property: - function p2ext(iE: Property) { - // skip empty properties: - if (!iE.value) return; - - // skip hidden properties: - let pC: PropertyClass = itemById(spD.propertyClasses, iE['class']); - if (Array.isArray(opts.hiddenProperties)) { - // CONFIG.hiddenProperties.forEach( (hP)=>{ - opts.hiddenProperties.forEach((hP) => { - if (hP.title == (iE.title || pC.title) && (hP.value == undefined || hP.value == iE.value)) return; - }); - }; - - var oE: Property = { - // no id - class: iE['class'] - }; - if (iE.title) { - // skip the property title, if it is equal to the propertyClass' title: - let ti = titleOf(iE, opts); - if (ti != pC.title) oE.title = ti; - }; - if (iE.description) oE.description = languageValueOf(iE.description, opts); - - // According to the schema, all property values are represented by a string - // and we want to store them as string to avoid inaccuracies by multiple transformations: - if (opts.targetLanguage) { - // reduce to the selected language; is used for generation of human readable documents - // or for formats not supporting multiple languages: - let dT: DataType = dataTypeOf(spD, iE['class']); - switch (dT.type) { - case 'xs:string': - case 'xhtml': - if (opts.targetLanguage) { - if (CONFIG.excludedFromFormatting.indexOf(iE.title || pC.title) > -1) - // if it is e.g. a title, remove all formatting: - oE.value = stripHTML(languageValueOf(iE.value, opts) - // remove any leading whiteSpace: - .replace(/^\s+/, "")); - else - // otherwise transform to HTML, if possible; - // especially for publication, for example using WORD format: - oE.value = languageValueOf(iE.value, opts) - // remove any leading whiteSpace: - .replace(/^\s+/, "") - .makeHTML(opts) - .replace(/
\n/g, "
"); - - // console.debug('p2ext',iE,languageValueOf( iE.value, opts ),oE.value); - break; - }; - // else: no break - return the original value - default: - // in case of 'xs:enumeration', - // an id of the dataType's value is given, so it can be taken directly: - oE.value = iE.value; + function p2int(iE): Property { + var dT: DataType = dataTypeOf(self, iE[names.pClass]), + oE: Property = { + // no id + class: iE[names.pClass] }; - } - else { - // for SpecIF export, keep full data structure: - oE.value = iE.value; + if (iE.title) oE.title = Lib.cleanValue(iE.title); + if (iE.description) oE.description = Lib.cleanValue(iE.description); + + switch (dT.type) { + case 'xs:string': + case 'xhtml': + oE.value = Lib.cleanValue(iE.value); + oE.value = Array.isArray(oE.value) ? + // multiple languages: + Lib.forAll(oE.value, + (val) => { + val.text = Lib.uriBack2slash(val.text); + return val; + }) + // single language: + : Lib.uriBack2slash(oE.value); + break; + default: + // According to the schema, all property values are represented by a string + // and internally they are stored as string as well to avoid inaccuracies + // by multiple transformations: + oE.value = Lib.cleanValue(iE.value); }; - // properties do not have their own revision and change info; the parent's apply. - return oE; + // properties do not have their own revision and change info +// console.debug('propValue 2int',iE,pT,oE); + return oE } // common for all instances: - function a2ext(iE) { - var oE = i2ext(iE); - // console.debug('a2ext',iE,opts); - // resources and hierarchies usually have individual titles, and so we will not lookup: - oE['class'] = iE['class']; - if (iE.alternativeIds) oE.alternativeIds = iE.alternativeIds; - if (iE.properties && iE.properties.length > 0) oE.properties = forAll(iE.properties, p2ext); - return oE; + function a2int(iE): Instance { + var oE = i2int(iE); + // resources must have a title, but statements may come without: + if (iE.title) + oE.title = Lib.cleanValue(iE.title); + if (iE.properties && iE.properties.length > 0) + oE.properties = Lib.forAll( iE.properties, (e: any): Property => { return p2int(e) }); +// console.debug('a2int',iE,simpleClone(oE)); + return oE } // a resource: - function r2ext(iE: Resource) { - var oE: Resource = a2ext(iE); - // console.debug('resource 2int',iE,oE); - return oE; + function r2int(iE): Resource { + var oE: Resource = a2int(iE); + oE['class'] = iE[names.rClass]; +// console.debug('resource 2int',iE,simpleClone(oE)); + return oE } // a statement: - function s2ext(iE: Statement) { - // console.debug('statement2ext',iE.title); - // Skip statements with an open end; - // At the end it will be checked, wether all referenced resources resp. statements are listed: - if (!iE.subject || itemIdOf(iE.subject) == CONFIG.placeholder - || !iE.object || itemIdOf(iE.object) == CONFIG.placeholder - ) return; - - // The statements usually do use a vocabulary item (and not have an individual title), - // so we lookup, if so desired, e.g. when exporting to ePub: - var oE: Statement = a2ext(iE); - - // Skip the title, if it is equal to the statementClass' title; - // ToDo: remove limitation of single language. - if (oE.title && typeof (oE.title) == "string") { - let sC = itemById(spD.statementClasses, iE['class']); - if (typeof (sC.title) == "string" && oE.title == sC.title) - delete oE.title; - }; - + function s2int(iE): Statement { + var oE = a2int(iE); + oE['class'] = iE[names.sClass]; + // SpecIF allows subjects and objects with id alone or with a key (id+revision): + // keep original and normalize to id+revision for display: // if( iE.isUndirected ) oE.isUndirected = iE.isUndirected; - // for the time being, multiple revisions are not supported: - if (opts.revisionDate) { - // supply only the id, but not a key: - oE.subject = itemIdOf(iE.subject); - oE.object = itemIdOf(iE.object); - } - else { - // supply key or id: - oE.subject = iE.subject; - oE.object = iE.object; - }; - return oE; + oE.subject = iE.subject; + oE.object = iE.object; + + // special feature to import statements to complete, + // used for example by the XLS or ReqIF import: + if (iE.subjectToFind) oE.subjectToFind = iE.subjectToFind; + if (iE.objectToFind) oE.objectToFind = iE.objectToFind; +// console.debug('statement 2int',iE,oE); + return oE } - // a hierarchy node: - function n2ext(iN: SpecifNode) { - // console.debug( 'n2ext', iN ); - // just take the non-redundant properties (omit 'title', for example): - let eN: SpecifNode = { - id: iN.id, - changedAt: iN.changedAt - }; - // for the time being, multiple revisions are not supported: - if (opts.revisionDate) { - // supply only the id, but not a key: - eN.resource = itemIdOf(iN.resource) + // a hierarchy: + function h2int(eH: SpecifNode) { + // the properties are stored with a resource, while the hierarchy is stored as a node with reference to that resource: + var iH: any; + if (names.hClasses) { + // up until v0.10.6, transform hierarchy root to a regular resource: + var iR = a2int(eH); + // ... and add a link to the hierarchy: + iH = { + id: 'N-' + iR.id, + resource: iR.id, + changedAt: eH.changedAt + }; + iR['class'] = eH[names.hClass]; + self.resources.push(iR); + + if (eH.revision) iH.revision = eH.revision.toString() } else { - // supply key or id: - eN.resource = iN.resource + // starting v0.10.8: + iH = i2int(eH); + iH.resource = eH.resource }; - if (iN.nodes && iN.nodes.length > 0) - eN.nodes = forAll(iN.nodes, n2ext); - if (iN.revision) - eN.revision = iN.revision; - return eN - } - // a file: - function f2ext(iF: IFileWithContent) { - var eF:SpecifFile = { - id: iF.id, // is the distinguishing/relative part of the URL - title: iF.title, - type: iF.type - }; - if (iF.blob) eF.blob = iF.blob; - if (iF.revision) eF.revision = iF.revision; - eF.changedAt = iF.changedAt; - if (iF.changedBy) eF.changedBy = iF.changedBy; - // if( iF.createdAt ) eF.createdAt = iF.createdAt; - // if( iF.createdBy ) eF.createdBy = iF.createdBy; - return eF - } - } - private cacheNode(e: INodeWithPosition): boolean { - // add or replace a node in a hierarchy; - // e may specify a predecessor or parent, the former prevails if both are specified - // - if there is no predecessor or it isn't found, insert as first element - // - if no parent is specified or the parent isn't found, insert at root level - - // 1. Delete the node, if it exists somewhere to prevent - // that there are multiple nodes with the same id; - // Thus, 'cacheNode' is in fact a 'move': - this.uncache('node', { id: e.id }); - // 2. Insert the node, if the predecessor exists somewhere: - if (iterateNodes(this.hierarchies, - // continue searching until found: - (nd: SpecifNode) => { return nd.id != e.predecessor }, - // insert the node after the predecessor: - (ndL: SpecifNode[]) => { - let i = indexById(ndL as Item[], e.predecessor); - if (i > -1) ndL.splice(i + 1, 0, e); - } - )) - return true; + // SpecIF allows resource references with id alone or with a key (id+revision): + iH.nodes = Lib.forAll(eH.nodes, n2int); +// console.debug('hierarchy 2int',eH,iH); + return iH - // 3. Insert the node, if the parent exists somewhere: - if (iterateNodes(this.hierarchies, - // continue searching until found: - (nd: SpecifNode) => { - if (nd.id == e['parent']) { - if (!Array.isArray(nd.nodes)) nd.nodes = []; - // we will not find a predecessor at this point any more, - // so insert as first element of the children: - nd.nodes.unshift(e); - return false; // stop searching + // a hierarchy node: + function n2int(eN) { + switch (typeof (eN.revision)) { + case 'number': + eN.revision = eN.revision.toString() }; - return true; // continue searching + Lib.forAll(eN.nodes, n2int); + return eN } - // no list function - ) - ) return true; - - // 4. insert the node as first root element, otherwise: - this.hierarchies.unshift(e); - return false; - } - cache(ctg: string, item: Item[] | Item): void { - if (!item || Array.isArray(item) && item.length < 1) - return; - // If item is a list, all elements must have the same category. - let fn = Array.isArray(item) ? cacheL : cacheE; - switch (ctg) { - case 'hierarchy': - case 'dataType': - case 'propertyClass': - case 'resourceClass': - case 'statementClass': - // @ts-ignore - addressing is perfectly ok - fn(this[standardTypes.listNameOf(ctg)], item); - return; - case 'resource': - case 'statement': - case 'file': - if (app.cache.cacheInstances) { - // @ts-ignore - addressing is perfectly ok - fn(this[standardTypes.listNameOf(ctg)], item); - }; - return; - case 'node': - if (Array.isArray(item)) - throw Error("No list of nodes supported."); - // console.debug('cache',ctg,item); - this.cacheNode(item as INodeWithPosition); - return - default: - throw Error("Invalid category '" + ctg + "'."); - }; - // all cases have a return statement .. - - } - readCache(ctg: string, itm: Item[] | Item | string): Item[] { - // Read an item from cache, unless 'reload' is specified. - // - itm can be single or a list, - // - each element can be an object with attribute id or an id string - // @ts-ignore - addressing is perfectly ok - let cch: Item[] = this[standardTypes.listNameOf(ctg)], - idx: number; - - if (itm == 'all') { - // return all cached items asynchronously: - return simpleClone(cch); // return a new list with the original elements - }; - - if (Array.isArray(itm)) { - let allFound = true, i = 0, I = itm.length; - var rL: Item[] = []; - while (allFound && i < I) { - idx = indexById(cch, itm[i].id || itm[i]); - if (idx > -1) { - rL.push(cch[idx]); - i++; - } else { - allFound = false; - } - }; - if (allFound) { - // console.debug( 'readCache array - allFound', cch, itm ); - return rL; - } else { - return []; - }; } - else { - // is a single item: - idx = indexById(cch, itm.id || itm); - if (idx > -1) { - return [cch[idx]] - } else { - return []; + // a file: + function f2int(iF) { + var oF = i2int(iF); + oF.title = iF.title ? iF.title.replace(/\\/g, '/') : iF.id; + // store the blob and it's type: + if (iF.blob) { + oF.type = iF.blob.type || iF.type || Lib.attachment2mediaType(iF.title); + oF.blob = iF.blob; } - }; - // console.debug('readCache - not found', ctg, itm); - } - uncache(ctg: string, item: Item): boolean | undefined { - if (!item) return; - let fn = Array.isArray(item) ? uncacheL : uncacheE; - switch (ctg) { - case 'hierarchy': - case 'dataType': - case 'propertyClass': - case 'resourceClass': - case 'statementClass': - // @ts-ignore - addressing is perfectly ok - return fn(this[standardTypes.listNameOf(ctg)], item); - case 'resource': - case 'statement': - case 'file': - if (app.cache.cacheInstances) - // @ts-ignore - addressing is perfectly ok - return fn(this[standardTypes.listNameOf(ctg)], item); - return; - case 'node': - if (Array.isArray(item)) - item.forEach((el: SpecifNode) => { delNodes(this.hierarchies, el) }) - else - delNodes(this.hierarchies, item as SpecifNode); - return; - /* default: return; // programming error */ - }; - // all cases have a return statement .. - - function delNodes(L: SpecifNode[], el: SpecifNode): void { - // Delete all nodes specified by the element; - // if el is the node, 'id' will be used to identify it (obviously at most one node), - // and if el is the referenced resource, 'resource' will be used to identify all referencing nodes. - if (Array.isArray(L)) - for (var h = L.length - 1; h > -1; h--) { - if (L[h].id == el.id || L[h].resource == el.resource) { - L.splice(h, 1); - break; // can't delete any children - }; - // step down, if the node hasn't been deleted: - delNodes(L[h].nodes as SpecifNode[], el); - }; + else if (iF.dataURL) { + oF.type = iF.type || Lib.attachment2mediaType(iF.title); + oF.dataURL = iF.dataURL; + } + else + oF.type = iF.type; + return oF } } -} + toExt(opts?: any): Promise { + // transform self.data to SpecIF following defined options; + // a clone is delivered. + // if opts.targetLanguage has no value, all available languages are kept. -const specif = { - check: (data: SpecIF, opts?: any): Promise => { - // Check the SpecIF data for schema compliance and consistency; - // no data of app.cache is modified: +// console.debug('toExt', this, opts ); + // transform internal data to SpecIF: return new Promise( - (resolve,reject)=>{ + (resolve, reject) => { + var pend = 0, + spD: SpecIF = { + id: this.id, + title: languageValueOf(this.title, opts), + $schema: 'https://specif.de/v' + CONFIG.specifVersion + '/schema.json', + generator: app.title, + generatorVersion: CONFIG.appVersion, + createdAt: new Date().toISOString() + }; - if (typeof (data) == 'object') { + if (this.description) spD.description = languageValueOf(this.description, opts); - // 1. Get the "official" routine for checking schema and constraints - // - where already loaded checking routines are replaced by the newly loaded ones - // - use $.ajax() with options since it is more flexible than $.getScript - // - the first (relative) URL is for debugging within a local clone of Github - // - both of the other (absolute) URLs are for a production environment - $.ajax({ - dataType: "script", - cache: true, - url: (data['$schema'] && data['$schema'].indexOf('v1.0')<0 ? - (window.location.href.startsWith('file:/') ? '../../SpecIF/check/check.js' - : 'https://specif.de/v' + /\/(?:v|specif-)([0-9]+\.[0-9]+)\//.exec(data['$schema'])[1] + '/check.min.js' ) - : 'https://specif.de/v1.0/check.js' ) // older versions are covered by v1.0/check.js - }) - .done( ()=>{ - // 2. Get the specified schema file: - httpGet({ - // @ts-ignore - 'specifVersion' is defined for versions <1.0 - url: (data['$schema'] || 'https://specif.de/v' + data.specifVersion + '/schema'), - responseType: 'arraybuffer', - withCredentials: false, - done: handleResult, - fail: handleError - }); - }) - .fail(handleError); + if (this.rights && this.rights.title && this.rights.url) + spD.rights = this.rights; + else + spD.rights = { + title: "Creative Commons 4.0 CC BY-SA", + url: "https://creativecommons.org/licenses/by-sa/4.0/" + }; + + if (app.me && app.me.email) { + spD.createdBy = { + familyName: app.me.lastName, + givenName: app.me.firstName, + email: { type: "text/html", value: app.me.email } + }; + if (app.me.organization) + spD.createdBy.org = { organizationName: app.me.organization }; } else { - reject({ status: 999, statusText: 'No SpecIF data to check' }); + if (this.createdBy && this.createdBy.email && this.createdBy.email.value) { + spD.createdBy = { + familyName: this.createdBy.familyName, + givenName: this.createdBy.givenName, + email: { type: "text/html", value: this.createdBy.email.value } + }; + if (this.createdBy.org && this.createdBy.org.organizationName) + spD.createdBy.org = this.createdBy.org; + }; + // else: don't add createdBy without data }; + + // Now start to assemble the SpecIF output: + spD.dataTypes = Lib.forAll(this.dataTypes, dT2ext); + spD.propertyClasses = Lib.forAll(this.propertyClasses, pC2ext); + spD.resourceClasses = Lib.forAll(this.resourceClasses, rC2ext); + spD.statementClasses = Lib.forAll(this.statementClasses, sC2ext); + spD.resources = Lib.forAll((opts.allResources ? this.resources : collectResourcesByHierarchy(this)), r2ext); + spD.statements = Lib.forAll(this.statements, s2ext); + spD.hierarchies = Lib.forAll(this.hierarchies, n2ext); + spD.files = []; + this.files.forEach((f) => { f2ext(f).then(finalize, reject); }); + if (pend < 1) finalize(); // no files, so finalize right away return; - function handleResult(xhr: XMLHttpRequest) { - if (typeof (checkSchema) == 'function' && typeof (checkConstraints) == 'function') { -// console.debug('schema', xhr); - // 1. check data against schema: - // @ts-ignore - checkSchema() is defined in check.js loaded at runtime - let rc: xhrMessage = checkSchema(data, { schema: JSON.parse(ab2str(xhr.response)) }); - if (rc.status == 0) { - // 2. Check further constraints: - // @ts-ignore - checkConstraints() is defined in check.js loaded at runtime - rc = checkConstraints(data, opts); - if (rc.status == 0) { - resolve(data); + function finalize(oF?) { + if(oF) spD.files.push(oF); + if (--pend < 1) { + // Check whether all statements reference resources or statements, which are listed. + // Obviously this check can only be done at the end .. + let lenBefore: number; + do { + lenBefore = spD.statements.length; + spD.statements = spD.statements.filter( + (s) => { + return (indexById(spD.resources, Lib.itemIdOf(s.subject)) > -1 + || indexById(spD.statements, Lib.itemIdOf(s.subject)) > -1) + && (indexById(spD.resources, Lib.itemIdOf(s.object)) > -1 + || indexById(spD.statements, Lib.itemIdOf(s.object)) > -1) + } + ); + console.info("Suppressed " + (lenBefore - spD.statements.length) + " statements, because subject or object are not listed."); + } + while (spD.statements.length < lenBefore); + + // Add a resource as hierarchyRoot, if needed. + // It is assumed, + // - that in general SpecIF data do not have a hierarchy root with meta-data. + // - that ReqIF specifications (=hierarchyRoots) are transformed to regular resources on input. + function outlineTypeIsNotHidden(hPL?): boolean { + if (!hPL || hPL.length < 1) return true; + for (var i = hPL.length - 1; i > -1; i--) { + if (hPL[i].title == CONFIG.propClassType + && (typeof (hPL[i].value) != 'string' || hPL[i].value == CONFIG.resClassOutline)) + return false; + }; + return true; + } + if (opts.createHierarchyRootIfNotPresent && aHierarchyHasNoRoot(spD)) { + + console.info("Adding a hierarchyRoot"); + addE("resourceClass", "RC-HierarchyRoot", spD); + + // ToDo: Let the program derive the referenced class ids from the above + addE("propertyClass", "PC-Type", spD); + addE("propertyClass", "PC-Description", spD); + addE("propertyClass", "PC-Name", spD); + addE("dataType", "DT-ShortString", spD); + addE("dataType", "DT-Text", spD); + + var res = { + id: 'R-' + simpleHash(spD.id), + title: spD.title, + class: "RC-HierarchyRoot", + properties: [{ + class: "PC-Name", + value: spD.title + }], + changedAt: spD.createdAt + }; + // Add the resource type, if it is not hidden: + let rC = itemById(spD.resourceClasses, "RC-HierarchyRoot"); + if (outlineTypeIsNotHidden(opts.hiddenProperties)) { + addP(res, { + class: "PC-Type", + value: rC.title // should be CONFIG.resClassOutline + }); + }; + // Add a description property only if it has a value: + if (spD.description) + addP(res, { + class: "PC-Description", + value: spD.description + }); + spD.resources.push(r2ext(res)); + // create a new root instance: + spD.hierarchies = [{ + id: "H-" + res.id, + resource: res.id, + // .. and add the previous hierarchies as children: + nodes: spD.hierarchies, + changedAt: spD.changedAt + }]; + }; + + // ToDo: schema and consistency check (if we want to detect any programming errors) + // console.debug('specif.toExt exit',spD); + resolve(spD); + }; + } + + function aHierarchyHasNoRoot(dta: SpecIF): boolean { + for (var i = dta.hierarchies.length - 1; i > -1; i--) { + let hR = itemById(dta.resources, dta.hierarchies[i].resource); + if (!hR) { + throw Error("Hierarchy '"+dta.hierarchies[i].id+"' is corrupt"); + }; + let prpV = valByTitle(hR, CONFIG.propClassType, dta), + hC = itemById(dta.resourceClasses, hR['class']); + // The type of the hierarchy root can be specified by a property titled CONFIG.propClassType + // or by the title of the resourceClass: + if ((!prpV || CONFIG.hierarchyRoots.indexOf(prpV) < 0) + && (!hC || CONFIG.hierarchyRoots.indexOf(hC.title) < 0)) + return true; + }; + return false; + } + // common for all items: + function i2ext(iE) { + var oE = { + id: iE.id, + changedAt: iE.changedAt + }; + // most items must have a title, but statements may come without: + if (iE.title) oE.title = titleOf(iE, opts); + if (iE.description) oE.description = languageValueOf(iE.description, opts); + if (iE.revision) oE.revision = iE.revision; + if (iE.replaces) oE.replaces = iE.replaces; + if (iE.changedBy) oE.changedBy = iE.changedBy; + if (iE.createdAt) oE.createdAt = iE.createdAt; + if (iE.createdBy) oE.createdBy = iE.createdBy; + return oE; + } + // a data type: + function dT2ext(iE: DataType) { + var oE: DataType = i2ext(iE); + oE.type = iE.type; + switch (iE.type) { + case "xs:double": + if (iE.fractionDigits) oE.fractionDigits = iE.fractionDigits; + case "xs:integer": + if (typeof (iE.minInclusive) == 'number') oE.minInclusive = iE.minInclusive; + if (typeof (iE.maxInclusive) == 'number') oE.maxInclusive = iE.maxInclusive; + break; + case "xhtml": + case "xs:string": + if (iE.maxLength) oE.maxLength = iE.maxLength; + break; + case "xs:enumeration": + if (opts.targetLanguage) + // reduce to the language specified: + oE.values = Lib.forAll(iE.values, (val) => { return { id: val.id, value: languageValueOf(val.value, opts) } }) + else + oE.values = iE.values + }; + return oE + } + // a property class: + function pC2ext(iE: PropertyClass) { + var oE: PropertyClass = i2ext(iE); + if (iE.value) oE.value = iE.value; // a default value + oE.dataType = iE.dataType; + let dT = itemById(spD.dataTypes, iE.dataType); + switch (dT.type) { + case 'xs:enumeration': + // With SpecIF, he 'multiple' property should be defined at dataType level + // and can be overridden at propertyType level. + // dT.multiple aTs.multiple aTs.multiple effect + // --------------------------------------------------------- + // undefined undefined undefined false + // false undefined undefined false + // true undefined undefined true + // undefined false undefined false + // false false undefined false + // true false false false + // undefined true true true + // false true true true + // true true undefined true + // Include the property only, if is different from the dataType's: + if (iE.multiple && !dT.multiple) oE.multiple = true + else if (iE.multiple == false && dT.multiple) oE.multiple = false + }; + return oE + } + // common for all instance classes: + function aC2ext(iE) { + var oE = i2ext(iE); + if (iE.icon) oE.icon = iE.icon; + if (iE.instantiation) oE.instantiation = iE.instantiation; + if (iE._extends) oE['extends'] = iE._extends; + if (iE.propertyClasses.length > 0) oE.propertyClasses = iE.propertyClasses; + return oE + } + // a resource class: + function rC2ext(iE: ResourceClass) { + var oE: ResourceClass = aC2ext(iE); + // Include "isHeading" in SpecIF only if true: + if (iE.isHeading) oE.isHeading = true; + return oE + } + // a statement class: + function sC2ext(iE: StatementClass) { + var oE: StatementClass = aC2ext(iE); + if (iE.isUndirected) oE.isUndirected = iE.isUndirected; + if (iE.subjectClasses && iE.subjectClasses.length > 0) oE.subjectClasses = iE.subjectClasses; + if (iE.objectClasses && iE.objectClasses.length > 0) oE.objectClasses = iE.objectClasses; + return oE + } + // a property: + function p2ext(iE: Property) { + // skip empty properties: + if (!iE.value) return; + + // skip hidden properties: + let pC: PropertyClass = itemById(spD.propertyClasses, iE['class']); + if (Array.isArray(opts.hiddenProperties)) { + opts.hiddenProperties.forEach((hP) => { + if (hP.title == (iE.title || pC.title) && (hP.value == undefined || hP.value == iE.value)) return; + }); + }; + + var oE: Property = { + // no id + class: iE['class'] + }; + if (iE.title) { + // skip the property title, if it is equal to the propertyClass' title: + let ti = titleOf(iE, opts); + if (ti != pC.title) oE.title = ti; + }; + if (iE.description) oE.description = languageValueOf(iE.description, opts); + + // According to the schema, all property values are represented by a string + // and we want to store them as string to avoid inaccuracies by multiple transformations: + if( opts.targetLanguage ) { + // reduce to the selected language; is used for generation of human readable documents + // or for formats not supporting multiple languages: + let dT: DataType = dataTypeOf(spD, iE['class']); + if (['xs:string', 'xhtml'].indexOf(dT.type) > -1) { + if (CONFIG.excludedFromFormatting.indexOf(iE.title || pC.title) > -1) { + // if it is e.g. a title, remove all formatting: + oE.value = languageValueOf(iE.value, opts) + .replace(/^\s+/, "") // remove any leading whiteSpace + .stripHTML(); } else { - reject(rc); + // otherwise transform to HTML, if possible; + // especially for publication, for example using WORD format: + oE.value = languageValueOf(iE.value, opts) + .replace(/^\s+/, "") // remove any leading whiteSpace + .makeHTML(opts) + .replace(/
\n/g, "
"); + + oE.value = refDiagramsAsImg(oE.value); }; + // return 'published' data structure (single language, ...): +// console.debug('p2ext',iE,languageValueOf( iE.value, opts ),oE.value); + return oE; + }; + }; + // else, keep full data structure: + if (Array.isArray(iE.value)) { + // Just to avoid the climbing through list and objects, unless necessary: + if (opts.allDiagramsAsImage) { + oE.value = []; + iE.value.forEach( + (iV) => { + oE.value.push({ text: refDiagramsAsImg(iV.text), language: iV.language }) + } + ); } - else { - // older versions of the checking routine don't set the responseType: - if (typeof (rc.responseText) == 'string' && rc.responseText.length > 0) - rc.responseType = 'text'; - reject(rc); + else + // no replacement of links: + oE.value = iE.value; + } + else + // iE.value is a string: + oE.value = refDiagramsAsImg(iE.value); + return oE; + + function refDiagramsAsImg(val: string): string { + if (opts.allDiagramsAsImage) { + // Replace all links to application files like BPMN by links to SVG images: + let replaced = false; + // @ts-ignore - $0 is never read, but must be specified anyways + val = val.replace(RE.tagObject, ($0, $1, $2) => { +// console.debug('#a', $0, $1, $2); + if ($1) $1 = $1.replace(RE.attrType, ($4, $5) => { +// console.debug('#b', $4, $5); + // ToDo: Further application file formats ... once in use. + // Use CONFIG.applTypes ... once appropriate. + if (["application/bpmn+xml"].indexOf($5) > -1) { + replaced = true; + return 'type="image/svg+xml"' + } + else + return $4; + }); + // @ts-ignore - $6 is never read, but must be specified anyways + if (replaced) $1 = $1.replace(RE.attrData, ($6, $7) => { +// console.debug('#c', $6, $7); + return 'data="' + $7.fileName() + '.svg"' + }); + return ' -1) + // if it is e.g. a title, remove all formatting: + oE.value = stripHTML(languageValueOf(iE.value, opts) + // remove any leading whiteSpace: + .replace(/^\s+/, "")); + else + // otherwise transform to HTML, if possible; + // especially for publication, for example using WORD format: + oE.value = languageValueOf(iE.value, opts) + // remove any leading whiteSpace: + .replace(/^\s+/, "") + .makeHTML(opts) + .replace(/
\n/g, "
"); + +// console.debug('p2ext',iE,languageValueOf( iE.value, opts ),oE.value); + break; + }; + // else: no break - return the original value + default: + // in case of 'xs:enumeration', + // an id of the dataType's value is given, so it can be taken directly: + oE.value = iE.value; }; } else { - reject({ status: 999, statusText: 'Standard routines checkSchema and checkConstraints are not available.' }); + // for SpecIF export, keep full data structure: + oE.value = iE.value; + }; + // properties do not have their own revision and change info; the parent's apply. + return oE; */ + } + // common for all instances: + function a2ext(iE) { + var oE = i2ext(iE); + // console.debug('a2ext',iE,opts); + // resources and hierarchies usually have individual titles, and so we will not lookup: + oE['class'] = iE['class']; + if (iE.alternativeIds) oE.alternativeIds = iE.alternativeIds; + if (iE.properties && iE.properties.length > 0) oE.properties = Lib.forAll(iE.properties, p2ext); + return oE; + } + // a resource: + function r2ext(iE: Resource) { + var oE: Resource = a2ext(iE); + // console.debug('resource 2int',iE,oE); + return oE; + } + // a statement: + function s2ext(iE: Statement) { + // console.debug('statement2ext',iE.title); + // Skip statements with an open end; + // At the end it will be checked, wether all referenced resources resp. statements are listed: + if (!iE.subject || Lib.itemIdOf(iE.subject) == CONFIG.placeholder + || !iE.object || Lib.itemIdOf(iE.object) == CONFIG.placeholder + ) return; + + // The statements usually do use a vocabulary item (and not have an individual title), + // so we lookup, if so desired, e.g. when exporting to ePub: + var oE: Statement = a2ext(iE); + + // Skip the title, if it is equal to the statementClass' title; + // ToDo: remove limitation of single language. + if (oE.title && typeof (oE.title) == "string") { + let sC = itemById(spD.statementClasses, iE['class']); + if (typeof (sC.title) == "string" && oE.title == sC.title) + delete oE.title; + }; + + // if( iE.isUndirected ) oE.isUndirected = iE.isUndirected; + // for the time being, multiple revisions are not supported: + if (opts.revisionDate) { + // supply only the id, but not a key: + oE.subject = Lib.itemIdOf(iE.subject); + oE.object = Lib.itemIdOf(iE.object); } + else { + // supply key or id: + oE.subject = iE.subject; + oE.object = iE.object; + }; + return oE; } - function handleError(xhr: xhrMessage) { - switch (xhr.status) { - case 404: - // @ts-ignore - 'specifVersion' is defined for versions <1.0 - let v = data.specifVersion ? 'version ' + data.specifVersion : 'with Schema ' + data['$schema']; - xhr = { status: 903, statusText: 'SpecIF ' + v + ' is not supported by the program!' }; - // no break - default: - reject(xhr); + // a hierarchy node: + function n2ext(iN: SpecifNode) { +// console.debug( 'n2ext', iN ); + // just take the non-redundant properties (omit 'title', for example): + let oN: SpecifNode = { + id: iN.id, + // for the time being, multiple revisions are not supported: + // supply only the id, but not a key + // | supply key or id + resource: opts.revisionDate? Lib.itemIdOf(iN.resource) : iN.resource, + changedAt: iN.changedAt }; + + if (iN.nodes && iN.nodes.length > 0) + oN.nodes = Lib.forAll(iN.nodes, n2ext); + if (iN.revision) + oN.revision = iN.revision; + return oN + } + // a file: + function f2ext(iF: IFileWithContent): Promise { + return new Promise( + (resolve, reject) => { +// console.debug('f2ext',iF,opts) + + if (!opts || !opts.allDiagramsAsImage || CONFIG.imgTypes.indexOf(iF.type) > -1 ) { + var oF: IFileWithContent = { + id: iF.id, + title: iF.title, + type: iF.type, + changedAt: iF.changedAt + }; + if (iF.revision) oF.revision = iF.revision; + if (iF.changedBy) oF.changedBy = iF.changedBy; +// if( iF.createdAt ) oF.createdAt = iF.createdAt; +// if( iF.createdBy ) oF.createdBy = iF.createdBy; + if (iF.blob) oF.blob = iF.blob; + if (iF.dataURL) oF.dataURL = iF.dataURL; + resolve(oF); + } + else { + // Transform to an image: + // Remember to also replace any referencing links in property values! + switch (iF.type) { + case 'application/bpmn+xml': + pend++; + // Read and render BPMN as SVG: + Lib.blob2text(iF, (txt: string) => { + bpmn2svg(txt).then( + (result) => { + let nFileName = iF.title.fileName() + '.svg'; + resolve({ + // blob: new Blob([result.svg], { type: "image/svg+xml; charset=utf-8" }), + blob: new Blob([result.svg], { type: "image/svg+xml" }), + id: 'F-' + simpleHash(nFileName), + title: nFileName, + type: 'image/svg+xml', + changedAt: iF.changedAt + } as IFileWithContent ) + }, + reject + ) + }); + break; + default: + reject({status:999,statusText:"Cannot transform file '"+iF.title+"' of type '"+iF.type+"' to an image."}) + }; + }; + } + ); } } - ); + ) } } diff --git a/src/vendor/assets/javascripts/BPMN2SpecIF.js b/src/vendor/assets/javascripts/BPMN2SpecIF.js index ee1c458..d80fe6f 100644 --- a/src/vendor/assets/javascripts/BPMN2SpecIF.js +++ b/src/vendor/assets/javascripts/BPMN2SpecIF.js @@ -129,10 +129,11 @@ function BPMN2Specif( xmlString, opts ) { changedAt: opts.fileDate }]; - const nbsp = ' ', // non-breakable space + const +// nbsp = ' ', // non-breakable space apx = simpleHash(model.id), diagramId = 'D-' + apx, - hId = 'BPMN-outline-' + apx, +// hId = 'BPMN-outline-' + apx, diagRef = ''+opts.fileName+''; // 1. Add the folders: @@ -1247,7 +1248,7 @@ function BPMN2Specif( xmlString, opts ) { if( L[i][p]==s ) return L[i] // return list item } } - function indexBy( L, p, s ) { +/* function indexBy( L, p, s ) { if( L && p && s ) { // Return the index of an element in list 'L' whose property 'p' equals searchterm 's': // hand in property and searchTerm as string ! @@ -1255,7 +1256,7 @@ function BPMN2Specif( xmlString, opts ) { if( L[i][p]==s ) return i }; return -1 - } + } */ // Make a very simple hash code from a string: // http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ function simpleHash(str) {for(var r=0,i=0;i ul.jqtree_common { + display: none; + } + + ul.jqtree-tree li.jqtree_common { + clear: both; + list-style-type: none; + } + + ul.jqtree-tree .jqtree-toggler { + border-bottom: none; + color: #333; + text-decoration: none; + vertical-align: middle; + } + + ul.jqtree-tree .jqtree-toggler:hover { + color: #000; + text-decoration: none; + } + + ul.jqtree-tree .jqtree-toggler.jqtree-closed { + background-position: 0 0; + } + + ul.jqtree-tree .jqtree-toggler.jqtree-toggler-left { + margin-right: 0.5em; + } + + ul.jqtree-tree .jqtree-toggler.jqtree-toggler-right { + margin-left: 0.5em; + } + + ul.jqtree-tree .jqtree-element { + cursor: pointer; + position: relative; + display: flex; + } + + ul.jqtree-tree .jqtree-title { + color: #1c4257; + vertical-align: middle; + } + + ul.jqtree-tree .jqtree-title-button-left { + margin-left: 1.5em; + } + + ul.jqtree-tree .jqtree-title-button-left.jqtree-title-folder { + margin-left: 0; + } + + ul.jqtree-tree li.jqtree-folder { + margin-bottom: 4px; + } + + ul.jqtree-tree li.jqtree-folder.jqtree-closed { + margin-bottom: 1px; + } + + ul.jqtree-tree li.jqtree-ghost { + position: relative; + z-index: 10; + margin-right: 10px; + } + + ul.jqtree-tree li.jqtree-ghost span { + display: block; + } + + ul.jqtree-tree li.jqtree-ghost span.jqtree-circle { + border: solid 2px #0000ff; + border-radius: 100px; + height: 8px; + width: 8px; + position: absolute; + top: -4px; + left: -6px; + box-sizing: border-box; + } + + ul.jqtree-tree li.jqtree-ghost span.jqtree-line { + background-color: #0000ff; + height: 2px; + padding: 0; + position: absolute; + top: -1px; + left: 2px; + width: 100%; + } + + ul.jqtree-tree li.jqtree-ghost.jqtree-inside { + margin-left: 48px; + } + + ul.jqtree-tree span.jqtree-border { + position: absolute; + display: block; + left: -2px; + top: 0; + border: solid 2px #0000ff; + border-radius: 6px; + margin: 0; + box-sizing: content-box; + } + + ul.jqtree-tree li.jqtree-selected > .jqtree-element, + ul.jqtree-tree li.jqtree-selected > .jqtree-element:hover { + background-color: #97bdd6; + background: linear-gradient(#bee0f5, #89afca); + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + } + + ul.jqtree-tree .jqtree-moving > .jqtree-element .jqtree-title { + outline: dashed 1px #0000ff; + } + +ul.jqtree-tree.jqtree-rtl { + direction: rtl; +} + +ul.jqtree-tree.jqtree-rtl ul.jqtree_common { + margin-left: 0; + margin-right: 12px; + } + +ul.jqtree-tree.jqtree-rtl .jqtree-toggler { + margin-left: 0.5em; + margin-right: 0; + } + +ul.jqtree-tree.jqtree-rtl .jqtree-title { + margin-left: 0; + margin-right: 1.5em; + } + +ul.jqtree-tree.jqtree-rtl .jqtree-title.jqtree-title-folder { + margin-right: 0; + } + +ul.jqtree-tree.jqtree-rtl li.jqtree-ghost { + margin-right: 0; + margin-left: 10px; + } + +ul.jqtree-tree.jqtree-rtl li.jqtree-ghost span.jqtree-circle { + right: -6px; + } + +ul.jqtree-tree.jqtree-rtl li.jqtree-ghost span.jqtree-line { + right: 2px; + } + +ul.jqtree-tree.jqtree-rtl li.jqtree-ghost.jqtree-inside { + margin-left: 0; + margin-right: 48px; + } + +ul.jqtree-tree.jqtree-rtl span.jqtree-border { + right: -2px; + } + +span.jqtree-dragging { + color: #fff; + background: #000; + opacity: 0.6; + cursor: pointer; + padding: 2px 8px; +} diff --git a/src/vendor/assets/javascripts/jqtree.js b/src/vendor/assets/javascripts/jqtree.js new file mode 100644 index 0000000..d0203e4 --- /dev/null +++ b/src/vendor/assets/javascripts/jqtree.js @@ -0,0 +1,21 @@ +/* +JqTree 1.6.1 + +Copyright 2021 Marco Braak + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +@license + +*/ +var jqtree=function(e){"use strict";function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);t&&(i=i.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,i)}return n}function n(e){for(var n=1;ne.length)&&(t=e.length);for(var n=0,i=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[i++]}},e:function(e){throw e},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,s=!0,a=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){a=!0,o=e},f:function(){try{s||null==n.return||n.return()}finally{if(a)throw o}}}}var k;!function(e){e[e.Before=1]="Before",e[e.After=2]="After",e[e.Inside=3]="Inside",e[e.None=4]="None"}(k||(k={}));var N={before:k.Before,after:k.After,inside:k.Inside,none:k.None},S=function(e){for(var t in N)if(Object.prototype.hasOwnProperty.call(N,t)&&N[t]===e)return t;return""},_=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:e;r(this,e),a(this,"id",void 0),a(this,"name",void 0),a(this,"children",void 0),a(this,"parent",void 0),a(this,"idMapping",void 0),a(this,"tree",void 0),a(this,"nodeClass",void 0),a(this,"load_on_demand",void 0),a(this,"is_open",void 0),a(this,"element",void 0),a(this,"is_loading",void 0),a(this,"isEmptyFolder",void 0),this.name="",this.isEmptyFolder=!1,this.load_on_demand=!1,this.setData(t),this.children=[],this.parent=null,n&&(this.idMapping=new Map,this.tree=this,this.nodeClass=i)}return s(e,[{key:"setData",value:function(e){if(e)if("string"==typeof e)this.name=e;else if("object"===i(e))for(var t in e)if(Object.prototype.hasOwnProperty.call(e,t)){var n=e[t];"label"===t||"name"===t?"string"==typeof n&&(this.name=n):"children"!==t&&"parent"!==t&&(this[t]=n)}}},{key:"loadFromData",value:function(e){this.removeChildren();var t,n=y(e);try{for(n.s();!(t=n.n()).done;){var r=t.value,o=this.createNode(r);this.addChild(o),"object"===i(r)&&r.children&&r.children instanceof Array&&(0===r.children.length?o.isEmptyFolder=!0:o.loadFromData(r.children))}}catch(e){n.e(e)}finally{n.f()}return this}},{key:"addChild",value:function(e){this.children.push(e),e.setParent(this)}},{key:"addChildAtPosition",value:function(e,t){this.children.splice(t,0,e),e.setParent(this)}},{key:"removeChild",value:function(e){e.removeChildren(),this.doRemoveChild(e)}},{key:"getChildIndex",value:function(e){return this.children.indexOf(e)}},{key:"hasChildren",value:function(){return 0!==this.children.length}},{key:"isFolder",value:function(){return this.hasChildren()||this.load_on_demand}},{key:"iterate",value:function(e){!function t(n,i){if(n.children){var r,o=y(n.children);try{for(o.s();!(r=o.n()).done;){var s=r.value;e(s,i)&&s.hasChildren()&&t(s,i+1)}}catch(e){o.e(e)}finally{o.f()}}}(this,0)}},{key:"moveNode",value:function(e,t,n){if(!e.parent||e.isParentOf(t))return!1;switch(e.parent.doRemoveChild(e),n){case k.After:return!!t.parent&&(t.parent.addChildAtPosition(e,t.parent.getChildIndex(t)+1),!0);case k.Before:return!!t.parent&&(t.parent.addChildAtPosition(e,t.parent.getChildIndex(t)),!0);case k.Inside:return t.addChildAtPosition(e,0),!0;default:return!1}}},{key:"getData",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=function e(t){return t.map((function(t){var n={};for(var i in t)if(-1===["parent","children","element","idMapping","load_on_demand","nodeClass","tree","isEmptyFolder"].indexOf(i)&&Object.prototype.hasOwnProperty.call(t,i)){var r=t[i];n[i]=r}return t.hasChildren()&&(n.children=e(t.children)),n}))};return t(e?[this]:this.children)}},{key:"getNodeByName",value:function(e){return this.getNodeByCallback((function(t){return t.name===e}))}},{key:"getNodeByNameMustExist",value:function(e){var t=this.getNodeByCallback((function(t){return t.name===e}));if(!t)throw"Node with name ".concat(e," not found");return t}},{key:"getNodeByCallback",value:function(e){var t=null;return this.iterate((function(n){return!t&&(!e(n)||(t=n,!1))})),t}},{key:"addAfter",value:function(e){if(this.parent){var t=this.createNode(e),n=this.parent.getChildIndex(this);return this.parent.addChildAtPosition(t,n+1),"object"===i(e)&&e.children&&e.children instanceof Array&&e.children.length&&t.loadFromData(e.children),t}return null}},{key:"addBefore",value:function(e){if(this.parent){var t=this.createNode(e),n=this.parent.getChildIndex(this);return this.parent.addChildAtPosition(t,n),"object"===i(e)&&e.children&&e.children instanceof Array&&e.children.length&&t.loadFromData(e.children),t}return null}},{key:"addParent",value:function(e){if(this.parent){var t=this.createNode(e);this.tree&&t.setParent(this.tree);var n,i=this.parent,r=y(i.children);try{for(r.s();!(n=r.n()).done;){var o=n.value;t.addChild(o)}}catch(e){r.e(e)}finally{r.f()}return i.children=[],i.addChild(t),t}return null}},{key:"remove",value:function(){this.parent&&(this.parent.removeChild(this),this.parent=null)}},{key:"append",value:function(e){var t=this.createNode(e);return this.addChild(t),"object"===i(e)&&e.children&&e.children instanceof Array&&e.children.length&&t.loadFromData(e.children),t}},{key:"prepend",value:function(e){var t=this.createNode(e);return this.addChildAtPosition(t,0),"object"===i(e)&&e.children&&e.children instanceof Array&&e.children.length&&t.loadFromData(e.children),t}},{key:"isParentOf",value:function(e){for(var t=e.parent;t;){if(t===this)return!0;t=t.parent}return!1}},{key:"getLevel",value:function(){for(var e=0,t=this;t.parent;)e+=1,t=t.parent;return e}},{key:"getNodeById",value:function(e){return this.idMapping.get(e)||null}},{key:"addNodeToIndex",value:function(e){null!=e.id&&this.idMapping.set(e.id,e)}},{key:"removeNodeFromIndex",value:function(e){null!=e.id&&this.idMapping.delete(e.id)}},{key:"removeChildren",value:function(){var e=this;this.iterate((function(t){var n;return null===(n=e.tree)||void 0===n||n.removeNodeFromIndex(t),!0})),this.children=[]}},{key:"getPreviousSibling",value:function(){if(this.parent){var e=this.parent.getChildIndex(this)-1;return e>=0?this.parent.children[e]:null}return null}},{key:"getNextSibling",value:function(){if(this.parent){var e=this.parent.getChildIndex(this)+1;return e0&&void 0!==arguments[0])||arguments[0];if(e&&this.hasChildren()&&this.is_open)return this.children[0];if(this.parent){var t=this.getNextSibling();return t||this.parent.getNextNode(!1)}return null}},{key:"getPreviousNode",value:function(){if(this.parent){var e=this.getPreviousSibling();return e?e.hasChildren()&&e.is_open?e.getLastChild():e:this.getParent()}return null}},{key:"getParent",value:function(){return this.parent&&this.parent.parent?this.parent:null}},{key:"getLastChild",value:function(){if(this.hasChildren()){var e=this.children[this.children.length-1];return e.hasChildren()&&e.is_open?e.getLastChild():e}return null}},{key:"initFromData",value:function(e){var t,n=this,r=function(e){var t,i=y(e);try{for(i.s();!(t=i.n()).done;){var r=t.value,o=n.createNode();o.initFromData(r),n.addChild(o)}}catch(e){i.e(e)}finally{i.f()}};t=e,n.setData(t),"object"===i(t)&&t.children&&t.children instanceof Array&&t.children.length&&r(t.children)}},{key:"setParent",value:function(e){var t;this.parent=e,this.tree=e.tree,null===(t=this.tree)||void 0===t||t.addNodeToIndex(this)}},{key:"doRemoveChild",value:function(e){var t;this.children.splice(this.getChildIndex(e),1),null===(t=this.tree)||void 0===t||t.removeNodeFromIndex(e)}},{key:"getNodeClass",value:function(){var t;return this.nodeClass||(null==this||null===(t=this.tree)||void 0===t?void 0:t.nodeClass)||e}},{key:"createNode",value:function(e){return new(this.getNodeClass())(e)}}]),e}(),D=function(){function e(t){r(this,e),a(this,"hitAreas",void 0),a(this,"isDragging",void 0),a(this,"currentItem",void 0),a(this,"hoveredArea",void 0),a(this,"positionInfo",void 0),a(this,"treeWidget",void 0),a(this,"dragElement",void 0),a(this,"previousGhost",void 0),a(this,"openFolderTimer",void 0),this.treeWidget=t,this.hoveredArea=null,this.hitAreas=[],this.isDragging=!1,this.currentItem=null,this.positionInfo=null}return s(e,[{key:"mouseCapture",value:function(e){var t=jQuery(e.target);if(!this.mustCaptureElement(t))return null;if(this.treeWidget.options.onIsMoveHandle&&!this.treeWidget.options.onIsMoveHandle(t))return null;var n=this.treeWidget._getNodeElement(t);return n&&this.treeWidget.options.onCanMove&&(this.treeWidget.options.onCanMove(n.node)||(n=null)),this.currentItem=n,null!=this.currentItem}},{key:"mouseStart",value:function(e){var t;if(!this.currentItem||void 0===e.pageX||void 0===e.pageY)return!1;this.refresh();var n=jQuery(e.target).offset(),i=n?n.left:0,r=n?n.top:0,o=this.currentItem.node;return this.dragElement=new I(o.name,e.pageX-i,e.pageY-r,this.treeWidget.element,null===(t=this.treeWidget.options.autoEscape)||void 0===t||t),this.isDragging=!0,this.positionInfo=e,this.currentItem.$element.addClass("jqtree-moving"),!0}},{key:"mouseDrag",value:function(e){if(!this.currentItem||!this.dragElement||void 0===e.pageX||void 0===e.pageY)return!1;this.dragElement.move(e.pageX,e.pageY),this.positionInfo=e;var t=this.findHoveredArea(e.pageX,e.pageY);return t&&this.canMoveToArea(t)?(t.node.isFolder()||this.stopOpenFolderTimer(),this.hoveredArea!==t&&(this.hoveredArea=t,this.mustOpenFolderTimer(t)?this.startOpenFolderTimer(t.node):this.stopOpenFolderTimer(),this.updateDropHint())):(this.removeDropHint(),this.stopOpenFolderTimer(),this.hoveredArea=t),t||this.treeWidget.options.onDragMove&&this.treeWidget.options.onDragMove(this.currentItem.node,e.originalEvent),!0}},{key:"mouseStop",value:function(e){this.moveItem(e),this.clear(),this.removeHover(),this.removeDropHint(),this.removeHitAreas();var t=this.currentItem;return this.currentItem&&(this.currentItem.$element.removeClass("jqtree-moving"),this.currentItem=null),this.isDragging=!1,this.positionInfo=null,!this.hoveredArea&&t&&this.treeWidget.options.onDragStop&&this.treeWidget.options.onDragStop(t.node,e.originalEvent),!1}},{key:"refresh",value:function(){this.removeHitAreas(),this.currentItem&&(this.generateHitAreas(),this.currentItem=this.treeWidget._getNodeElementForNode(this.currentItem.node),this.isDragging&&this.currentItem.$element.addClass("jqtree-moving"))}},{key:"generateHitAreas",value:function(){if(this.currentItem){var e=new b(this.treeWidget.tree,this.currentItem.node,this.getTreeDimensions().bottom);this.hitAreas=e.generate()}else this.hitAreas=[]}},{key:"mustCaptureElement",value:function(e){return!e.is("input,select,textarea")}},{key:"canMoveToArea",value:function(e){if(!this.treeWidget.options.onCanMoveTo)return!0;if(!this.currentItem)return!1;var t=S(e.position);return this.treeWidget.options.onCanMoveTo(this.currentItem.node,e.node,t)}},{key:"removeHitAreas",value:function(){this.hitAreas=[]}},{key:"clear",value:function(){this.dragElement&&(this.dragElement.remove(),this.dragElement=null)}},{key:"removeDropHint",value:function(){this.previousGhost&&this.previousGhost.remove()}},{key:"removeHover",value:function(){this.hoveredArea=null}},{key:"findHoveredArea",value:function(e,t){var n=this.getTreeDimensions();if(en.right||t>n.bottom)return null;for(var i=0,r=this.hitAreas.length;i>1,s=this.hitAreas[o];if(ts.bottom))return s;i=o+1}}return null}},{key:"mustOpenFolderTimer",value:function(e){var t=e.node;return t.isFolder()&&!t.is_open&&e.position===k.Inside}},{key:"updateDropHint",value:function(){if(this.hoveredArea){this.removeDropHint();var e=this.treeWidget._getNodeElementForNode(this.hoveredArea.node);this.previousGhost=e.addDropHint(this.hoveredArea.position)}}},{key:"startOpenFolderTimer",value:function(e){var t=this;this.stopOpenFolderTimer(),this.openFolderTimer=window.setTimeout((function(){t.treeWidget._openNode(e,t.treeWidget.options.slide,(function(){t.refresh(),t.updateDropHint()}))}),this.treeWidget.options.openFolderDelay)}},{key:"stopOpenFolderTimer",value:function(){this.openFolderTimer&&(clearTimeout(this.openFolderTimer),this.openFolderTimer=null)}},{key:"moveItem",value:function(e){var t=this;if(this.currentItem&&this.hoveredArea&&this.hoveredArea.position!==k.None&&this.canMoveToArea(this.hoveredArea)){var n=this.currentItem.node,i=this.hoveredArea.node,r=this.hoveredArea.position,o=n.parent;r===k.Inside&&(this.hoveredArea.node.is_open=!0);var s=function(){t.treeWidget.tree.moveNode(n,i,r),t.treeWidget.element.empty(),t.treeWidget._refreshElements(null)};this.treeWidget._triggerEvent("tree.move",{move_info:{moved_node:n,target_node:i,position:S(r),previous_parent:o,do_move:s,original_event:e.originalEvent}}).isDefaultPrevented()||s()}}},{key:"getTreeDimensions",value:function(){var e=this.treeWidget.element.offset();if(e){var t=this.treeWidget.element,n=t.width()||0,i=t.height()||0,r=e.left+this.treeWidget._getScrollLeft();return{left:r,top:e.top,right:r+n,bottom:e.top+i+16}}return{left:0,top:0,right:0,bottom:0}}}]),e}(),b=function(e){l(n,e);var t=v(n);function n(e,i,o){var s;return r(this,n),a(h(s=t.call(this,e)),"currentNode",void 0),a(h(s),"treeBottom",void 0),a(h(s),"positions",void 0),a(h(s),"lastTop",void 0),s.currentNode=i,s.treeBottom=o,s}return s(n,[{key:"generate",value:function(){return this.positions=[],this.lastTop=0,this.iterate(),this.generateHitAreas(this.positions)}},{key:"generateHitAreas",value:function(e){var t,n=-1,i=[],r=[],o=y(e);try{for(o.s();!(t=o.n()).done;){var s=t.value;s.top!==n&&i.length&&(i.length&&this.generateHitAreasForGroup(r,i,n,s.top),n=s.top,i=[]),i.push(s)}}catch(e){o.e(e)}finally{o.f()}return this.generateHitAreasForGroup(r,i,n,this.treeBottom),r}},{key:"handleOpenFolder",value:function(e,t){return e!==this.currentNode&&(e.children[0]!==this.currentNode&&this.addPosition(e,k.Inside,this.getTop(t)),!0)}},{key:"handleClosedFolder",value:function(e,t,n){var i=this.getTop(n);e===this.currentNode?this.addPosition(e,k.None,i):(this.addPosition(e,k.Inside,i),t!==this.currentNode&&this.addPosition(e,k.After,i))}},{key:"handleFirstNode",value:function(e){e!==this.currentNode&&this.addPosition(e,k.Before,this.getTop(jQuery(e.element)))}},{key:"handleAfterOpenFolder",value:function(e,t){e===this.currentNode||t===this.currentNode?this.addPosition(e,k.None,this.lastTop):this.addPosition(e,k.After,this.lastTop)}},{key:"handleNode",value:function(e,t,n){var i=this.getTop(n);e===this.currentNode?this.addPosition(e,k.None,i):this.addPosition(e,k.Inside,i),t===this.currentNode||e===this.currentNode?this.addPosition(e,k.None,i):this.addPosition(e,k.After,i)}},{key:"getTop",value:function(e){var t=e.offset();return t?t.top:0}},{key:"addPosition",value:function(e,t,n){var i={top:n,bottom:0,node:e,position:t};this.positions.push(i),this.lastTop=n}},{key:"generateHitAreasForGroup",value:function(e,t,n,i){for(var r=Math.min(t.length,4),o=Math.round((i-n)/r),s=n,a=0;a").addClass("jqtree-title jqtree-dragging"),s?this.$element.text(t):this.$element.html(t),this.$element.css("position","absolute"),o.append(this.$element)}return s(e,[{key:"move",value:function(e,t){this.$element.offset({left:e-this.offsetX,top:t-this.offsetY})}},{key:"remove",value:function(){this.$element.remove()}}]),e}(),j=function(e){return e?"true":"false"},E=function(){function e(t){r(this,e),a(this,"openedIconElement",void 0),a(this,"closedIconElement",void 0),a(this,"treeWidget",void 0),this.treeWidget=t,this.openedIconElement=this.createButtonElement(t.options.openedIcon||"+"),this.closedIconElement=this.createButtonElement(t.options.closedIcon||"-")}return s(e,[{key:"render",value:function(e){e&&e.parent?this.renderFromNode(e):this.renderFromRoot()}},{key:"renderFromRoot",value:function(){var e=this.treeWidget.element;e.empty(),this.createDomElements(e[0],this.treeWidget.tree.children,!0,1)}},{key:"renderFromNode",value:function(e){var t=jQuery(e.element),n=this.createLi(e,e.getLevel());this.attachNodeData(e,n),t.after(n),t.remove(),e.children&&this.createDomElements(n,e.children,!1,e.getLevel()+1)}},{key:"createDomElements",value:function(e,t,n,i){var r=this.createUl(n);e.appendChild(r);var o,s=y(t);try{for(s.s();!(o=s.n()).done;){var a=o.value,l=this.createLi(a,i);r.appendChild(l),this.attachNodeData(a,l),a.hasChildren()&&this.createDomElements(l,a.children,!1,i+1)}}catch(e){s.e(e)}finally{s.f()}}},{key:"attachNodeData",value:function(e,t){e.element=t,jQuery(t).data("node",e)}},{key:"createUl",value:function(e){var t,n;e?(t="jqtree-tree",n="tree",this.treeWidget.options.rtl&&(t+=" jqtree-rtl")):(t="",n="group"),this.treeWidget.options.dragAndDrop&&(t+=" jqtree-dnd");var i=document.createElement("ul");return i.className="jqtree_common ".concat(t),i.setAttribute("role",n),i}},{key:"createLi",value:function(e,t){var n=Boolean(this.treeWidget.selectNodeHandler.isNodeSelected(e)),i=e.isFolder()||e.isEmptyFolder&&this.treeWidget.options.showEmptyFolder?this.createFolderLi(e,t,n):this.createNodeLi(e,t,n);return this.treeWidget.options.onCreateLi&&this.treeWidget.options.onCreateLi(e,jQuery(i),n),i}},{key:"createFolderLi",value:function(e,t,n){var i=this.getButtonClasses(e),r=this.getFolderClasses(e,n),o=e.is_open?this.openedIconElement:this.closedIconElement,s=document.createElement("li");s.className="jqtree_common ".concat(r),s.setAttribute("role","presentation");var a=document.createElement("div");a.className="jqtree-element jqtree_common",a.setAttribute("role","presentation"),s.appendChild(a);var l=document.createElement("a");return l.className=i,l.appendChild(o.cloneNode(!0)),l.setAttribute("role","presentation"),l.setAttribute("aria-hidden","true"),this.treeWidget.options.buttonLeft&&a.appendChild(l),a.appendChild(this.createTitleSpan(e.name,t,n,e.is_open,!0)),this.treeWidget.options.buttonLeft||a.appendChild(l),s}},{key:"createNodeLi",value:function(e,t,n){var i=["jqtree_common"];n&&i.push("jqtree-selected");var r=i.join(" "),o=document.createElement("li");o.className=r,o.setAttribute("role","presentation");var s=document.createElement("div");return s.className="jqtree-element jqtree_common",s.setAttribute("role","presentation"),o.appendChild(s),s.appendChild(this.createTitleSpan(e.name,t,n,e.is_open,!1)),o}},{key:"createTitleSpan",value:function(e,t,n,i,r){var o=document.createElement("span"),s="jqtree-title jqtree_common";if(r&&(s+=" jqtree-title-folder"),s+=" jqtree-title-button-".concat(this.treeWidget.options.buttonLeft?"left":"right"),o.className=s,o.setAttribute("role","treeitem"),o.setAttribute("aria-level","".concat(t)),o.setAttribute("aria-selected",j(n)),o.setAttribute("aria-expanded",j(i)),n){var a=this.treeWidget.options.tabIndex;void 0!==a&&o.setAttribute("tabindex","".concat(a))}return this.treeWidget.options.autoEscape?o.textContent=e:o.innerHTML=e,o}},{key:"getButtonClasses",value:function(e){var t=["jqtree-toggler","jqtree_common"];return e.is_open||t.push("jqtree-closed"),this.treeWidget.options.buttonLeft?t.push("jqtree-toggler-left"):t.push("jqtree-toggler-right"),t.join(" ")}},{key:"getFolderClasses",value:function(e,t){var n=["jqtree-folder"];return e.is_open||n.push("jqtree-closed"),t&&n.push("jqtree-selected"),e.is_loading&&n.push("jqtree-loading"),n.join(" ")}},{key:"createButtonElement",value:function(e){if("string"==typeof e){var t=document.createElement("div");return t.innerHTML=e,document.createTextNode(t.innerHTML)}return jQuery(e)[0]}}]),e}(),C=function(){function e(t){r(this,e),a(this,"treeWidget",void 0),this.treeWidget=t}return s(e,[{key:"loadFromUrl",value:function(e,t,n){var i=this;if(e){var r=this.getDomElement(t);this.addLoadingClass(r),this.notifyLoading(!0,t,r);var o=function(){i.removeLoadingClass(r),i.notifyLoading(!1,t,r)};this.submitRequest(e,(function(e){o(),i.treeWidget.loadData(i.parseData(e),t),n&&"function"==typeof n&&n()}),(function(e){o(),i.treeWidget.options.onLoadFailed&&i.treeWidget.options.onLoadFailed(e)}))}}},{key:"addLoadingClass",value:function(e){e&&e.addClass("jqtree-loading")}},{key:"removeLoadingClass",value:function(e){e&&e.removeClass("jqtree-loading")}},{key:"getDomElement",value:function(e){return e?jQuery(e.element):this.treeWidget.element}},{key:"notifyLoading",value:function(e,t,n){this.treeWidget.options.onLoading&&this.treeWidget.options.onLoading(e,t,n),this.treeWidget._triggerEvent("tree.loading_data",{isLoading:e,node:t,$el:n})}},{key:"submitRequest",value:function(e,t,i){var r,o=n({method:"GET",cache:!1,dataType:"json",success:t,error:i},"string"==typeof e?{url:e}:e);o.method=(null===(r=o.method)||void 0===r?void 0:r.toUpperCase())||"GET",jQuery.ajax(o)}},{key:"parseData",value:function(e){var t=this.treeWidget.options.dataFilter,n="string"==typeof e?JSON.parse(e):e;return t?t(n):n}}]),e}(),w=function(){function e(t){var n=this;r(this,e),a(this,"treeWidget",void 0),a(this,"handleKeyDown",(function(t){if(!n.canHandleKeyboard())return!0;var i=n.treeWidget.getSelectedNode();if(!i)return!0;switch(t.which){case e.DOWN:return n.moveDown(i);case e.UP:return n.moveUp(i);case e.RIGHT:return n.moveRight(i);case e.LEFT:return n.moveLeft(i);default:return!0}})),this.treeWidget=t,t.options.keyboardSupport&&jQuery(document).on("keydown.jqtree",this.handleKeyDown)}return s(e,[{key:"deinit",value:function(){jQuery(document).off("keydown.jqtree")}},{key:"moveDown",value:function(e){return this.selectNode(e.getNextNode())}},{key:"moveUp",value:function(e){return this.selectNode(e.getPreviousNode())}},{key:"moveRight",value:function(e){return!e.isFolder()||(e.is_open?this.selectNode(e.getNextNode()):(this.treeWidget.openNode(e),!1))}},{key:"moveLeft",value:function(e){return e.isFolder()&&e.is_open?(this.treeWidget.closeNode(e),!1):this.selectNode(e.getParent())}},{key:"selectNode",value:function(e){return!e||(this.treeWidget.selectNode(e),this.treeWidget.scrollHandler.isScrolledIntoView(jQuery(e.element).find(".jqtree-element"))||this.treeWidget.scrollToNode(e),!1)}},{key:"canHandleKeyboard",value:function(){return!!this.treeWidget.options.keyboardSupport&&this.treeWidget.selectNodeHandler.isFocusOnTree()}}]),e}();a(w,"LEFT",37),a(w,"UP",38),a(w,"RIGHT",39),a(w,"DOWN",40);var F=function(e,t){var n=function(){return"simple_widget_".concat(t)},r=function(e,t){var n=jQuery.data(e,t);return n&&n instanceof W?n:null},o=function(t,i){var o,s=n(),a=y(t.get());try{for(a.s();!(o=a.n()).done;){var l=o.value;if(!r(l,s)){var d=new e(l,i);jQuery.data(l,s)||jQuery.data(l,s,d),d.init()}}}catch(e){a.e(e)}finally{a.f()}return t},s=function(e){var t,i=n(),o=y(e.get());try{for(o.s();!(t=o.n()).done;){var s=t.value,a=r(s,i);a&&a.destroy(),jQuery.removeData(s,i)}}catch(e){o.e(e)}finally{o.f()}},a=function(e,t,i){var r,o=null,s=y(e.get());try{for(s.s();!(r=s.n()).done;){var a=r.value,l=jQuery.data(a,n());if(l&&l instanceof W){var d=l[t];d&&"function"==typeof d&&(o=d.apply(l,i))}}}catch(e){s.e(e)}finally{s.f()}return o};jQuery.fn[t]=function(t){if(!t)return o(this,null);if("object"===i(t)){var n=t;return o(this,n)}if("string"==typeof t&&"_"!==t[0]){var r=t;if("destroy"===r)return s(this);if("get_widget_class"===r)return e;for(var l=arguments.length,d=new Array(l>1?l-1:0),u=1;u1)){var n=t.changedTouches[0];e.handleMouseDown(A(n,t))}})),a(h(e),"touchMove",(function(t){if(t&&!(t.touches.length>1)){var n=t.changedTouches[0];e.handleMouseMove(t,A(n,t))}})),a(h(e),"touchEnd",(function(t){if(t&&!(t.touches.length>1)){var n=t.changedTouches[0];e.handleMouseUp(A(n,t))}})),e}return s(n,[{key:"init",value:function(){var e=this.$el.get(0);e.addEventListener("mousedown",this.mouseDown,{passive:!1}),e.addEventListener("touchstart",this.touchStart,{passive:!1}),this.isMouseStarted=!1,this.mouseDelayTimer=null,this.isMouseDelayMet=!1,this.mouseDownInfo=null}},{key:"deinit",value:function(){var e=this.$el.get(0);e.removeEventListener("mousedown",this.mouseDown,{passive:!1}),e.removeEventListener("touchstart",this.touchStart,{passive:!1}),this.removeMouseMoveEventListeners()}},{key:"handleMouseDown",value:function(e){return this.isMouseStarted&&this.handleMouseUp(e),this.mouseDownInfo=e,!!this.mouseCapture(e)&&(this.handleStartMouse(),!0)}},{key:"handleStartMouse",value:function(){document.addEventListener("mousemove",this.mouseMove,{passive:!1}),document.addEventListener("touchmove",this.touchMove,{passive:!1}),document.addEventListener("mouseup",this.mouseUp,{passive:!1}),document.addEventListener("touchend",this.touchEnd,{passive:!1});var e=this.getMouseDelay();e?this.startMouseDelayTimer(e):this.isMouseDelayMet=!0}},{key:"startMouseDelayTimer",value:function(e){var t=this;this.mouseDelayTimer&&clearTimeout(this.mouseDelayTimer),this.mouseDelayTimer=window.setTimeout((function(){t.mouseDownInfo&&(t.isMouseDelayMet=!0)}),e),this.isMouseDelayMet=!1}},{key:"handleMouseMove",value:function(e,t){if(this.isMouseStarted)return this.mouseDrag(t),void(e.cancelable&&e.preventDefault());this.isMouseDelayMet&&(this.mouseDownInfo&&(this.isMouseStarted=!1!==this.mouseStart(this.mouseDownInfo)),this.isMouseStarted?(this.mouseDrag(t),e.cancelable&&e.preventDefault()):this.handleMouseUp(t))}},{key:"handleMouseUp",value:function(e){this.removeMouseMoveEventListeners(),this.isMouseDelayMet=!1,this.mouseDownInfo=null,this.isMouseStarted&&(this.isMouseStarted=!1,this.mouseStop(e))}},{key:"removeMouseMoveEventListeners",value:function(){document.removeEventListener("mousemove",this.mouseMove,{passive:!1}),document.removeEventListener("touchmove",this.touchMove,{passive:!1}),document.removeEventListener("mouseup",this.mouseUp,{passive:!1}),document.removeEventListener("touchend",this.touchEnd,{passive:!1})}}]),n}(W),H=function(){function e(t){r(this,e),a(this,"treeWidget",void 0),a(this,"_supportsLocalStorage",void 0),this.treeWidget=t}return s(e,[{key:"saveState",value:function(){var e=JSON.stringify(this.getState());this.treeWidget.options.onSetStateFromStorage?this.treeWidget.options.onSetStateFromStorage(e):this.supportsLocalStorage()&&localStorage.setItem(this.getKeyName(),e)}},{key:"getStateFromStorage",value:function(){var e=this.loadFromStorage();return e?this.parseState(e):null}},{key:"getState",value:function(){var e,t,n=this;return{open_nodes:(t=[],n.treeWidget.tree.iterate((function(e){return e.is_open&&e.id&&e.hasChildren()&&t.push(e.id),!0})),t),selected_node:(e=[],n.treeWidget.getSelectedNodes().forEach((function(t){null!=t.id&&e.push(t.id)})),e)}}},{key:"setInitialState",value:function(e){if(e){var t=!1;return e.open_nodes&&(t=this.openInitialNodes(e.open_nodes)),e.selected_node&&(this.resetSelection(),this.selectInitialNodes(e.selected_node)),t}return!1}},{key:"setInitialStateOnDemand",value:function(e,t){e?this.doSetInitialStateOnDemand(e.open_nodes,e.selected_node,t):t()}},{key:"getNodeIdToBeSelected",value:function(){var e=this.getStateFromStorage();return e&&e.selected_node?e.selected_node[0]:null}},{key:"parseState",value:function(e){var t,n=JSON.parse(e);return n&&n.selected_node&&("number"==typeof(t=n.selected_node)&&t%1==0)&&(n.selected_node=[n.selected_node]),n}},{key:"loadFromStorage",value:function(){return this.treeWidget.options.onGetStateFromStorage?this.treeWidget.options.onGetStateFromStorage():this.supportsLocalStorage()?localStorage.getItem(this.getKeyName()):null}},{key:"openInitialNodes",value:function(e){var t,n=!1,i=y(e);try{for(i.s();!(t=i.n()).done;){var r=t.value,o=this.treeWidget.getNodeById(r);o&&(o.load_on_demand?n=!0:o.is_open=!0)}}catch(e){i.e(e)}finally{i.f()}return n}},{key:"selectInitialNodes",value:function(e){var t,n=0,i=y(e);try{for(i.s();!(t=i.n()).done;){var r=t.value,o=this.treeWidget.getNodeById(r);o&&(n+=1,this.treeWidget.selectNodeHandler.addToSelection(o))}}catch(e){i.e(e)}finally{i.f()}return 0!==n}},{key:"resetSelection",value:function(){var e=this.treeWidget.selectNodeHandler;e.getSelectedNodes().forEach((function(t){e.removeFromSelection(t)}))}},{key:"doSetInitialStateOnDemand",value:function(e,t,n){var i=this,r=0,o=e,s=function(){var e,s=[],l=y(o);try{for(l.s();!(e=l.n()).done;){var d=e.value,u=i.treeWidget.getNodeById(d);u?u.is_loading||(u.load_on_demand?a(u):i.treeWidget._openNode(u,!1,null)):s.push(d)}}catch(e){l.e(e)}finally{l.f()}o=s,i.selectInitialNodes(t)&&i.treeWidget._refreshElements(null),0===r&&n()},a=function(e){r+=1,i.treeWidget._openNode(e,!1,(function(){r-=1,s()}))};s()}},{key:"getKeyName",value:function(){return"string"==typeof this.treeWidget.options.saveState?this.treeWidget.options.saveState:"tree"}},{key:"supportsLocalStorage",value:function(){return null==this._supportsLocalStorage&&(this._supportsLocalStorage=function(){if(null==localStorage)return!1;try{var e="_storage_test";sessionStorage.setItem(e,"value"),sessionStorage.removeItem(e)}catch(e){return!1}return!0}()),this._supportsLocalStorage}}]),e}(),P=function(){function e(t){r(this,e),a(this,"treeWidget",void 0),a(this,"previousTop",void 0),a(this,"isInitialized",void 0),a(this,"$scrollParent",void 0),a(this,"scrollParentTop",void 0),this.treeWidget=t,this.previousTop=-1,this.isInitialized=!1}return s(e,[{key:"checkScrolling",value:function(){this.ensureInit(),this.checkVerticalScrolling(),this.checkHorizontalScrolling()}},{key:"scrollToY",value:function(e){if(this.ensureInit(),this.$scrollParent)this.$scrollParent[0].scrollTop=e;else{var t=this.treeWidget.$el.offset(),n=t?t.top:0;jQuery(document).scrollTop(e+n)}}},{key:"isScrolledIntoView",value:function(e){var t,n,i,r;this.ensureInit();var o=e.height()||0;if(this.$scrollParent){r=0,n=this.$scrollParent.height()||0;var s=e.offset();t=(i=(s?s.top:0)-this.scrollParentTop)+o}else{n=(r=jQuery(window).scrollTop()||0)+(jQuery(window).height()||0);var a=e.offset();t=(i=a?a.top:0)+o}return t<=n&&i>=r}},{key:"getScrollLeft",value:function(){return this.$scrollParent&&this.$scrollParent.scrollLeft()||0}},{key:"initScrollParent",value:function(){var e=this,t=function(){e.scrollParentTop=0,e.$scrollParent=null};"fixed"===this.treeWidget.$el.css("position")&&t();var n=function(){var t=["overflow","overflow-y"],n=function(e){var n,i=y(t);try{for(i.s();!(n=i.n()).done;){var r=n.value,o=e.css(r);if("auto"===o||"scroll"===o)return!0}}catch(e){i.e(e)}finally{i.f()}return!1};if(n(e.treeWidget.$el))return e.treeWidget.$el;var i,r=y(e.treeWidget.$el.parents().get());try{for(r.s();!(i=r.n()).done;){var o=i.value,s=jQuery(o);if(n(s))return s}}catch(e){r.e(e)}finally{r.f()}return null}();if(n&&n.length&&"HTML"!==n[0].tagName){this.$scrollParent=n;var i=this.$scrollParent.offset();this.scrollParentTop=i?i.top:0}else t();this.isInitialized=!0}},{key:"ensureInit",value:function(){this.isInitialized||this.initScrollParent()}},{key:"handleVerticalScrollingWithScrollParent",value:function(e){var t=this.$scrollParent&&this.$scrollParent[0];t&&(this.scrollParentTop+t.offsetHeight-e.bottom<20?(t.scrollTop+=20,this.treeWidget.refreshHitAreas(),this.previousTop=-1):e.top-this.scrollParentTop<20&&(t.scrollTop-=20,this.treeWidget.refreshHitAreas(),this.previousTop=-1))}},{key:"handleVerticalScrollingWithDocument",value:function(e){var t=jQuery(document).scrollTop()||0;e.top-t<20?jQuery(document).scrollTop(t-20):(jQuery(window).height()||0)-(e.bottom-t)<20&&jQuery(document).scrollTop(t+20)}},{key:"checkVerticalScrolling",value:function(){var e=this.treeWidget.dndHandler.hoveredArea;e&&e.top!==this.previousTop&&(this.previousTop=e.top,this.$scrollParent?this.handleVerticalScrollingWithScrollParent(e):this.handleVerticalScrollingWithDocument(e))}},{key:"checkHorizontalScrolling",value:function(){var e=this.treeWidget.dndHandler.positionInfo;e&&(this.$scrollParent?this.handleHorizontalScrollingWithParent(e):this.handleHorizontalScrollingWithDocument(e))}},{key:"handleHorizontalScrollingWithParent",value:function(e){if(void 0!==e.pageX&&void 0!==e.pageY){var t=this.$scrollParent,n=t&&t.offset();if(t&&n){var i=t[0],r=i.scrollLeft+i.clientWidth0,s=n.left+i.clientWidth,a=n.left,l=e.pageX>s-20,d=e.pageX0,o=e.pageX>i-20,s=e.pageX-n<20;o?t.scrollLeft(n+20):s&&r&&t.scrollLeft(Math.max(n-20,0))}}}]),e}(),M=function(){function e(t){r(this,e),a(this,"treeWidget",void 0),a(this,"selectedNodes",void 0),a(this,"selectedSingleNode",void 0),this.treeWidget=t,this.selectedNodes=new Set,this.clear()}return s(e,[{key:"getSelectedNode",value:function(){var e=this.getSelectedNodes();return!!e.length&&e[0]}},{key:"getSelectedNodes",value:function(){var e=this;if(this.selectedSingleNode)return[this.selectedSingleNode];var t=[];return this.selectedNodes.forEach((function(n){var i=e.treeWidget.getNodeById(n);i&&t.push(i)})),t}},{key:"getSelectedNodesUnder",value:function(e){if(this.selectedSingleNode)return e.isParentOf(this.selectedSingleNode)?[this.selectedSingleNode]:[];var t=[];for(var n in this.selectedNodes)if(Object.prototype.hasOwnProperty.call(this.selectedNodes,n)){var i=this.treeWidget.getNodeById(n);i&&e.isParentOf(i)&&t.push(i)}return t}},{key:"isNodeSelected",value:function(e){return null!=e.id?this.selectedNodes.has(e.id):!!this.selectedSingleNode&&this.selectedSingleNode.element===e.element}},{key:"clear",value:function(){this.selectedNodes.clear(),this.selectedSingleNode=null}},{key:"removeFromSelection",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];null==e.id?this.selectedSingleNode&&e.element===this.selectedSingleNode.element&&(this.selectedSingleNode=null):(this.selectedNodes.delete(e.id),n&&e.iterate((function(){return null!=e.id&&t.selectedNodes.delete(e.id),!0})))}},{key:"addToSelection",value:function(e){null!=e.id?this.selectedNodes.add(e.id):this.selectedSingleNode=e}},{key:"isFocusOnTree",value:function(){var e=document.activeElement;return Boolean(e&&"SPAN"===e.tagName&&this.treeWidget._containsElement(e))}}]),e}(),O=function(){function e(t,n){r(this,e),a(this,"node",void 0),a(this,"$element",void 0),a(this,"treeWidget",void 0),this.init(t,n)}return s(e,[{key:"init",value:function(e,t){this.node=e,this.treeWidget=t,e.element||(e.element=this.treeWidget.element.get(0)),this.$element=jQuery(e.element)}},{key:"addDropHint",value:function(e){return this.mustShowBorderDropHint(e)?new x(this.$element,this.treeWidget._getScrollLeft()):new q(this.node,this.$element,e)}},{key:"select",value:function(e){var t,n=this.getLi();n.addClass("jqtree-selected"),n.attr("aria-selected","true");var i=this.getSpan();i.attr("tabindex",null!==(t=this.treeWidget.options.tabIndex)&&void 0!==t?t:null),e&&i.trigger("focus")}},{key:"deselect",value:function(){var e=this.getLi();e.removeClass("jqtree-selected"),e.attr("aria-selected","false");var t=this.getSpan();t.removeAttr("tabindex"),t.blur()}},{key:"getUl",value:function(){return this.$element.children("ul:first")}},{key:"getSpan",value:function(){return this.$element.children(".jqtree-element").find("span.jqtree-title")}},{key:"getLi",value:function(){return this.$element}},{key:"mustShowBorderDropHint",value:function(e){return e===k.Inside}}]),e}(),$=function(e){l(n,e);var t=v(n);function n(){return r(this,n),t.apply(this,arguments)}return s(n,[{key:"open",value:function(e){var t=this,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"fast";if(!this.node.is_open){this.node.is_open=!0;var r=this.getButton();r.removeClass("jqtree-closed"),r.html("");var o=r.get(0);if(o){var s=this.treeWidget.renderer.openedIconElement.cloneNode(!0);o.appendChild(s)}var a=function(){t.getLi().removeClass("jqtree-closed"),t.getSpan().attr("aria-expanded","true"),e&&e(t.node),t.treeWidget._triggerEvent("tree.open",{node:t.node})};n?this.getUl().slideDown(i,a):(this.getUl().show(),a())}}},{key:"close",value:function(){var e=this,t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"fast";if(this.node.is_open){this.node.is_open=!1;var i=this.getButton();i.addClass("jqtree-closed"),i.html("");var r=i.get(0);if(r){var o=this.treeWidget.renderer.closedIconElement.cloneNode(!0);r.appendChild(o)}var s=function(){e.getLi().addClass("jqtree-closed"),e.getSpan().attr("aria-expanded","false"),e.treeWidget._triggerEvent("tree.close",{node:e.node})};t?this.getUl().slideUp(n,s):(this.getUl().hide(),s())}}},{key:"mustShowBorderDropHint",value:function(e){return!this.node.is_open&&e===k.Inside}},{key:"getButton",value:function(){return this.$element.children(".jqtree-element").find("a.jqtree-toggler")}}]),n}(O),x=function(){function e(t,n){r(this,e),a(this,"$hint",void 0);var i=t.children(".jqtree-element"),o=t.width()||0,s=Math.max(o+n-4,0),l=i.outerHeight()||0,d=Math.max(l-4,0);this.$hint=jQuery(''),i.append(this.$hint),this.$hint.css({width:s,height:d})}return s(e,[{key:"remove",value:function(){this.$hint.remove()}}]),e}(),q=function(){function e(t,n,i){r(this,e),a(this,"$element",void 0),a(this,"node",void 0),a(this,"$ghost",void 0),this.$element=n,this.node=t,this.$ghost=jQuery('
  • \n
  • '),i===k.After?this.moveAfter():i===k.Before?this.moveBefore():i===k.Inside&&(t.isFolder()&&t.is_open?this.moveInsideOpenFolder():this.moveInside())}return s(e,[{key:"remove",value:function(){this.$ghost.remove()}},{key:"moveAfter",value:function(){this.$element.after(this.$ghost)}},{key:"moveBefore",value:function(){this.$element.before(this.$ghost)}},{key:"moveInsideOpenFolder",value:function(){jQuery(this.node.children[0].element).before(this.$ghost)}},{key:"moveInside",value:function(){this.$element.after(this.$ghost),this.$ghost.addClass("jqtree-inside")}}]),e}(),B="Node parameter is empty",Q="Parameter is empty: ",U=function(e){l(o,e);var t=v(o);function o(){var e;r(this,o);for(var n=arguments.length,i=new Array(n),s=0;s1&&void 0!==arguments[1]?arguments[1]:null;if(!e)throw Error(B);var n=null!=t?t:this.options.slide;return e.is_open?this.closeNode(e,n):this.openNode(e,n),this.element}},{key:"getTree",value:function(){return this.tree}},{key:"selectNode",value:function(e,t){return this.doSelectNode(e,t),this.element}},{key:"getSelectedNode",value:function(){return this.selectNodeHandler.getSelectedNode()}},{key:"toJson",value:function(){return JSON.stringify(this.tree.getData())}},{key:"loadData",value:function(e,t){return this.doLoadData(e,t),this.element}},{key:"loadDataFromUrl",value:function(e,t,n){return"string"==typeof e?this.doLoadDataFromUrl(e,t,null!=n?n:null):this.doLoadDataFromUrl(null,e,t),this.element}},{key:"reload",value:function(e){return this.doLoadDataFromUrl(null,null,e),this.element}},{key:"refresh",value:function(){return this._refreshElements(null),this.element}},{key:"getNodeById",value:function(e){return this.tree.getNodeById(e)}},{key:"getNodeByName",value:function(e){return this.tree.getNodeByName(e)}},{key:"getNodeByNameMustExist",value:function(e){return this.tree.getNodeByNameMustExist(e)}},{key:"getNodesByProperty",value:function(e,t){return this.tree.getNodesByProperty(e,t)}},{key:"getNodeByHtmlElement",value:function(e){return this.getNode(jQuery(e))}},{key:"getNodeByCallback",value:function(e){return this.tree.getNodeByCallback(e)}},{key:"openNode",value:function(e,t,n){var i=this;if(!e)throw Error(B);var r=function(){var e,r,o;("function"==typeof t?(e=t,r=null):(r=t,e=n),null==r)&&(r=null!==(o=i.options.slide)&&void 0!==o&&o);return[r,e]}(),o=p(r,2),s=o[0],a=o[1];return this._openNode(e,s,a),this.element}},{key:"closeNode",value:function(e,t){if(!e)throw Error(B);var n=null!=t?t:this.options.slide;return(e.isFolder()||e.isEmptyFolder)&&(new $(e,this).close(n,this.options.animationSpeed),this.saveState()),this.element}},{key:"isDragging",value:function(){return this.dndHandler.isDragging}},{key:"refreshHitAreas",value:function(){return this.dndHandler.refresh(),this.element}},{key:"addNodeAfter",value:function(e,t){var n=t.addAfter(e);return n&&this._refreshElements(t.parent),n}},{key:"addNodeBefore",value:function(e,t){if(!t)throw Error(Q+"existingNode");var n=t.addBefore(e);return n&&this._refreshElements(t.parent),n}},{key:"addParentNode",value:function(e,t){if(!t)throw Error(Q+"existingNode");var n=t.addParent(e);return n&&this._refreshElements(n.parent),n}},{key:"removeNode",value:function(e){if(!e)throw Error(B);if(!e.parent)throw Error("Node has no parent");this.selectNodeHandler.removeFromSelection(e,!0);var t=e.parent;return e.remove(),this._refreshElements(t),this.element}},{key:"appendNode",value:function(e,t){var n=t||this.tree,i=n.append(e);return this._refreshElements(n),i}},{key:"prependNode",value:function(e,t){var n=null!=t?t:this.tree,i=n.prepend(e);return this._refreshElements(n),i}},{key:"updateNode",value:function(e,t){if(!e)throw Error(B);var n="object"===i(t)&&t.id&&t.id!==e.id;n&&this.tree.removeNodeFromIndex(e),e.setData(t),n&&this.tree.addNodeToIndex(e),"object"===i(t)&&t.children&&t.children instanceof Array&&(e.removeChildren(),t.children.length&&e.loadFromData(t.children));var r=this.selectNodeHandler.isFocusOnTree(),o=this.isSelectedNodeInSubtree(e);return this._refreshElements(e),o&&this.selectCurrentNode(r),this.element}},{key:"isSelectedNodeInSubtree",value:function(e){var t=this.getSelectedNode();return!!t&&(e===t||e.isParentOf(t))}},{key:"moveNode",value:function(e,t,n){if(!e)throw Error(B);if(!t)throw Error(Q+"targetNode");var i=N[n];return void 0!==i&&(this.tree.moveNode(e,t,i),this._refreshElements(null)),this.element}},{key:"getStateFromStorage",value:function(){return this.saveStateHandler.getStateFromStorage()}},{key:"addToSelection",value:function(e,t){if(!e)throw Error(B);return this.selectNodeHandler.addToSelection(e),this._getNodeElementForNode(e).select(void 0===t||t),this.saveState(),this.element}},{key:"getSelectedNodes",value:function(){return this.selectNodeHandler.getSelectedNodes()}},{key:"isNodeSelected",value:function(e){if(!e)throw Error(B);return this.selectNodeHandler.isNodeSelected(e)}},{key:"removeFromSelection",value:function(e){if(!e)throw Error(B);return this.selectNodeHandler.removeFromSelection(e),this._getNodeElementForNode(e).deselect(),this.saveState(),this.element}},{key:"scrollToNode",value:function(e){if(!e)throw Error(B);var t=jQuery(e.element).offset(),n=t?t.top:0,i=this.$el.offset(),r=n-(i?i.top:0);return this.scrollHandler.scrollToY(r),this.element}},{key:"getState",value:function(){return this.saveStateHandler.getState()}},{key:"setState",value:function(e){return this.saveStateHandler.setInitialState(e),this._refreshElements(null),this.element}},{key:"setOption",value:function(e,t){return this.options[e]=t,this.element}},{key:"moveDown",value:function(){var e=this.getSelectedNode();return e&&this.keyHandler.moveDown(e),this.element}},{key:"moveUp",value:function(){var e=this.getSelectedNode();return e&&this.keyHandler.moveUp(e),this.element}},{key:"getVersion",value:function(){return"1.6.1"}},{key:"_triggerEvent",value:function(e,t){var n=jQuery.Event(e,t);return this.element.trigger(n),n}},{key:"_openNode",value:function(e){var t=this,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2?arguments[2]:void 0,r=function(e,n,i){new $(e,t).open(i,n,t.options.animationSpeed)};if(e.isFolder()||e.isEmptyFolder)if(e.load_on_demand)this.loadFolderOnDemand(e,n,i);else{for(var o=e.parent;o;)o.parent&&r(o,!1,null),o=o.parent;r(e,n,i),this.saveState()}}},{key:"_refreshElements",value:function(e){this.renderer.render(e),this._triggerEvent("tree.refresh")}},{key:"_getNodeElementForNode",value:function(e){return e.isFolder()?new $(e,this):new O(e,this)}},{key:"_getNodeElement",value:function(e){var t=this.getNode(e);return t?this._getNodeElementForNode(t):null}},{key:"_containsElement",value:function(e){var t=this.getNode(jQuery(e));return null!=t&&t.tree===this.tree}},{key:"_getScrollLeft",value:function(){return this.scrollHandler.getScrollLeft()}},{key:"init",value:function(){f(d(o.prototype),"init",this).call(this),this.element=this.$el,this.isInitialized=!1,this.options.rtl=this.getRtlOption(),null==this.options.closedIcon&&(this.options.closedIcon=this.getDefaultClosedIcon()),this.renderer=new E(this),this.dataLoader=new C(this),this.saveStateHandler=new H(this),this.selectNodeHandler=new M(this),this.dndHandler=new D(this),this.scrollHandler=new P(this),this.keyHandler=new w(this),this.initData(),this.element.on("click",this.handleClick),this.element.on("dblclick",this.handleDblclick),this.options.useContextMenu&&this.element.on("contextmenu",this.handleContextmenu)}},{key:"deinit",value:function(){this.element.empty(),this.element.off(),this.keyHandler.deinit(),this.tree=new _({},!0),f(d(o.prototype),"deinit",this).call(this)}},{key:"mouseCapture",value:function(e){return!!this.options.dragAndDrop&&this.dndHandler.mouseCapture(e)}},{key:"mouseStart",value:function(e){return!!this.options.dragAndDrop&&this.dndHandler.mouseStart(e)}},{key:"mouseDrag",value:function(e){if(this.options.dragAndDrop){var t=this.dndHandler.mouseDrag(e);return this.scrollHandler.checkScrolling(),t}return!1}},{key:"mouseStop",value:function(e){return!!this.options.dragAndDrop&&this.dndHandler.mouseStop(e)}},{key:"getMouseDelay",value:function(){var e;return null!==(e=this.options.startDndDelay)&&void 0!==e?e:0}},{key:"initData",value:function(){this.options.data?this.doLoadData(this.options.data,null):this.getDataUrlInfo(null)?this.doLoadDataFromUrl(null,null,null):this.doLoadData([],null)}},{key:"getDataUrlInfo",value:function(e){var t,n=this,r=this.options.dataUrl||this.element.data("url"),o=function(t){if(null!=e&&e.id){var i={node:e.id};t.data=i}else{var r=n.getNodeIdToBeSelected();if(r){var o={selected_node:r};t.data=o}}};return"function"==typeof r?r(e):"string"==typeof r?(o(t={url:r}),t):r&&"object"===i(r)?(o(r),r):null}},{key:"getNodeIdToBeSelected",value:function(){return this.options.saveState?this.saveStateHandler.getNodeIdToBeSelected():null}},{key:"initTree",value:function(e){var t=this,n=function(){t.isInitialized||(t.isInitialized=!0,t._triggerEvent("tree.init"))};if(this.options.nodeClass){this.tree=new this.options.nodeClass(null,!0,this.options.nodeClass),this.selectNodeHandler.clear(),this.tree.loadFromData(e);var i=this.setInitialState();this._refreshElements(null),i?this.setInitialStateOnDemand(n):n()}}},{key:"setInitialState",value:function(){var e=this,t=p(function(){if(e.options.saveState){var t=e.saveStateHandler.getStateFromStorage();return t?[!0,e.saveStateHandler.setInitialState(t)]:[!1,!1]}return[!1,!1]}(),2),n=t[0],i=t[1];return n||(i=function(){if(!1===e.options.autoOpen)return!1;var t=e.getAutoOpenMaxLevel(),n=!1;return e.tree.iterate((function(e,i){return e.load_on_demand?(n=!0,!1):!!e.hasChildren()&&(e.is_open=!0,i!==t)})),n}()),i}},{key:"setInitialStateOnDemand",value:function(e){var t,n,i,r,o=this;(function(){if(o.options.saveState){var t=o.saveStateHandler.getStateFromStorage();return!!t&&(o.saveStateHandler.setInitialStateOnDemand(t,e),!0)}return!1})()||(t=o.getAutoOpenMaxLevel(),n=0,i=function(e){n+=1,o._openNode(e,!1,(function(){n-=1,r()}))},(r=function(){o.tree.iterate((function(e,n){return e.load_on_demand?(e.is_loading||i(e),!1):(o._openNode(e,!1,null),n!==t)})),0===n&&e()})())}},{key:"getAutoOpenMaxLevel",value:function(){return!0===this.options.autoOpen?-1:"number"==typeof this.options.autoOpen?this.options.autoOpen:"string"==typeof this.options.autoOpen?parseInt(this.options.autoOpen,10):0}},{key:"getClickTarget",value:function(e){var t=jQuery(e),n=t.closest(".jqtree-toggler");if(n.length){var i=this.getNode(n);if(i)return{type:"button",node:i}}else{var r=t.closest(".jqtree-element");if(r.length){var o=this.getNode(r);if(o)return{type:"label",node:o}}}return null}},{key:"getNode",value:function(e){var t=e.closest("li.jqtree_common");return 0===t.length?null:t.data("node")}},{key:"saveState",value:function(){this.options.saveState&&this.saveStateHandler.saveState()}},{key:"selectCurrentNode",value:function(e){var t=this.getSelectedNode();if(t){var n=this._getNodeElementForNode(t);n&&n.select(e)}}},{key:"deselectCurrentNode",value:function(){var e=this.getSelectedNode();e&&this.removeFromSelection(e)}},{key:"getDefaultClosedIcon",value:function(){return this.options.rtl?"◀":"►"}},{key:"getRtlOption",value:function(){if(null!=this.options.rtl)return this.options.rtl;var e=this.element.data("rtl");return null!==e&&!1!==e&&void 0!==e}},{key:"doSelectNode",value:function(e,t){var i=this,r=function(){i.options.saveState&&i.saveStateHandler.saveState()};if(!e)return this.deselectCurrentNode(),void r();var o=n(n({},{mustSetFocus:!0,mustToggle:!0}),t||{});if(i.options.onCanSelectNode?!0===i.options.selectable&&i.options.onCanSelectNode(e):!0===i.options.selectable){if(this.selectNodeHandler.isNodeSelected(e))o.mustToggle&&(this.deselectCurrentNode(),this._triggerEvent("tree.select",{node:null,previous_node:e}));else{var s=this.getSelectedNode()||null;this.deselectCurrentNode(),this.addToSelection(e,o.mustSetFocus),this._triggerEvent("tree.select",{node:e,deselected_node:s}),(a=e.parent)&&a.parent&&!a.is_open&&i.openNode(a,!1)}var a;r()}}},{key:"doLoadData",value:function(e,t){e&&(this._triggerEvent("tree.load_data",{tree_data:e}),t?(this.deselectNodes(t),this.loadSubtree(e,t)):this.initTree(e),this.isDragging()&&this.dndHandler.refresh())}},{key:"deselectNodes",value:function(e){var t,n=y(this.selectNodeHandler.getSelectedNodesUnder(e));try{for(n.s();!(t=n.n()).done;){var i=t.value;this.selectNodeHandler.removeFromSelection(i)}}catch(e){n.e(e)}finally{n.f()}}},{key:"loadSubtree",value:function(e,t){t.loadFromData(e),t.load_on_demand=!1,t.is_loading=!1,this._refreshElements(t)}},{key:"doLoadDataFromUrl",value:function(e,t,n){var i=e||this.getDataUrlInfo(t);this.dataLoader.loadFromUrl(i,t,n)}},{key:"loadFolderOnDemand",value:function(e){var t=this,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],i=arguments.length>2?arguments[2]:void 0;e.is_loading=!0,this.doLoadDataFromUrl(null,e,(function(){t._openNode(e,n,i)}))}}]),o}(L);return a(U,"defaults",{animationSpeed:"fast",autoEscape:!0,autoOpen:!1,buttonLeft:!0,closedIcon:void 0,data:void 0,dataFilter:void 0,dataUrl:void 0,dragAndDrop:!1,keyboardSupport:!0,nodeClass:_,onCanMove:void 0,onCanMoveTo:void 0,onCanSelectNode:void 0,onCreateLi:void 0,onDragMove:void 0,onDragStop:void 0,onGetStateFromStorage:void 0,onIsMoveHandle:void 0,onLoadFailed:void 0,onLoading:void 0,onSetStateFromStorage:void 0,openedIcon:"▼",openFolderDelay:500,rtl:void 0,saveState:!1,selectable:!0,showEmptyFolder:!1,slide:!0,startDndDelay:300,tabIndex:0,useContextMenu:!0}),W.register(U,"tree"),e.JqTreeWidget=U,Object.defineProperty(e,"__esModule",{value:!0}),e}({}); +//# sourceMappingURL=tree.jquery.js.map diff --git a/src/vendor/assets/javascripts/toOxml.js b/src/vendor/assets/javascripts/toOxml.js index 8bedbdf..a458103 100644 --- a/src/vendor/assets/javascripts/toOxml.js +++ b/src/vendor/assets/javascripts/toOxml.js @@ -158,7 +158,8 @@ function toOxml( data, opts ) { if( typeof(opts.showEmptyProperties)!='boolean' ) opts.showEmptyProperties = false; if( typeof(opts.hasContent)!='function' ) opts.hasContent = hasContent; if( typeof(opts.lookup)!='function' ) opts.lookup = function(str) { return str }; - if( !opts.titleProperties ) opts.titleProperties = ['dcterms:title']; + if (!opts.titleLinkTargets) opts.titleLinkTargets = ['FMC:Actor','FMC:State','FMC:Event','SpecIF:Collection','SpecIF:Diagram','FMC:Plan']; + if( !opts.titleProperties ) opts.titleProperties = ['dcterms:title']; if( !opts.typeProperty ) opts.typeProperty = 'dcterms:type'; if( !opts.descriptionProperties ) opts.descriptionProperties = ['dcterms:description','SpecIF:Diagram']; if( !opts.stereotypeProperties ) opts.stereotypeProperties = ['UML:Stereotype']; @@ -197,7 +198,7 @@ function toOxml( data, opts ) { // A single comprehensive or tag pair ... // Limitation: the innerHTML may not have any tags. // The [^<] assures that just the single object is matched. With [\\s\\S] also nested objects match for some reason. - const reSO = ']+)(/>|>([^<]*?))', + const reSO = ']+?)(/>|>([^<]*?)
    )', reSingleObject = new RegExp( reSO, '' ); /* // Two nested objects, where the inner is a comprehensive or a tag pair ..: // .. but nothing useful can be done in a WORD file with the outer object ( for details see below in splitRuns() ). @@ -206,7 +207,7 @@ function toOxml( data, opts ) { // Regex to isolate text runs constituting a paragraph: const reR = '([\\s\\S]*?)(' - + '||||||||]*>|' + + '||||||||]*?>|' + '|'+reA + '|'+reI /* // The nested object pattern must be checked before the single object pattern: @@ -246,9 +247,10 @@ function toOxml( data, opts ) { // Remove all formatting for the title, as the app's format shall prevail. // Before, remove all marked deletions (as prepared be diffmatchpatch). ti = stripHtml( itm.properties[a].value ); - } else { + } + else { // In certain cases (SpecIF hierarchy root or comment), there is no title property. - ti = elTitleOf(itm); + ti = staTitleOf(itm); }; ti = minEscape( opts.lookup( ti ) ); if( !ti ) return ''; // no paragraph, if title is empty @@ -260,7 +262,8 @@ function toOxml( data, opts ) { // console.debug('titleOf',itm,ic,ti); - if( !pars || typeof(pars.level)!='number' ) return ic+ti; // return raw text + if (!pars || typeof (pars.level) != 'number') + return ic + ti; // return raw text if( pars.level==0 ) return wParagraph( {text: ic+ti, format:{ title:true }} ); @@ -451,27 +454,22 @@ function toOxml( data, opts ) { function anchorOf( resId ) { // Find the hierarchy node id for a given resource; // the first occurrence is returned: - let m, M, n, N, ndId=null; - for( m=0, M=data.hierarchies.length; m not found function ndByRef( nd ) { if( nd.resource==resId ) return nd.id; - let ndId=null; + let t,T,ndId; if( nd.nodes ) - for( var t=0, T=nd.nodes.length; t - cs.push( {p:{text:($1.trim()||nbsp), format:{font:{weight:'bold'}}}, border:{style:'single'}} ); + cs.push({ p: { text: ($1? ($1.trim() || nbsp) : nbsp ), format:{font:{weight:'bold'}}}, border:{style:'single'}} ); // ToDo: Somehow the text is not printed boldly ... return ''; }); @@ -683,7 +681,7 @@ function toOxml( data, opts ) { // console.debug('td',$0,'|',$1); // the 'td' cell with it's content // $1 is undefined in case of - cs.push( {p:{text:($1.trim()||nbsp)}, border:{style:'single'}} ) + cs.push({ p: { text: ($1? ($1.trim() || nbsp) : nbsp )}, border:{style:'single'}} ) return '' }); // the row with it's content: @@ -771,13 +769,13 @@ function toOxml( data, opts ) { }; // Set the color of the next text span; // Limitation: Only numeric color codes are recognized, so far: - let sp = /]+color: ?#([0-9a-fA-F]{6})[^>]*>/.exec($2); + let sp = /]+?"color: ?#([0-9a-fA-F]{6})"[^>]*>/.exec($2); if( sp && sp.length>1 ) { fmt.font.color = sp[1].toUpperCase(); return ''; }; if( /<\/span>/.test($2) ) { - delete fmt.font.color; // simply, since there is only one value so far. + delete fmt.font.color; return ''; }; // ToDo: Transform '' @@ -958,23 +956,25 @@ function toOxml( data, opts ) { if( !opts.addTitleLinks || lk[1].length-1;x-- ) { - cO = data.resources[x]; + cR = data.resources[x]; // avoid self-reflection: - // if(ob.id==cO.id) continue; + // if(ob.id==cR.id) continue; - // get the pure title text: - ti = cO.title; - // ti = minEscape( cO.title ); - // disregard objects whose title is too short: + ti = cR.title; + // ti = minEscape( cR.title ); if( !ti || ti.length'+r+'' + tg = 'w:anchor="' + limit(ct.format.hyperlink.internal) + '"'; + return '' + r + '' // Limitation: Note that OOXML allows that a hyperlink contains multiple 'runs'. We are restricted to a single run. }; @@ -1242,7 +1243,9 @@ function toOxml( data, opts ) { return ''; } function limit(e) { - // it is unique for length<41, so don't change it: + // MS Word truncates internal links to 40 characters resulting in links which are not unique; + // so longer ones are hashed to assure uniqueness. + // Link is unique for length<41, so don't change it: return e.length<41? e : 'h'+hashCode(e) } function pushReferencedUrl( u ) { @@ -2753,7 +2756,7 @@ function toOxml( data, opts ) { }; // return undefined } - function elTitleOf( el ) { + function staTitleOf( el ) { // get the title of a resource or statement as defined by itself or it's class, // where a resource always has a statement of its own, i.e. the second clause never applies: return el.title || itemById(data.statementClasses,el['class']).title diff --git a/src/vendor/assets/javascripts/toXhtml.js b/src/vendor/assets/javascripts/toXhtml.js index 5361cbe..43fa7ce 100644 --- a/src/vendor/assets/javascripts/toXhtml.js +++ b/src/vendor/assets/javascripts/toXhtml.js @@ -34,6 +34,7 @@ function toXhtml( data, opts ) { if( typeof(opts.showEmptyProperties)!='boolean' ) opts.showEmptyProperties = false; if( typeof(opts.hasContent)!='function' ) opts.hasContent = hasContent; if( typeof(opts.lookup)!='function' ) opts.lookup = function(str) { return str }; + if (!opts.titleLinkTargets) opts.titleLinkTargets = ['FMC:Actor', 'FMC:State', 'FMC:Event', 'SpecIF:Collection', 'SpecIF:Diagram', 'FMC:Plan']; if( !opts.titleProperties ) opts.titleProperties = ['dcterms:title']; if( !opts.descriptionProperties ) opts.descriptionProperties = ['dcterms:description','SpecIF:Diagram']; if( !opts.stereotypeProperties ) opts.stereotypeProperties = ['UML:Stereotype']; @@ -116,7 +117,7 @@ function toXhtml( data, opts ) { ti = stripHtml( itm.properties[a].value ); } else { // In certain cases (SpecIF hierarchy root, comment or ReqIF export), there is no title property. - ti = elTitleOf(itm); + ti = staTitleOf(itm); }; ti = escapeXML( opts.lookup( ti ) ); if( !ti ) return ''; @@ -459,7 +460,7 @@ function toXhtml( data, opts ) { str = str.replace( opts.RE.TitleLink, function( $0, $1 ) { // if( $1.length-1;x-- ) { cR = data.resources[x]; @@ -473,6 +474,10 @@ function toXhtml( data, opts ) { // disregard objects whose title is too short: if( !ti || ti.length'+$1+'' }; @@ -573,7 +578,7 @@ function toXhtml( data, opts ) { // get the title of a resource/statement property as defined by itself or it's class: return prp.title || itemBy(data.propertyClasses,'id',prp['class']).title } - function elTitleOf( el ) { + function staTitleOf( el ) { // get the title of a resource or statement as defined by itself or it's class, // where a resource always has a statement of its own, i.e. the second clause never applies: return el.title || itemBy(data.statementClasses,'id',el['class']).title diff --git a/src/view.html b/src/view.html index 643fad2..73ca19d 100644 --- a/src/view.html +++ b/src/view.html @@ -40,7 +40,7 @@ document.body.appendChild(el) } let pend=4; -getScript( 'https://code.jquery.com/jquery-3.5.1.min.js' ); +getScript('https://code.jquery.com/jquery-3.6.0.min.js'); getScript( './config/definitions.js?'+ Date.now().toString() ); // with cache-busting getScript( './config/moduleManager.js?'+ Date.now().toString() ); getScript( './'+fileName+'.js?'+ Date.now().toString() ); diff --git a/src/view.ts b/src/view.ts index 7752a0e..be389eb 100644 --- a/src/view.ts +++ b/src/view.ts @@ -161,9 +161,10 @@ function viewSpecif():IApp { ); }; self.export = function():void { - if( !self.cache.selectedProject || !self.cache.selectedProject.data.id ) - message.show( i18n.MsgNoProjectLoaded, {severity:'warning', duration:CONFIG.messageDisplayTimeShort} ); - self.cache.selectedProject.chooseFormatAndExport(); + if (self.cache.selectedProject && self.cache.selectedProject.isLoaded() ) + self.cache.selectedProject.chooseFormatAndExport(); + else + message.show(i18n.MsgNoProjectLoaded, { severity: 'warning', duration: CONFIG.messageDisplayTimeShort }); }; /* self.updateMe = function() { self.me.beginUpdate();