UNPKG

jsharmony-cms

Version:
370 lines (303 loc) 13.7 kB
/* 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 FormDialog = require('../dialogs/formDialog'); var HTMLPropertyEditorController = require('./htmlPropertyEditorController'); var TemplateRenderer = require('../templateRenderer'); /** @typedef {import('../templateRenderer').RenderConfig} RenderConfig */ /** @typedef {import('../componentModel/componentTemplate').MediaBrowserControlInfo} MediaBrowserControlInfo */ /** @typedef {import('../componentModel/componentTemplate').ComponentTemplate} ComponentTemplate */ /** * @callback DataEditor_Form~beforeRenderDataItemPreview * @param {RenderConfig} renderConfig */ /** * @callback DataEditor_Form~renderDataItemPreview * @param {HTMLElement} element * @param {Object} data - the component data * @param {Object} properties - the component properties * @param {Object} cms - the parent jsHarmonyCMSInstance * @param {Object} component - the parent component */ /** * @class * @param {ComponentTemplate} componentTemplate * @param {(import('../templateRenderer').GridPreviewRenderContext | undefined)} gridContext * @param {Object} cms * @param {Object} jsh * @param {Object} component */ function DataEditor_Form(componentTemplate, gridContext, isReadOnly, cms, jsh, component) { /** @private @type {ComponentTemplate} */ this._componentTemplate = componentTemplate; /** @private @type {boolean} */ this._isReadOnly = isReadOnly; /** @private @type {Object} */ this._cms = cms; /** @private */ this._gridContext = gridContext; /** @private @type {Object} */ this._jsh = jsh; /** @private @type {Object} */ this._component = component; /** @private @type {HTMLPropertyEditorController[]} */ this._htmlEditors = []; /** @private @type {DataEditor_Form~beforeRenderDataItemPreview} */ this._onBeforeRenderDataItemPreview = undefined; /** @private @type {DataEditor_Form~renderDataItemPreview} */ this._onRenderDataItemPreview = undefined; } /** * @private * @param {JQuery} $dialog - the dialog element. * @param {JQuery} $wrapper - the preview wrapper element. */ DataEditor_Form.prototype.attachEditors = function($dialog, $wrapper, $toolbar) { var _this = this; _.forEach(this._htmlEditors, function(editor) { editor.destroy(); }); _.forEach($wrapper.find('[data-component-full-editor]'), function (editorEl) { var $el = _this._jsh.$(editorEl); var propName = $el.attr('data-component-full-editor'); var editor = new HTMLPropertyEditorController('full', _this._jsh, _this._cms, $dialog, propName, $el, $toolbar); editor.initialize(function() {}); _this._htmlEditors.push(editor); }); _.forEach($wrapper.find('[data-component-title-editor]'), function (editorEl) { var $el = _this._jsh.$(editorEl); var propName = $el.attr('data-component-title-editor'); var editor = new HTMLPropertyEditorController('title', _this._jsh, _this._cms, $dialog, propName, $el, $toolbar); editor.initialize(function() {}); _this._htmlEditors.push(editor); }); }; /** * @private * @param {JQuery} $dialog * @param {MediaBrowserControlInfo} info * @param {boolean} enable */ DataEditor_Form.prototype.enableBrowserControl = function($dialog, info, enable) { var jctrl = $dialog.find('.xform_ctrl.' + info.titleFieldName); if(jctrl.hasClass('editable')){ $dialog.find('.xform_ctrl.' + info.titleFieldName).attr('disabled', enable ? null : true); } }; /** * Open the editor * @public * @param {Object} itemData - the data used to render the component. * @param {Object} properties - the component's configured properties (used to render the component) * @param {Function} onAcceptCb - Called if the data is updated. Arg0 is updated data. * @param {Function} onCloseCb - Called anytime the dialog is closed. */ DataEditor_Form.prototype.open = function(itemData, properties, onAcceptCb, onCloseCb) { var _this = this; var modelTemplate = this._componentTemplate.getDataModelTemplate_FormPreview(); var modelConfig = modelTemplate.getModelInstance(); var template = modelTemplate.getItemTemplate(); if (this._isReadOnly) { modelConfig.actions = 'B'; } itemData = modelTemplate.populateDataInstance(itemData || {}); var dialog = new FormDialog(this._jsh, this._cms, modelConfig, { acceptButtonLabel: 'OK', cancelButtonLabel: 'Cancel', closeOnBackdropClick: true, cssClass: (this._component.getDialogClass()||'')+' jsharmony_cms_component_dialog jsharmony_cms_component_dialog_form jsharmony_cms_component_dataFormItemEditor jsharmony_cms_component_dataFormItemEditor_' + this._componentTemplate.getTemplateId(), dialogId: modelConfig.id, minHeight: modelConfig.popup[1], minWidth: modelConfig.popup[0] }); var $toolbar; dialog.onBeforeOpen = function(xmodel, dialogSelector, onComplete) { var editor = _this._jsh.App[xmodel.id]; var $dialog = _this._jsh.$(dialogSelector); $dialog.css('opacity', '0'); _this._formSelector = dialogSelector; // remove this // Note that the toolbar HAS to be in the popup DOM hierarchy for focus/blur // events to work correctly. $toolbar = _this._jsh.$('<div class="jsharmony_cms_content_editor_toolbar"></div>') .css('position', 'fixed') .css('top', '0px') .css('left', '0') .css('width', '100%') .css('z-index', '1999999999'); _this._jsh.$(dialogSelector).append($toolbar); _.forEach(modelTemplate.getBrowserFieldInfos(), function(info) { var title = itemData[info.titleFieldName] || ''; var data = itemData[info.dataFieldName] || ''; var fieldsMatch = title === data; var isDataEmpty = title.length < 1 && data.length < 1; var fieldIsEditable = fieldsMatch || isDataEmpty; _this.enableBrowserControl($dialog, info, fieldIsEditable); }); editor.onChangeData_noDebounce = function() { if(!_this._jsh.XModels[xmodel.id]){ return; } var updatedData = {}; _.forEach(modelConfig.fields, function(field) { if (field.type != undefined) { updatedData[field.name] = xmodel.get(field.name); } }); var $wrapper = $dialog.find('[data-id="previewWrapper"]').first(); _this.renderPreview($wrapper, template, updatedData, properties, function(){ // Don't attach any events until after the onRenderGridItemPreview hook is called. // Otherwise, the events might be attached to elements that get replaced or removed. _this.attachEditors($dialog, $wrapper, $toolbar); }); }; // This function NEEDS to be debounced. // It SHOULD be anyway so it doesn't re-render the preview on every // keystroke, but it HAS to be just in case two fields change // at the same time (in which case the first change causes a re-render // and the second change breaks things since parts of the re-render are async) // ** Follow up - All "debounce" should be removed and replaced with custom render queues editor.onChangeData = _.debounce(editor.onChangeData_noDebounce, 300); editor.openEditorBrowser = function(browserControlName) { var info = modelTemplate.getBrowserFieldInfo(browserControlName); if (info == undefined) return; var update = function(url, title) { // IMPORTANT! Set the title FIRST. // The change handler is attached to the title // so that will run and update the link control, // and then we override the link control. xmodel.set(info.titleFieldName, title); xmodel.set(browserControlName, url); _this.enableBrowserControl($dialog, info, false); editor.onChangeData(); }; if (info.browserType === 'link') { if (info == undefined) return; _this._cms.editor.picker.openLink(function(url, data) { var title = url||''; if(data){ if(data.page_path) title = data.page_path; else if(data.media_path) title = data.media_path; else if(data.item_path) title = data.item_path; } update(url, title); }, xmodel.get(browserControlName)); } else if (info.browserType === 'media') { _this._cms.editor.picker.openMedia(function(url, data) { var title = data.media_path; update(url, title); }, xmodel.get(browserControlName)); } else { console.warn(new Error('Unknown editor browser type ' + info.browserType)); // eslint-disable-line no-console } }; editor.onChangeBrowserTitleControl = function(browserControlName) { // When the user manually changes the link title, // the link value must be set to the title value. var info = modelTemplate.getBrowserFieldInfo(browserControlName); if (info == undefined) return; xmodel.set(browserControlName, xmodel.get(info.titleFieldName)); editor.onChangeData(); }; editor.resetEditorBrowser = function(linkControlName) { var info = modelTemplate.getBrowserFieldInfo(linkControlName); if (info == undefined) return; _this.enableBrowserControl($dialog, info, true); xmodel.set(linkControlName, ''); xmodel.set(info.titleFieldName, ''); editor.onChangeData(); }; _this._onBeforeRenderDataItemPreview = editor.onBeforeRenderDataItemPreview; _this._onRenderDataItemPreview = editor.onRenderDataItemPreview; if(onComplete) onComplete(); }; dialog.onOpened = function($dialog, xmodel) { var editor = _this._jsh.App[xmodel.id]; // Manually call change to do initial render setTimeout(function() { editor.onChangeData_noDebounce(); setTimeout(function() { $dialog.css('opacity', '1'); }, 50); }); }; dialog.onAccept = function($dialog, xmodel) { if(!xmodel.controller.Commit(itemData, 'U')) return false; itemData = modelTemplate.makePristineCopy(itemData); if (_.isFunction(onAcceptCb)) onAcceptCb(itemData); return true; }; dialog.onCancel = function(options, $dialog, xmodel) { if (!options.force && xmodel.controller.HasUpdates()) { _this._jsh.XExt.Confirm('Close without saving changes?', function() { xmodel.controller.form.ResetDataset(); options.forceCancel(); }); return false; } }; dialog.onClose = function($dialog, xmodel) { //Destroy model if (xmodel.controller && xmodel.controller.OnDestroy) xmodel.controller.OnDestroy(); if (typeof xmodel.ondestroy != 'undefined') xmodel.ondestroy(xmodel); delete _this._jsh.XModels[xmodel.id]; delete _this._jsh.App[xmodel.id]; _.forEach(_this._htmlEditors, function(editor) { editor.destroy(); }); if (_.isFunction(onCloseCb)) onCloseCb(); }; dialog.open(itemData); }; /** * @private * @param {JQuery} $wrapper * @param {string} template * @param {Object} data * @param {Object} properties */ DataEditor_Form.prototype.renderPreview = function($wrapper, template, data, properties, callback) { var _this = this; var renderData = { item: data }; var componentConfig = this._componentTemplate && this._componentTemplate._componentConfig; if(componentConfig && componentConfig.data && (componentConfig.data.layout == 'grid_preview')){ renderData = { items: [data] }; } var renderConfig = TemplateRenderer.createRenderConfig(template, renderData, properties, this._cms); renderConfig.gridContext = this._gridContext; if (_.isFunction(this._onBeforeRenderDataItemPreview)) this._onBeforeRenderDataItemPreview(renderConfig); var rendered = TemplateRenderer.render(renderConfig, 'gridItemPreview', this._jsh, this._cms, componentConfig); $wrapper.empty().append(rendered); if(this._cms && this._cms.editor) this._cms.editor.disableLinks($wrapper); var renderPromise = null; if (_.isFunction(this._onRenderDataItemPreview)){ var renderRslt = this._onRenderDataItemPreview($wrapper.children()[0], renderConfig.data, renderConfig.properties, _this._cms, _this._component); if(renderRslt && renderRslt.then) renderPromise = renderRslt; } _this._component.notifyUpdate($wrapper.children()[0], { data: renderConfig.data, properties: renderConfig.properties, isItemPreview: true, }); setTimeout(function() { _.forEach(_this._jsh.$($wrapper.children()[0]).find('[data-component]'), function(el) { _this._cms.componentManager.renderContentComponent(el); }); if(callback){ if(renderPromise){ renderPromise.then(function(){ callback(); }); } else callback(); } }, 50); }; exports = module.exports = DataEditor_Form;