jsharmony-cms
Version:
1,395 lines (1,184 loc) • 854 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var DataModelTemplate_GridPreview = require('./dataModelTemplate_gridPreview');
var DataModelTemplate_FormPreview = require('./dataModelTemplate_formPreview');
var PropertiesModelTemplate_Form = require('./propertiesModelTemplate_form');
/**
* @typedef {Object} MediaBrowserControlInfo
* @property {string} dataFieldName
* @property {string} titleFieldName
* @property {('link' | 'media')} browserType
*/
/** @typedef {ComponentTemplate} ComponentTemplate */
/**
* @class
* @param {Object} componentConfig - the component configuration as defined by the component JSON.
* @param {Object} jsh
*/
function ComponentTemplate(componentConfig, jsh, cms) {
/** @private @type {Object} */
this._componentConfig = componentConfig;
/** @private @type {Object} */
this._jsh = jsh;
/** @private @type {Object} */
this._cms = cms;
/** @private @type {DataModelTemplate_GridPreview} */
this._dataModelTemplate_GridPreview = undefined;
/** @private @type {DataModelTemplate_FormPreview} */
this._dataModelTemplate_FormPreview = undefined;
/** @private @type {PropertiesModelTemplate_Form} */
this._propertiesModelTemplate_Form = undefined;
if (this._componentConfig.data) {
this._componentConfig.data.fields = this.processBrowserFields(this._componentConfig.data.fields || []);
this._dataModelTemplate_GridPreview = new DataModelTemplate_GridPreview(this, this._componentConfig.data);
this._dataModelTemplate_FormPreview = new DataModelTemplate_FormPreview(this, this._componentConfig.data);
}
if (this._componentConfig.properties) {
this._componentConfig.properties.fields = this.processBrowserFields(this._componentConfig.properties.fields || []);
this._propertiesModelTemplate_Form = new PropertiesModelTemplate_Form(this, this._componentConfig.properties);
}
}
/**
* Get the component captions tuple as defined by the component JSON.
* The first element is the title, the second element is the singular caption
* (if exists), and the third element is the plural caption (if exists).
* @public
* @returns {[string, string, string]}
*/
ComponentTemplate.prototype.getCaptions = function() {
var captions = [this._componentConfig.title];
if (_.isArray(this._componentConfig.caption)) {
captions.push(this._componentConfig.caption[0]);
captions.push(this._componentConfig.caption[1]);
} else {
captions.push(this._componentConfig.caption);
captions.push(this._componentConfig.caption);
}
return captions;
};
/**
* Get the component configuration as defined by the component JSON.
* @public
* @returns {Object}
*/
ComponentTemplate.prototype.getComponentConfig = function() {
return this._componentConfig;
};
/**
* Return the editor type
* @public
* @returns {('grid' | 'grid_preview' | 'form' | undefined)}
*/
ComponentTemplate.prototype.getDataEditorType = function() {
if (this._componentConfig.data) {
return this._componentConfig.data.layout;
}
return undefined;
};
/**
* @public
* @returns {(DataModelTemplate_FormPreview | undefined)}
*/
ComponentTemplate.prototype.getDataModelTemplate_FormPreview = function() {
return this._dataModelTemplate_FormPreview;
};
/**
* @public
* @returns {(DataModelTemplate_GridPreview | undefined)}
*/
ComponentTemplate.prototype.getDataModelTemplate_GridPreview = function() {
return this._dataModelTemplate_GridPreview;
};
/**
* @public
* @returns {(PropertiesModelTemplate_Form | undefined)}
*/
ComponentTemplate.prototype.getPropertiesModelTemplate_Form = function() {
return this._propertiesModelTemplate_Form;
};
/**
* Get the ID specified for the component configuration.
* This is NOT an instance id.
* @public
* @returns {(string | undefined)}
*/
ComponentTemplate.prototype.getTemplateId = function() {
return this._componentConfig.id;
};
/**
* Gets the base class name for this component
* @public
* @returns {(string | undefined)}
*/
ComponentTemplate.prototype.getClassName = function() {
return this._componentConfig.className || this._jsh.XExt.escapeCSSClass(this._componentConfig.id, { nodash: true });
};
/**
* @private
* @param {object[]} fields
* @returns {object[]}
*/
ComponentTemplate.prototype.processBrowserFields = function(fields) {
var retVal = [];
_.forEach(fields, function(field) {
retVal.push(field);
if (field.control !== 'link_browser' && field.control !== 'media_browser') {
return;
}
var browserType = { link_browser: 'link', media_browser: 'media' }[field.control];
/** @type {MediaBrowserControlInfo} */
var info = {
dataFieldName: field.name,
titleFieldName: field.name + '_jsh_browserDataTitle',
browserType: browserType,
validate: field.validate,
};
field.mediaBrowserControlInfo = info;
field.name = info.titleFieldName;
field.control = 'textbox';
field.controlclass = 'xtextbox_M';
field.type = 'varchar';
field.onchange = '(function() { var m = jsh.App[modelid]; if (m && m.onChangeBrowserTitleControl) m.onChangeBrowserTitleControl("' + info.dataFieldName + '"); })()';
delete field.validate;
retVal.push({
name: field.name + '_browserButton',
caption: '',
control: 'button',
value: 'Browse',
nl: false,
onclick: '(function() { var m = jsh.App[modelid]; if (m && m.openEditorBrowser) m.openEditorBrowser("' + info.dataFieldName + '"); })()'
});
retVal.push({
name: field.name + '_resetButton',
controlclass: 'secondary',
controlstyle: 'margin-left:10px;',
caption: '',
control: 'button',
value: 'Reset',
nl: false,
onclick: '(function() { var m = jsh.App[modelid]; if (m && m.resetEditorBrowser) m.resetEditorBrowser("' + info.dataFieldName + '"); })()'
});
var coreField = {
name: info.dataFieldName,
caption: '',
control: 'hidden',
type: 'varchar'
};
if(info.validate) coreField.validate = info.validate;
if(field.caption) coreField.caption_ext = field.caption;
retVal.push(coreField);
});
return retVal;
};
exports = module.exports = ComponentTemplate;
},{"./dataModelTemplate_formPreview":2,"./dataModelTemplate_gridPreview":3,"./propertiesModelTemplate_form":5,"lodash":32}],2:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var Cloner = require('../utils/cloner');
var FieldModel = require('./fieldModel');
/** @typedef {DataModelTemplate_FormPreview} DataModelTemplate_FormPreview */
/**
* @class
* @classdesc Normalizes the model for use in the form preview
* editor. Creates a model template that can then be used to generate
* unique instances of the model for editing.
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} dataModel - the raw data model from the component config
*/
function DataModelTemplate_FormPreview(componentTemplate, dataModel) {
/** @private @type {Object} */
this._jsh = componentTemplate._jsh;
/** @private @type {Object} */
this._cms = componentTemplate._cms;
/** @private @type {Object} */
this._componentTemplate = componentTemplate;
/** @private @type {string} */
this._componentTemplateId = componentTemplate.getTemplateId();
/** @private @type {string} */
this._itemTemplate = '';
/** @private @type {object} */
this._modelTemplate = {};
/** @private @type {string} */
this._rawOriginalJs = '';
this.buildTemplate(componentTemplate, dataModel);
}
/**
* @private
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} dataModel - the raw data model from the component config
*/
DataModelTemplate_FormPreview.prototype.buildTemplate = function(componentTemplate, dataModel) {
var modelConfig = Cloner.deepClone(dataModel || {});
if (modelConfig.layout !== 'grid_preview' && modelConfig.layout !== 'form') return undefined;
var componentConfig = this._componentTemplate && this._componentTemplate._componentConfig;
if(modelConfig.js && _.isString(modelConfig.js) && modelConfig.js.trim()) modelConfig.js = '(function(){ var cms = '+this._cms._instance+';' + modelConfig.js + ' })();';
this._rawOriginalJs = '\r\n' + (modelConfig.js || '') + '\r\n';
var popup = _.isArray(modelConfig.popup) ? modelConfig.popup : [];
var fields = modelConfig.fields || [];
fields.unshift({ control:'html', value:'<div class="jsharmony_cms">', captionclass:'hidden'});
fields.push({ control:'html', value:'</div>', captionclass:'hidden'});
fields.push({
caption: '', control:'html', value:'<div class="jsharmony_cms_preview_editor jsharmony_cms_component_preview" data-id="previewWrapper"></div>', 'block':true, captionclass:'hidden'
});
var model = _.extend({}, modelConfig);
this._modelTemplate = model;
model.title = modelConfig.title ? modelConfig.title : 'Edit ' + componentTemplate.getCaptions()[1];
model.popup = [ _.isNumber(popup[0]) ? popup[0] : 400, _.isNumber(popup[1]) ? popup[1] : 200 ];
model.fields = fields;
model.layout = 'form';
model.unbound = true;
model.onecolumn = true;
model.formclass = ((model.formclass||'')+' '+(componentConfig&&componentConfig.options&&componentConfig.options.component_preview_size=='collapse'?'jsharmony_cms_component_preview_collapse':'jsharmony_cms_component_preview_expand')).trim();
model.ejs = '';
model.js = this._rawOriginalJs;
this._jsh.XPage.ParseModelDefinition(model, null, null, { ignoreErrors: true });
_.forEach(fields, function(field) {
if (field.type != undefined && field.mediaBrowserControlInfo == undefined) {
field.onchange = '(function() { var m = jsh.App[modelid]; if (m && m.onChangeData) m.onChangeData(); })()';
}
});
var templateHtml = '<div>' + modelConfig.ejs + '</div>';
var itemTemplate = '';
var selItemPreview = (modelConfig.templates || {}).itemPreview;
if(selItemPreview){
//If itemPreview is set, extract the template from the model.ejs file
var itemPreview = this._jsh.$(templateHtml).find(selItemPreview);
if (itemPreview.length > 1) throw new Error('Item template must contain a single root element. Found ' + itemPreview.length + ' elements');
itemTemplate = itemPreview ? itemPreview.html() : undefined;
}
else {
//If templates are not used, return the entire model.ejs as the template
itemTemplate = templateHtml;
}
this._itemTemplate = itemTemplate;
};
/**
* Get the link browser field info (if exists) for the link field with
* the given field name.
* @public
* @param {string} fieldName
* @returns {(import('./componentTemplate').MediaBrowserControlInfo | undefined)}
*/
DataModelTemplate_FormPreview.prototype.getBrowserFieldInfo = function(fieldName) {
var field = _.find(this._modelTemplate.fields, function(field) {
return field.mediaBrowserControlInfo && field.mediaBrowserControlInfo.dataFieldName === fieldName;
});
return field ? field.mediaBrowserControlInfo : undefined;
};
/**
* Get the link browser field infos
* @public
* @returns {import('./componentTemplate').MediaBrowserControlInfo[]]}
*/
DataModelTemplate_FormPreview.prototype.getBrowserFieldInfos = function() {
var retVal = [];
_.forEach(this._modelTemplate.fields, function(field) {
if (field.mediaBrowserControlInfo) {
retVal.push(field.mediaBrowserControlInfo);
}
});
return retVal;
};
/**
* Get the EJS string used to render the item preview
* @public
* @returns {string}
*/
DataModelTemplate_FormPreview.prototype.getItemTemplate = function() {
return this._itemTemplate || '';
};
/**
* @public
*/
DataModelTemplate_FormPreview.prototype.getModelInstance = function() {
var model = Cloner.deepClone(this._modelTemplate);
model.id = DataModelTemplate_FormPreview.getNextInstanceId(this._componentTemplate);
return model;
};
/**
* Return the raw model JavaScript.
* @public
* @returns {Object}
*/
DataModelTemplate_FormPreview.prototype.getModelJs = function() {
return this._rawOriginalJs;
};
/**
* Get a unique ID for the model instance
* @private
* @returns {string}
*/
DataModelTemplate_FormPreview.getNextInstanceId = function(componentTemplate) {
if (DataModelTemplate_FormPreview._id == undefined) DataModelTemplate_FormPreview._id = 0;
var id = DataModelTemplate_FormPreview._id++;
return 'DataModel_FormPreview_' + componentTemplate.getClassName() + '_' + id;
};
/**
* Create a pristine copy of the data.
* This will remove extraneous properties (that don't exist in the model)
* and do data conversions. It will also add missing fields.
* The returned value will match the field model exactly.
*
* NOTE: this does not set default values! If the value is not set in
* dataInstance then the property will be set to undefined.
*
* @public
* @param {Object} dataInstance - the existing field values.
* @returns {Object} a copy of the dataInstance with type conversions done and extraneous
* properties removed.
*/
DataModelTemplate_FormPreview.prototype.makePristineCopy = function(dataInstance) {
return FieldModel.makePristineCopy(dataInstance, this._modelTemplate.fields);
};
/**
* Iterates through the fieldModels
* to look for fields with "type" property. If a field has the type property
* then the field will be added to the new data instance object.
*
* Setting the field follows specific rules
* 1. If the data instance does not contain the property key
* then the property is set to either undefined or the default value.
* 2. If the data instance contains the property and the property value is
* defined then it is left as-is.
* 3. If the data instance contains the property and the property value is
* null/undefined then the property is overridden if there is a default AND
* it is a required field. If it is not required then the value is left as
* null/undefined (which allows the user to clear default values that are
* not required fields).
*
* This will also correctly convert values as needed.
*
* This mutates the dataInstance.
* @public
* @param {Object} dataInstance - the data instance. Each property corresponds
* to a field in the field array. This object will be mutated.
* @returns {Object} the new or mutated data
*/
DataModelTemplate_FormPreview.prototype.populateDataInstance = function(dataInstance) {
return FieldModel.populateDataInstance(dataInstance, this._modelTemplate.fields || []);
};
exports = module.exports = DataModelTemplate_FormPreview;
},{"../utils/cloner":18,"./fieldModel":4,"lodash":32}],3:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var Cloner = require('../utils/cloner');
var FieldModel = require('./fieldModel');
/** @typedef {DataModelTemplate_GridPreview} DataModelTemplate_GridPreview */
/**
* @class
* @classdesc Normalizes the model for use in the grid preview
* editor. Creates a model template that can then be used to generate
* unique instances of the model for editing.
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} dataModel - the raw data model from the component config
*/
function DataModelTemplate_GridPreview(componentTemplate, dataModel) {
/** @private @type {Object} */
this._jsh = componentTemplate._jsh;
/** @private @type {Object} */
this._cms = componentTemplate._cms;
/** @private @type {Object} */
this._componentTemplate = componentTemplate;
/** @private @type {string} */
this._componentTemplateId = componentTemplate.getTemplateId();
/** @private @type {string} */
this._idFieldName = '';
/** @private @type {object} */
this._modelTemplate = {};
/** @private @type {string} */
this._rawOriginalJs = '';
/** @private @type {string} */
this._rowTemplate = '';
/** @private @type {string} */
this._sequenceFieldName = '';
this.buildTemplate(componentTemplate, dataModel);
}
/**
* @private
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} dataModel - the raw data model from the component config
*/
DataModelTemplate_GridPreview.prototype.buildTemplate = function(componentTemplate, dataModel) {
var modelConfig = Cloner.deepClone(dataModel || {});
if (modelConfig.layout !== 'grid_preview') return undefined;
if(modelConfig.js && _.isString(modelConfig.js) && modelConfig.js.trim()) modelConfig.js = '(function(){ var cms = '+this._cms._instance+';' + modelConfig.js + ' })();';
this._rawOriginalJs = '\r\n' + (modelConfig.js || '') + '\r\n';
var popup = _.isArray(modelConfig.popup) ? modelConfig.popup : [];
var fields = modelConfig.fields || [];
this._idFieldName = this.ensureIdField(fields);
this._sequenceFieldName = this.ensureSequenceField(fields);
_.forEach(fields, function(field) {
field.control = 'hidden';
});
fields.push({
name: 'component_preview', control: 'label', caption: '', unbound: true, controlstyle: 'vertical-align:baseline;display:block;min-height:1px;',
value: '<div tabindex="0" data-component-template="gridRow"></div>',
ongetvalue: 'return;'
});
var componentConfig = this._componentTemplate && this._componentTemplate._componentConfig;
var model = {};
this._modelTemplate = model;
model.title = 'Edit ' + componentTemplate.getCaptions()[0];
model.popup = [ _.isNumber(popup[0]) ? popup[0] : 400, _.isNumber(popup[1]) ? popup[1] : 200 ];
model.fields = fields;
model.layout = 'grid';
model.unbound = true;
model.newrowposition = 'last';
model.commitlevel= 'page';
model.hide_system_buttons = ['export', 'search', 'save', 'add'];
model.sort = [];
model.buttons = [
{link: 'js:_this.close()', icon: 'ok', actions: 'BIU', text: 'Done' },
{link: 'js:_this.addItem()', icon: 'add', actions: 'I', text: 'Add', class: 'jsharmony_cms_component_dataGridEditor_insert' },
];
model.getapi = 'return _this.getDataApi(xmodel, apitype)';
model.onrowbind = '_this.onRowBind(xmodel,jobj,datarow);';
model.oncommit = '_this.onCommit(xmodel, rowid, callback);';
model.ejs = '';
model.sort = { [this._sequenceFieldName]: 'asc' };
model.oninit = [
"jsh.$root('.xform'+xmodel.class).before('<div class=\"dataGridEditor_instructions\"><span style=\"font-size:1.3em;position:relative;top:1px;margin-right:2px;margin-left:4px;\">🛈</span> Add, edit, and re-order items using the icons <span class=\"dataGridEditor_instructions_doubleClick\">:: Double-click to edit</span></div>');",
].join(' ');
model.rowclass = "<%=xejs.iif(rowid==0,'first')%>";
model.tableclass = ((model.tableclass||'')+' '+(componentConfig&&componentConfig.options&&componentConfig.options.component_preview_size=='collapse'?'jsharmony_cms_component_preview_collapse':'jsharmony_cms_component_preview_expand')).trim();
this._jsh.XPage.ParseModelDefinition(model, null, null, { ignoreErrors: true });
//--------------------------------------------------
// Get templates
//--------------------------------------------------
var templateHtml = '<div>' + modelConfig.ejs + '</div>';
var rowTemplate = '';
var selRowPreview = (modelConfig.templates || {}).gridRowPreview;
if(selRowPreview){
//If gridRowPreview is set, extract the template from the model.ejs file
var rowPreview = this._jsh.$(templateHtml).find(selRowPreview);
if (rowPreview.length > 1) throw new Error('Row template must contain a single root element. Found ' + rowPreview.length + ' elements');
rowTemplate = rowPreview ? rowPreview.html() : undefined;
}
else {
//If templates are not used, return the entire model.ejs as the template
rowTemplate = templateHtml;
}
this._rowTemplate = rowTemplate;
return model;
};
/**
* Ensure that an ID field exists.
* If no ID field exists then one will be added.
* An error will be thrown if more than one ID field exists.
* @private
* @param {Object[]} fields
* @returns {string} the name of the ID field
*/
DataModelTemplate_GridPreview.prototype.ensureIdField = function(fields) {
var idFields = _.filter(fields, function(field) { return field.key; });
if (idFields.length > 1) throw new Error('Expected a single ID field. Found ' + idFields.length);
if (idFields.length < 1) {
var idField = { name: '_jsh_auto_id', type: 'varchar', control: 'hidden', key: true, caption: '', isAutoAddedField: true };
idFields = [idField];
fields.push(idField);
}
return idFields[0].name;
};
/**
* Ensure that a sequence field exists.
* If no sequence field exists then one will be added.
* @private
* @param {Object[]} fields
* @returns {string} the name of the sequence field
*/
DataModelTemplate_GridPreview.prototype.ensureSequenceField = function(fields) {
var seqFieldName = 'sequence'; //This is by convention!!!
var hasSeqField = _.some(fields, function(field) { return field.name === seqFieldName; });
if (!hasSeqField) {
var idField = { name: seqFieldName, type: 'int', control: 'hidden', caption: '', isAutoAddedField: true };
fields.push(idField);
}
return seqFieldName;
};
/**
* Get the name of the field used for the data item ID.
* @public
* @returns {string}
*/
DataModelTemplate_GridPreview.prototype.getIdFieldName = function() {
return this._idFieldName;
};
/**
* @public
*/
DataModelTemplate_GridPreview.prototype.getModelInstance = function() {
var model = Cloner.deepClone(this._modelTemplate);
model.id = DataModelTemplate_GridPreview.getNextInstanceId(this._componentTemplate);
//model.js is stringified and executed in the context of the model
/* globals modelid, jsh */
model.js = function() {
var gridApi = new jsh.XAPI.Grid.Static(modelid);
var formApi = new jsh.XAPI.Form.Static(modelid);
return {
getDataApi: function(xmodel, apiType) {
if (apiType === 'grid') return gridApi;
else if (apiType === 'form') return formApi;
}
};
};
return model;
};
/**
* Return the raw model JavaScript.
* @public
* @returns {Object}
*/
DataModelTemplate_GridPreview.prototype.getModelJs = function() {
return this._rawOriginalJs;
};
/**
* Get a unique ID for the model instance
* @private
* @returns {string}
*/
DataModelTemplate_GridPreview.getNextInstanceId = function(componentTemplate) {
if (DataModelTemplate_GridPreview._id == undefined) DataModelTemplate_GridPreview._id = 0;
var id = DataModelTemplate_GridPreview._id++;
return 'DataModel_GridPreview_' + componentTemplate.getClassName() + '_' + id;
};
/**
* Get the EJS string used to render the row item preview
* @public
* @returns {string}
*/
DataModelTemplate_GridPreview.prototype.getRowTemplate = function() {
return this._rowTemplate || '';
};
/**
* Create a pristine copy of the data.
* This will remove extraneous properties (that don't exist in the model)
* and do data conversions. It will also add missing fields.
* The returned value will match the field model exactly.
*
* NOTE: this does not set default values! If the value is not set in
* dataInstance then the property will be set to undefined.
*
* @public
* @param {Object} dataInstance - the existing field values.
* @param {Object} isAutoAddedField - if true then the added fields (e.g., ID, sequence) will be removed.
* @returns {Object} a copy of the dataInstance with type conversions done and extraneous
* properties removed.
*/
DataModelTemplate_GridPreview.prototype.makePristineCopy = function(dataInstance, removeAutoAddedFields) {
var fields = removeAutoAddedFields ? _.filter(this._modelTemplate.fields, function(field) { return !field.isAutoAddedField; }) : this._modelTemplate.fields;
return FieldModel.makePristineCopy(dataInstance, fields);
};
/**
* Iterates through the fieldModels
* to look for fields with "type" property. If a field has the type property
* then the field will be added to the new data instance object.
*
* Setting the field follows specific rules
* 1. If the data instance does not contain the property key
* then the property is set to either undefined or the default value.
* 2. If the data instance contains the property and the property value is
* defined then it is left as-is.
* 3. If the data instance contains the property and the property value is
* null/undefined then the property is overridden if there is a default AND
* it is a required field. If it is not required then the value is left as
* null/undefined (which allows the user to clear default values that are
* not required fields).
*
* This will also correctly convert values as needed.
*
* This mutates the dataInstance.
* @public
* @param {Object} dataInstance - the data instance. Each property corresponds
* to a field in the field array. This object will be mutated.
* @returns {Object} the new or mutated data
*/
DataModelTemplate_GridPreview.prototype.populateDataInstance = function(dataInstance) {
return FieldModel.populateDataInstance(dataInstance, this._modelTemplate.fields || []);
};
exports = module.exports = DataModelTemplate_GridPreview;
},{"../utils/cloner":18,"./fieldModel":4,"lodash":32}],4:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var Convert = require('../utils/convert');
/**
* @typedef {FieldModel} FieldModel
*/
/**
* @class
* @classdesc This class adds functionality for working with field models
* defined in the JSH component JSON.
*/
function FieldModel() {
}
/**
* JSH changes the types defined in the model.
* This will convert the fields in dataInstance to the correct type
* based on the fieldModel.
* @public
* @static
* @param {Object} dataInstance - the data to operate on (will be mutated).
* @param {Object[]} fields - the fields array from the model
*/
FieldModel.convertTypes = function(dataInstance, fields) {
if (dataInstance == undefined) return;
var numberTypeLut = {
bigint: true,
int: true,
smallint: true,
tinyint: true,
decimal: true,
float: true
};
dataInstance = dataInstance || {};
_.forEach(fields || [], function(field) {
var fieldName = field.name;
var fieldType = field.type;
if (fieldType == undefined) return;
if (!(fieldName in dataInstance)) return;
if (numberTypeLut[fieldType]) {
dataInstance[fieldName] = Convert.toNumber(dataInstance[fieldName]);
}
});
};
/**
* Create a pristine copy of the data.
* This will remove extraneous properties (that don't exist in the model)
* and do data conversions. It will also add missing fields.
* The returned value will match the field model exactly.
*
* NOTE: this does not set default values! If the value is not set in
* dataInstance then the property will be set to undefined.
*
* @public
* @static
* @param {Object} dataInstance - the existing field values.
* @param {Object[]} fields - the fields array from the model
* @returns {Object} a copy of the dataInstance with type conversions done and extraneous
* properties removed.
*/
FieldModel.makePristineCopy = function(dataInstance, fields) {
var pristineCopy = {};
_.forEach(fields, function(field) {
var fieldName = field.name;
var fieldType = field.type;
if (fieldType == undefined) return;
pristineCopy[fieldName] = dataInstance[fieldName];
});
FieldModel.convertTypes(pristineCopy);
return pristineCopy;
};
/**
* Iterates through the fieldModels
* to look for fields with "type" property. If a field has the type property
* then the field will be added to the new data instance object.
*
* Setting the field follows specific rules
* 1. If the data instance does not contain the property key
* then the property is set to either undefined or the default value.
* 2. If the data instance contains the property and the property value is
* defined then it is left as-is.
* 3. If the data instance contains the property and the property value is
* null/undefined then the property is overridden if there is a default AND
* it is a required field. If it is not required then the value is left as
* null/undefined (which allows the user to clear default values that are
* not required fields).
*
* This will also correctly convert values as needed.
*
* This mutates the dataInstance.
* @public
* @static
* @param {Object} dataInstance - the data instance. Each property corresponds
* to a field in the fieldModels array. This object will not be mutated.
* @param {Object[]} fields - the fields array from the model
* @returns {Object} the new or mutated data
*/
FieldModel.populateDataInstance = function(dataInstance, fields) {
dataInstance = dataInstance || {};
var fieldIndex = {};
_.forEach(fields || [], function(field) {
var fieldName = field.name;
var fieldType = field.type;
if (fieldType == undefined) return;
fieldIndex[fieldName] = field;
// Must follow the rules to ensure
// required fields are set to default values while also
// allowing default fields to be cleared by the user if they are
// not required fields.
if (dataInstance[fieldName] != undefined) {
return;
}
var isRequired = _.some((field.validate || []), function(a) { return a === 'Required'; });
var defaultValue= field.default;
var propertyKeyExists = fieldName in dataInstance;
if (propertyKeyExists && isRequired) {
// The property has been set by the user
// (since the key exists) but it is undefined/null
// while being required. This means the undefined value needs
// to be overridden with the default.
dataInstance[fieldName] = defaultValue;
} else if (!propertyKeyExists) {
// The property has not been set by the user
// (since the key does not exist) so must
// default to the default value (even if undefined/null)
dataInstance[fieldName] = defaultValue;
}
});
//Set _jsh_browserDataTitle to match field if blank
for(var fieldName in fieldIndex){
var titleFieldName = fieldName + '_jsh_browserDataTitle';
if(fieldIndex[titleFieldName]){
if(dataInstance[fieldName]){
if(!dataInstance[titleFieldName]) dataInstance[titleFieldName] = dataInstance[fieldName];
}
}
}
FieldModel.convertTypes(dataInstance);
// Must return in case original instance was null/undefined
return dataInstance;
};
exports = module.exports = FieldModel;
},{"../utils/convert":19,"lodash":32}],5:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var Cloner = require('../utils/cloner');
var FieldModel = require('./fieldModel');
/** @typedef {PropertiesModelTemplate_Form} PropertiesModelTemplate_Form */
/**
* @class
* @classdesc Normalizes the model for use in the property form
* editor. Creates a model template that can then be used to generate
* unique instances of the model for editing.
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} propertiesModel - the raw properties model from the component config
*/
function PropertiesModelTemplate_Form(componentTemplate, propertiesModel) {
/** @private @type {Object} */
this._jsh = componentTemplate._jsh;
/** @private @type {Object} */
this._cms = componentTemplate._cms;
/** @private @type {Object} */
this._componentTemplate = componentTemplate;
/** @private @type {string} */
this._componentTemplateId = componentTemplate.getTemplateId();
/** @private @type {object} */
this._modelTemplate = {};
this.buildTemplate(componentTemplate, propertiesModel);
}
/**
* @private
* @param {import('./componentTemplate').ComponentTemplate} componentTemplate
* @param {object} propertiesModel - the raw properties model from the component config
*/
PropertiesModelTemplate_Form.prototype.buildTemplate = function(componentTemplate, propertiesModel) {
var modelConfig = Cloner.deepClone(propertiesModel || {});
var model = _.extend({}, modelConfig);
if(modelConfig.fields && modelConfig.fields.length){
modelConfig.fields.unshift({ control:'html', value:'<div class="jsharmony_cms">',captionclass:'hidden'});
modelConfig.fields.push({ control:'html', value:'</div>',captionclass:'hidden'});
}
this._modelTemplate = model;
model.title = modelConfig.title ? modelConfig.title : 'Configure ' + componentTemplate.getCaptions()[0];
model.unbound = true;
model.layout = 'form';
model.onecolumn = true;
if(model.js && _.isString(model.js) && model.js.trim()) model.js = '(function(){ var cms = '+this._cms._instance+';' + model.js + ' })();';
this._jsh.XPage.ParseModelDefinition(model, null, null, { ignoreErrors: true });
};
/**
* @public
*/
PropertiesModelTemplate_Form.prototype.getModelInstance = function() {
var model = Cloner.deepClone(this._modelTemplate);
model.id = PropertiesModelTemplate_Form.getNextInstanceId(this._componentTemplate);
return model;
};
/**
* Get a unique ID for the model instance
* @private
* @returns {string}
*/
PropertiesModelTemplate_Form.getNextInstanceId = function(componentTemplate) {
if (PropertiesModelTemplate_Form._id == undefined) PropertiesModelTemplate_Form._id = 0;
var id = PropertiesModelTemplate_Form._id++;
return 'PropertiesModel_Form_' + componentTemplate.getClassName() + '_' + id;
};
/**
* Create a pristine copy of the data.
* This will remove extraneous properties (that don't exist in the model)
* and do data conversions. It will also add missing fields.
* The returned value will match the field model exactly.
*
* NOTE: this does not set default values! If the value is not set in
* dataInstance then the property will be set to undefined.
*
* @public
* @param {Object} dataInstance - the existing field values.
* @returns {Object} a copy of the dataInstance with type conversions done and extraneous
* properties removed.
*/
PropertiesModelTemplate_Form.prototype.makePristineCopy = function(dataInstance) {
return FieldModel.makePristineCopy(dataInstance, this._modelTemplate.fields);
};
/**
* Iterates through the fieldModels
* to look for fields with "type" property. If a field has the type property
* then the field will be added to the new data instance object.
*
* Setting the field follows specific rules
* 1. If the data instance does not contain the property key
* then the property is set to either undefined or the default value.
* 2. If the data instance contains the property and the property value is
* defined then it is left as-is.
* 3. If the data instance contains the property and the property value is
* null/undefined then the property is overridden if there is a default AND
* it is a required field. If it is not required then the value is left as
* null/undefined (which allows the user to clear default values that are
* not required fields).
*
* This will also correctly convert values as needed.
*
* This mutates the dataInstance.
* @public
* @param {Object} dataInstance - the data instance. Each property corresponds
* to a field in the field array. This object will be mutated.
* @returns {Object} the new or mutated data
*/
PropertiesModelTemplate_Form.prototype.populateDataInstance = function(dataInstance) {
return FieldModel.populateDataInstance(dataInstance, this._modelTemplate.fields || []);
};
exports = module.exports = PropertiesModelTemplate_Form;
},{"../utils/cloner":18,"./fieldModel":4,"lodash":32}],6:[function(require,module,exports){
/*
Copyright 2020 apHarmony
This file is part of jsHarmony.
jsHarmony is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jsHarmony is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this package. If not, see <http://www.gnu.org/licenses/>.
*/
var _ = require('lodash');
var DialogResizer = require('./dialogResizer');
var OverlayService = require('./overlayService');
/**
* @typedef {Object} DialogConfig
* @property {(boolean | undefined)} closeOnBackdropClick - Set true to close the dialog
* when the background is clicked
* @property {(string | undefined)} dialogId - set this to override the assigned unique ID for the dialog.
* There is no need to set this. If it is set, it must be globally unique among ALL dialogs.
* @property {(number | undefined)} maxHeight - set the max height (pixels) of the form if defined
* @property {(number | undefined)} maxWidth - set the max width (pixels) of the form if defined
* @property {(number | undefined)} minHeight - set the min height (pixels) of the form if defined
* @property {(number | undefined)} minWidth - set the min width (pixels) of the form if defined
* @property {(number | undefined)} width - set the width (pixels) of the form if defined
* @property {(number | undefined)} height - set the height (pixels) of the form if defined
* @property {(string | undefined)} cssClass - space delimited list of classes to add to the dialog element
*/
/**
* Called when the dialog wants to accept/save the changes
* @callback Dialog~acceptCallback
* @param {Function} successFunc - Call this function if successfully accepted (e.g., no data errors; valid save).
*/
/**
* Called when the dialog is first opened
* @callback Dialog~beforeOpenCallback
* @param {Object} xmodel - the JSH model instance
* @param {Function} onComplete - Should be called by handler when complete
*/
/**
* Called when the dialog wants to cancel/close without saving
* @callback Dialog~cancelCallback
* @param {Object} options
* @returns {boolean}
*/
/**
* Called when the dialog closes
* @callback Dialog~closeCallback
* @param {Object} options
*/
/**
* Called when the dialog is first opened
* @callback Dialog~openedCallback
* @param {JQuery} dialogWrapper - the dialog wrapper element
* @param {Object} xmodel - the JSH model instance
* @param {Function} acceptFunc - Call this function to trigger accept logic
* @param {Function} cancelFunc - Call this function to trigger cancel logic
*/
/**
* @class
* @param {Object} jsh
* @param {Object} cms
* @param {Object} model - the model that will be loaded into the virtual model
* @param {DialogConfig} config - the dialog configuration
*/
function Dialog(jsh, cms, model, config) {
this._jsh = jsh;
this._cms = cms;
this._model = model;
this._id = config.dialogId ? config.dialogId : this.getNextId();
/** @type {DialogConfig} */
this._config = config || {};
this._$wrapper = this.makeDialog(this._id, this._config);
this._destroyed = false;
this.overlayService = new OverlayService(this);
this._jsh.$(this._jsh.root).append(this._$wrapper);
/**
* @public
* @type {Dialog~acceptCallback}
*/
this.onAccept = undefined;
/**
* @public
* @type {Dialog~beforeOpenCallback}
*/
this.onBeforeOpen = undefined;
/**
* @public
* @type {Dialog~cancelCallback}
*/
this.onCancel = undefined;
/**
* @public
* @type {Dialog~closeCallback}
*/
this.onClose = undefined;
/**
* @public
* @type {Dialog~openedCallback}
*/
this.onOpened = undefined;
}
/**
* Used to keep track of dialog IDs to
* ensure IDs are unique.
* @type {Object.<string, boolean>}
*/
Dialog._idLookup = {};
/**
* Call when dialog is closed.
* Dialog is no longer usable after this is called.
* @private
*/
Dialog.prototype.destroy = function() {
this._$wrapper.remove();
if (this._$overlay) this._$overlay.remove();
delete Dialog._idLookup[this._id];
this._destroyed = true;
};
/**
* Get a CSS selector that can be used to find
* the wrapper element.
* @public
* @returns {string}
*/
Dialog.prototype.getFormSelector = function() {
return '.xdialogbox.' + this._id;
};
/**
* Get a globally unique (W.R.T this dialog class)
* ID to be used for the current dialog instance
* @private
* @returns {string}
*/
Dialog.prototype.getNextId = function() {
var id = undefined;
do {
id = 'jsharmony_component_dialog_uid_' + Math.random().toString().replace('.', '');
if (Dialog._idLookup[id]) {
id = undefined;
}
} while (!id);
Dialog._idLookup[id] = true;
return id;
};
/**
* Get the scroll top position for the page.
* @private
* @param {JQuery} $wrapper
* @returns {number}
*/
Dialog.prototype.getScrollTop = function($wrapper) {
return $wrapper.scrollParent().scrollTop();
};
/**
* @private
*/
Dialog.prototype.load = function(callback) {
var _this = this;
this._jsh.XPage.LoadVirtualModel(_this._jsh.$(_this.getFormSelector()), this._model, function(xmodel) {
callback(xmodel);
});
};
/**
* Create the dialog elements and append to the body DOM.
* @private
* @param {string} id - the ID that uniquely identifies the dialog
* @param {DialogConfig} config
*/
Dialog.prototype.makeDialog = function(id, config) {
var $form = this._jsh.$('<div class="xdialogbox"></div>')
.addClass(this._id)
.attr('id', this._id)
.addClass(config.cssClass || '');
if(config.maxWidth) $form.css('max-width', _.isNumber(config.maxWidth) ? config.maxWidth + 'px' : null);
if(config.maxHeight) $form.css('max-height', _.isNumber(config.maxHeight) ? config.maxHeight + 'px' : null);
if(config.minWidth) $form.css('min-width', _.isNumber(config.minWidth) ? config.minWidth + 'px' : null);
if(config.minHeight) $form.css('min-height', _.isNumber(config.minHeight) ? config.minHeight + 'px' : null);
if(config.height) $form.css('height', _.isNumber(config.height) ? config.height + 'px' : null);
if(config.width) $form.css('width', _.isNumber(config.width) ? config.width + 'px' : null);
var $wrapper = this._jsh.$('<div style="display: none;" class="xdialogbox-wrapper"></div>')
.attr('id', id)
.append($form);
return $wrapper;
};
/**
* @public
*/
Dialog.prototype.open = function() {
if (this._destroyed) {
throw new Error('Dialog ' + this._id + ' has already been destroyed.');
}
var _this = this;
var formSelector = this.getFormSelector();
var oldActive = document.activeElement;
this.load(function(xmodel) {
var $wrapper = _this._jsh.$(formSelector);
_this.registerLovs(xmodel);
var lastScrollTop = 0;
_this._jsh.XExt.execif(_this.onBeforeOpen,
function(f){
_this.onBeforeOpen(xmodel, f);
},
function(){
/** @type {DialogResizer} */
var dialogResizer = undefined;
_this._jsh.XExt.CustomPrompt(formSelector, _this._jsh.$(formSelector),
function(acceptFunc, cancelFunc) { //onInit
_this.overlayService.pushDialog($wrapper);
lastScrollTop = _this.getScrollTop($wrapper);
dialogResizer = new DialogResizer($wrapper[0], _this._jsh);
if (_.isFunction(_this.onOpened)) _this.onOpened($wrapper, xmodel, acceptFunc, cancelFunc);
},
function(success) { //onAccept
if (_.isFunction(_this.onAccept)) _this.onAccept(success);
},
function(options) { //onCancel
if (_.isFunction(_this.onCancel)) return _this.onCancel(options);
return false;
},
function() { //onClosed
dialogResizer.closeDialog();
if(_.isFunction(_this.onClose)) _this.onClose();
_this.destroy();
_this.overlayService.popDialog();
},
{
reuse: false,
backgroundClose: _this._config.closeOnBackdropClick,
restoreFocus: false,
onClosing: function(cb){
if (oldActive) oldActive.focus();
_this.setScrollTop(lastScrollTop, $wrapper);
return cb();
}
}
);
}
);
});
};
/**
* Register the LOVs defined in the model.
* @private
* @param {Object} xmodel
*/
Dialog.prototype.registerLovs = function(xmodel) {
_.forEach(this._model.fields, function(field) {
if (field.type == undefined || field.lov == undefined) return;
var lovs = undefined;
if (_.isArray(field.lov.values)) {
lovs = field.lov.values;
} else if (_.isObject(field.lov.values)) {
lovs = _.map(_.toPairs(field.lov.values), function(kvp) {
return { code_val: kvp[0], code_txt: kvp[1] };
});
}
if (lovs) {
xmodel.controller.setLOV(field.name, lovs);
}
});
};
/**
* Set the scroll top position for the page