/* * Copyright (C) 2023 Xibo Signage Ltd * * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * * Xibo is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * Xibo 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Xibo. If not, see . */ // Common functions/tools const Common = require('../editor-core/common.js'); const PlayerHelper = require('../helpers/player-helper.js'); // LOCAL FUNCTIONS // Check condition function checkCondition(type, value, targetValue, isTopLevel = true) { if (type === 'ne' && value === '') { return true; } else if (type === 'eq' && targetValue == value) { return true; } else if (type === 'neq' && targetValue != value) { return true; } else if (type === 'gt' && !Number.isNaN(parseInt(targetValue)) && !Number.isNaN(parseInt(value)) && parseInt(targetValue) > parseInt(value)) { return true; } else if (type === 'lt' && !Number.isNaN(parseInt(targetValue)) && !Number.isNaN(parseInt(value)) && parseInt(targetValue) < parseInt(value)) { return true; } else if (type === 'egt' && !Number.isNaN(parseInt(targetValue)) && !Number.isNaN(parseInt(value)) && parseInt(targetValue) >= parseInt(value)) { return true; } else if (type === 'elt' && !Number.isNaN(parseInt(targetValue)) && !Number.isNaN(parseInt(value)) && parseInt(targetValue) <= parseInt(value)) { return true; } else if (type === 'isTopLevel' && value == isTopLevel) { return true; } else { return false; } } /** * Create context and pass it to createTableFromContext * @param {object} dialog */ function createDisplayGroupMembersTable(dialog) { const control = $(dialog).find('.controlDiv'); const context = { tableName: '#displaysGroupsMembersTable', columns: [ {data: 'displayGroupId', responsivePriority: 2}, {data: 'displayGroup', responsivePriority: 2}, ], members: control.data().members.displayGroups, extra: dialog.data().extra.displayGroupsAssigned, id: 'displayGroupId', type: 'displayGroup', getUrl: control.data().displayGroupsGetUrl, }; createTableFromContext(dialog, context); } /** * Create context and pass it to createTableFromContext * @param {object} dialog */ function createDisplayMembersTable(dialog) { const control = $(dialog).find('.controlDiv'); const context = { tableName: '#displaysMembersTable', columns: [ {data: 'displayId', responsivePriority: 2}, {data: 'display', responsivePriority: 2}, { data: 'mediaInventoryStatus', responsivePriority: 2, render: function(data, type, row) { if (type != 'display') { return data; } let icon = ''; if (data == 1) { icon = 'fa-check'; } else if (data == 0) { icon = 'fa-times'; } else { icon = 'fa-cloud-download'; } return ''; }, }, { data: 'loggedIn', render: dataTableTickCrossColumn, responsivePriority: 3, }, { name: 'clientSort', responsivePriority: 3, data: function(data) { return data.clientType + ' ' + data.clientVersion + '-' + data.clientCode; }, visible: false, }, ], members: control.data().members.displays, extra: dialog.data().extra.displaysAssigned, id: 'displayId', type: 'display', getUrl: control.data().displayGetUrl, }; createTableFromContext(dialog, context); } /** * Create context and pass it to createTableFromContext * @param {object} dialog */ function createUserMembersTable(dialog) { const control = $(dialog).find('.controlDiv'); const context = { tableName: '#userMembersTable', columns: [ {data: 'userId', responsivePriority: 2}, {data: 'userName', responsivePriority: 2}, ], members: control.data().members.users, extra: dialog.data().extra.usersAssigned, id: 'userId', type: 'user', getUrl: control.data().userGetUrl, }; createTableFromContext(dialog, context); } /** * Create context and pass it to createTableFromContext * @param {object} dialog */ function createUserGroupMembersTable(dialog) { const control = $(dialog).find('.controlDiv'); const context = { tableName: '#userGroupMembersTable', columns: [ {data: 'groupId', responsivePriority: 2}, {data: 'group', responsivePriority: 2}, ], members: control.data().members.userGroups, extra: dialog.data().extra.userGroupsAssigned, id: 'groupId', type: 'userGroup', getUrl: control.data().userGroupsGetUrl, }; createTableFromContext(dialog, context); } /** * Create datatable from provided context * @param {object} dialog * @param {object} context */ function createTableFromContext(dialog, context) { const control = $(dialog).find('.controlDiv'); const columns = context.columns; columns.push({ name: 'member', responsivePriority: 2, data: function(data, type, row) { if (type != 'display') { return data; } let checked = ''; // Check if the element is already been checked/unchecked if (typeof control.data().members != 'undefined' && context.members[data[context.id]] != undefined ) { checked = (context.members[data[context.id]]) ? 'checked' : ''; } else { // If its not been altered, check for the original state if (dialog.data().extra) { context.extra.forEach(function(extraElement) { if (extraElement[context.id] == data[context.id]) { checked = 'checked'; } }); } } // Create checkbox return ''; }, }); const table = $(context.tableName).DataTable({ language: dataTablesLanguage, serverSide: true, stateSave: true, stateDuration: 0, filter: false, responsive: true, searchDelay: 3000, order: [[1, 'asc']], ajax: { url: context.getUrl ?? control.data().getUrl, data: function(data) { $.extend(data, $(context.tableName) .closest('.XiboGrid') .find('.FilterDiv form') .serializeObject(), ); return data; }, }, columns: columns, }); table.on('draw', dataTableDraw); table.on('processing.dt', dataTableProcessing); } function assignMediaToCampaign(url, media, unassignMedia) { toastr.info('Assign Media', media); $.ajax({ type: 'post', url: url, cache: false, dataType: 'json', data: {mediaId: media, unassignMediaId: unassignMedia}, success: XiboSubmitResponse, }); } function assignLayoutToCampaign(url, layout, unassignLayout) { toastr.info('Assign Layout', layout); $.ajax({ type: 'post', url: url, cache: false, dataType: 'json', data: {layoutId: layout, unassignLayoutId: unassignLayout}, success: XiboSubmitResponse, }); } /** * Set the checkbox state based on the adjacent features * @param triggerElement */ function setFeatureGroupCheckboxState(triggerElement) { // collect up the checkboxes belonging to the same group const $featureGroup = triggerElement.closest('tbody.feature-group'); const countChecked = $featureGroup.find('input[name=\'features[]\']:checked').length; const countTotal = $featureGroup.find('input[name=\'features[]\']').length; setCheckboxState( countChecked, countTotal, $featureGroup, '.feature-select-all', ); // collect up the inherit checkboxes belonging to the same group const countInheritChecked = $featureGroup.find('input.inherit-group:checked').length; const countInheritTotal = $featureGroup.find('input.inherit-group').length; setCheckboxState( countInheritChecked, countInheritTotal, $featureGroup, '.inherit-group-all', ); } /** * Set checkbox state helper function * @param count * @param countTotal * @param $selector * @param checkboxClass */ function setCheckboxState(count, countTotal, $selector, checkboxClass) { if (count <= 0) { $selector.find(checkboxClass) .prop('checked', false) .prop('indeterminate', false); } else if (count === countTotal) { $selector.find(checkboxClass) .prop('checked', true) .prop('indeterminate', false); } else { $selector.find(checkboxClass) .prop('checked', false) .prop('indeterminate', true); } } // GLOBAL FUNCTIONS window.forms = { /** * Create form inputs from an array of elements * @param {object} properties - The properties to set on the form * @param {object} targetContainer - The container to add the properties to * @param {string} [targetId] - Target Id ( widget, element, etc.) * @param {boolean} [playlistId] - If widget, the playlistId * @param {object[]} [propertyGroups] - Groups to add the properties to * @param {string} [customClass] - Custom class to add to the form fields * @param {boolean} [prepend] - Prepend fields instead? * - If the properties are for an element */ createFields: function( properties, targetContainer, targetId, playlistId, propertyGroups = [], customClass, prepend = false, ) { for (const key in properties) { if (properties.hasOwnProperty(key)) { const property = properties[key]; // If element is marked as skip if (property.skip === true) { continue; } // Handle default value if (property.value === null && property.default !== undefined) { property.value = property.default; } // Handle visibility if ( property.visibility.length ) { const rules = []; // Add all conditions to an array for (let i = 0; i < property.visibility.length; i++) { const test = property.visibility[i]; const testObject = { type: test.type, conditions: [], }; for (let j = 0; j < test.conditions.length; j++) { const condition = test.conditions[j]; testObject.conditions.push({ field: condition.field, type: condition.type, value: condition.value, }); } rules.push(testObject); } property.visibility = JSON.stringify(rules); } // Special properties // Dataset selector if (property.type === 'datasetSelector') { property.datasetSearchUrl = urlsForApi.dataset.search.url; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'dataSetId'; } } // Dataset Field selector if (property.type === 'datasetField') { // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'datasetField'; } } // Menu boards selector if (property.type === 'menuBoardSelector') { property.menuBoardSearchUrl = urlsForApi.menuBoard.search.url; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'menuId'; } } // Menu category selector if (property.type === 'menuBoardCategorySelector') { property.menuBoardCategorySearchUrl = urlsForApi.menuBoard.categorySearch.url; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'menuCategoryId'; } } // Media selector if (property.type === 'mediaSelector') { property.mediaSearchUrl = urlsForApi.library.get.url; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'mediaId'; } } // Fonts selector if (property.type === 'fontSelector') { property.fontsSearchUrl = getFontsUrl + '?length=10000'; } // Stored command selector if (property.type === 'commandSelector') { property.commandSearchUrl = urlsForApi.command.search.url; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = 'code'; } } // Playlist Mixer if (property.type === 'playlistMixer') { property.playlistId = playlistId; } // Colour format if ( property.type === 'color' && property.format != '' ) { property.colorFormat = property.format; } // dashboards available services if (property.type === 'connectorProperties') { property.connectorPropertiesUrl = urlsForApi.connectorProperties.search.url.replace(':id', targetId) + '?propertyId=' + property.id; // If we don't have a value, set value key pair to null if (property.value == '') { property.initialValue = null; property.initialKey = null; } else { property.initialValue = property.value; property.initialKey = property.id; } } // Change the name of the property to the id property.name = property.id; // Create the property id based on the targetId if (targetId) { property.id = targetId + '_' + property.id; } // Check if variant="autocomplete" exists and create isAutocomplete prop if (property.variant && property.variant === 'autocomplete') { property.isAutoComplete = true; } // Append the property to the target container if (templates.forms.hasOwnProperty(property.type)) { // New field const $newField = $(templates.forms[property.type]( Object.assign( {}, property, { trans: (typeof propertiesPanelTrans === 'undefined') ? {} : propertiesPanelTrans, }, ), )); // Target to append to let $targetContainer = $(targetContainer); // Check if the property has a group if (property.propertyGroupId) { // Get group object from propertyGroups const group = propertyGroups.find( (group) => group.id === property.propertyGroupId, ); // Only add to group if it exists if (group) { // Check if the group already exists in the DOM, if not create it if ( $(targetContainer).find('#' + property.propertyGroupId).length ) { // Set target container to be the group $targetContainer = $(targetContainer) .find('#' + property.propertyGroupId + ' .field-container'); } else { // Create the group and add it to the target container $targetContainer.append( $(templates.forms.group({ id: group.id, title: group.title, helpText: group.helpText, expanded: group.expanded, })), ); // Set target container to be the group field container $targetContainer = $( $(targetContainer).find('#' + property.propertyGroupId), ).find('.field-container'); } } } // Append the new field to the target container if (prepend) { $targetContainer.prepend($newField); } else { $targetContainer.append($newField); } // Handle help text if (property.helpText) { // Only add if not added yet if ( $newField.find('.input-info-container .xibo-help-text') .length === 0 ) { $newField.find('.input-info-container').append( $(templates.forms.addOns.helpText({ helpText: property.helpText, })), ); } } // Handle custom popover if (property.customPopOver) { $newField.find('.input-info-container').append( $(templates.forms.addOns.customPopOver({ content: property.customPopOver, })), ); } // Handle player compatibility if (property.playerCompatibility) { $newField.find('.input-info-container').append( $(templates.forms.addOns.playerCompatibility( property.playerCompatibility, )), ); } // Add date format helper popup when variant = dateFormat if (property.variant && property.variant === 'dateFormat') { $newField.find('label.control-label').append( $(templates.forms.addOns.dateFormatHelperPopup({ content: templates['php-date-format-table'], })), ); // Initialize popover $newField.find('[data-toggle="popover"]').popover({ sanitize: false, trigger: 'manual', }).on('mouseenter', function(ev) { $(ev.currentTarget).popover('show'); }); } // Handle setting value to datasetField if value is defined if (property.type === 'datasetField') { if (property.value !== undefined && String(property.value).length > 0 ) { $newField.find('select').attr('data-value', property.value); } } // Handle depends on property if not already set if ( property.dependsOn && !$newField.attr('data-depends-on') ) { $newField.attr('data-depends-on', property.dependsOn); } // Add visibility to the field if not already set if ( property.visibility.length && !$newField.attr('data-visibility') ) { $newField.attr('data-visibility', property.visibility); } // Add required attribute to the field if not already set if ( property?.validation?.tests?.length && !$newField.attr('data-is-required') ) { let added = false; $.each(property.validation.tests, function(index, el) { $.each(el.conditions, function(condIndex, condition) { if (condition?.type === 'required' && !added) { $newField.attr('data-is-required', true); property.isRequired = true; added = true; } }); }); } // Add custom class to the field if not already set if (customClass) { $newField.find('[name]').addClass(customClass); } } else { console.error('Form type not found: ' + property.type); } } } // Initialise tooltips Common.reloadTooltips( $(targetContainer), { position: 'left', }, ); }, /** * Initialise the form fields * @param {string} container - Main container Jquery selector * @param {object} target - Target Jquery selector or object * @param {string} [targetId] - Target Id ( widget, element, etc.) * @param {boolean} [readOnlyMode=false] * - If the properties are element properties */ initFields: function(container, target, targetId, readOnlyMode = false) { const forms = this; // Find elements, either they match // the children of the container or they are the target const findElements = function(selector, target) { if (target) { if ($(target).is(selector)) { return $(target); } else { // Return empty object return $(); } } return $(container).find(selector); }; // Dropdowns findElements( '.dropdown-input-group', target, ).each(function(_k, el) { const $dropdown = $(el).find('select'); // Check if options have a value with data-content and an image // If so, add the image to the dropdown $dropdown.find('option[data-content]').each(function(_k, option) { const $option = $(option); const $dataContent = $($option.data('content')); // Get the image const $image = $dataContent.find('img'); // Replace src with data-src $image.attr('src', assetDownloadUrl.replace(':assetId', $image.attr('src')), ); // Add html back to the option $option.data('content', $dataContent.html()); }); }); // Button group switch findElements( '.button-switch-input-group', target, ).each(function(_k, el) { const $buttonGroup = $(el).find('.btn-group'); const $hiddenInput = $(el).find('input[type="hidden"]'); $buttonGroup.find('button').on('click', function(ev) { const $button = $(ev.currentTarget); // Deselect all other buttons $buttonGroup.find('button') .removeClass('selected'); // Add selected to target button $button.addClass('selected'); // Update hidden input with chosen button value $hiddenInput.val($button.data('value')) .trigger('change'); }); }); // Dataset order clause findElements( '.dataset-order-clause', target, ).each(function(_k, el) { const $el = $(el); const datasetId = $el.data('depends-on-value'); // Initialise the dataset order clause // if the dataset id is not empty if (datasetId) { // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, }).done(function(data) { // Get the columns const datasetCols = data.data[0].columns; // Order Clause const $orderClauseFields = $el.find('.order-clause-container'); if ($orderClauseFields.length == 0) { return; } const $orderClauseHiddenInput = $el.find('#input_' + $el.data('order-id')); const orderClauseValues = $orderClauseHiddenInput.val() ? JSON.parse( $orderClauseHiddenInput.val(), ) : []; // Update the hidden field with a JSON string // of the order clauses const updateHiddenField = function() { const orderClauses = []; $orderClauseFields.find('.order-clause-row').each(function( _index, el, ) { const $el = $(el); const orderClause = $el.find('.order-clause').val(); const orderClauseDirection = $el.find('.order-clause-direction').val(); if (orderClause) { orderClauses.push({ orderClause: orderClause, orderClauseDirection: orderClauseDirection, }); } }); // Update the hidden field with a JSON string $orderClauseHiddenInput.val(JSON.stringify(orderClauses)) .trigger('change'); }; // Clear existing fields $orderClauseFields.empty(); // Get template const orderClauseTemplate = formHelpers.getTemplate('dataSetOrderClauseTemplate'); const ascTitle = datasetQueryBuilderTranslations.ascTitle; const descTitle = datasetQueryBuilderTranslations.descTitle; if (orderClauseValues.length == 0) { // Add a template row const context = { columns: datasetCols, title: '1', orderClause: '', orderClauseAsc: '', orderClauseDesc: '', buttonGlyph: 'fa-plus', ascTitle: ascTitle, descTitle: descTitle, }; $orderClauseFields.append(orderClauseTemplate(context)); } else { // For each of the existing codes, create form components let i = 0; $.each(orderClauseValues, function(_index, field) { i++; const direction = (field.orderClauseDirection == 'ASC'); const context = { columns: datasetCols, title: i, orderClause: field.orderClause, orderClauseAsc: direction, orderClauseDesc: !direction, buttonGlyph: ((i == 1) ? 'fa-plus' : 'fa-minus'), ascTitle: ascTitle, descTitle: descTitle, }; $orderClauseFields.append(orderClauseTemplate(context)); }); } // Show only on non read only mode if (!readOnlyMode) { // Nabble the resulting buttons $orderClauseFields.on('click', 'button', function(e) { e.preventDefault(); // find the gylph if ($(e.currentTarget).find('i').hasClass('fa-plus')) { const context = { columns: datasetCols, title: $orderClauseFields.find('.form-inline').length + 1, orderClause: '', orderClauseAsc: '', orderClauseDesc: '', buttonGlyph: 'fa-minus', ascTitle: ascTitle, descTitle: descTitle, }; $orderClauseFields.append(orderClauseTemplate(context)); } else { // Remove this row $(e.currentTarget).closest('.form-inline').remove(); } updateHiddenField(); }); // Update the hidden field when the order clause changes $el.on('change', 'select:not([type="hidden"])', function() { updateHiddenField(); }); } else { forms.makeFormReadOnly($el); } }).fail(function(jqXHR, textStatus, errorThrown) { console.error(jqXHR, textStatus, errorThrown); }); } }); // Dataset column selector findElements( '.dataset-column-selector', target, ).each(function(_k, el) { const $el = $(el); const datasetId = $el.data('depends-on-value'); // Initialise the dataset column selector // if the dataset id is not empty if (datasetId) { // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, }).done(function(data) { // Get the columns const datasetCols = data.data[0].columns; // Order Clause const $colsOutContainer = $el.find('#columnsOut'); const $colsInContainer = $el.find('#columnsIn'); if ($colsOutContainer.length == 0 || $colsInContainer.length == 0) { return; } const $selectHiddenInput = $el.find('#input_' + $el.data('select-id')); const selectedValue = $selectHiddenInput.val() ? JSON.parse( $selectHiddenInput.val(), ) : []; // Update the hidden field with a JSON string // of the order clauses const updateHiddenField = function(skipSave = false) { const selectedCols = []; $colsInContainer.find('li').each(function(_index, el) { const colId = $(el).attr('id'); selectedCols.push(Number(colId)); }); // Delete all temporary fields $el.find('.temp').remove(); // Create a hidden field for each of the selected columns $.each(selectedCols, function(_index, col) { $el.append( '', ); }); // Update the hidden field with a JSON string $selectHiddenInput.val(JSON.stringify(selectedCols)) .trigger( 'change', [{ skipSave: skipSave, }]); }; // Clear existing fields $colsOutContainer.empty(); $colsInContainer.empty(); const colAvailableTitle = datasetColumnSelectorTranslations.colAvailable; const colSelectedTitle = datasetColumnSelectorTranslations.colSelected; // Set titles $el.find('.col-out-title').text(colAvailableTitle); $el.find('.col-in-title').text(colSelectedTitle); // Get the selected columns const datasetColsOut = []; const datasetColsIn = []; // If the column is in the dataset // add it to the selected columns // if not add it to the remaining columns $.each(datasetCols, function(_index, col) { if (selectedValue.includes(col.dataSetColumnId)) { datasetColsIn.push(col); } else { datasetColsOut.push(col); } }); // Populate the available columns const $columnsOut = $el.find('#columnsOut'); $.each(datasetColsOut, function(_index, col) { $columnsOut.append( '
  • ' + col.heading + '
  • ', ); }); // Populate the selected columns const $columnsIn = $el.find('#columnsIn'); $.each(datasetColsIn, function(_index, col) { $columnsIn.append( '
  • ' + col.heading + '
  • ', ); }); if (!readOnlyMode) { // Setup lists drag and sort ( with double click ) $el.find('#columnsIn, #columnsOut').sortable({ connectWith: '.connectedSortable', dropOnEmpty: true, update: function() { updateHiddenField(); }, }).disableSelection(); // Double click to switch lists $el.find('.li-sortable').on('dblclick', function(ev) { const $this = $(ev.currentTarget); $this.appendTo($this.parent().is('#columnsIn') ? $columnsOut : $columnsIn); updateHiddenField(); }); } // Update hidden field on start updateHiddenField(true); }).fail(function(jqXHR, textStatus, errorThrown) { console.error(jqXHR, textStatus, errorThrown); }); } }); // Dataset field selector findElements( '.dataset-field-selector', target, ).each(function(_k, el) { const $el = $(el); const $select = $el.find('select'); const $dataTypeId = $(container).find('input[name="dataTypeId"]'); const datasetId = $el.data('depends-on-value'); const getFieldItems = function(fields, search) { return fields.reduce(function(cols, col) { const searchValue = String(search); let searchValues = []; if (searchValue.indexOf(',') !== -1) { searchValues = searchValue.split(','); } else { searchValues = [searchValue]; } if (searchValues.indexOf(String(col.dataTypeId)) !== -1) { return [...cols, col]; } return cols; }, []); }; // Initialise the dataset fields // if the dataset id is not empty if (datasetId) { $el.show(); // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, }).done(function(data) { // Get the columns const datasetCols = data.data[0].columns || []; if ($dataTypeId.length > 0 && datasetCols.length > 0) { // Find columns with given dataTypeId const fieldItems = getFieldItems(datasetCols, $dataTypeId.val()) || []; if (fieldItems.length > 0) { $select.find('option[value]').remove(); $.each(fieldItems, function(_k, field) { $select.append( $('', ), ); }); if ($select.data('value') !== undefined) { // Trigger change event $select.val($select.data('value')) .trigger('change', [{ skipSave: true, }]); } else if (fieldItems.length === 1) { // Set default value when 1 option exists $select.val(fieldItems[0].heading).trigger('change'); } else if (fieldItems.length > 1) { // Set default value to first option $select.val(fieldItems[0].heading).trigger('change'); } } else { $el.hide(); } } }); } }); // Dataset filter clause findElements( '.dataset-filter-clause', target, ).each(function(_k, el) { const $el = $(el); const datasetId = $el.data('depends-on-value'); // Initialise the dataset filter clause // if the dataset id is not empty if (datasetId) { // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, }).done(function(data) { // Get the columns const datasetCols = data.data[0].columns; // Filter Clause const $filterClauseFields = $el.find('.filter-clause-container'); if ($filterClauseFields.length == 0) { return; } const $filterClauseHiddenInput = $el.find('#input_' + $el.data('filter-id')); const filterClauseValues = $filterClauseHiddenInput.val() ? JSON.parse( $filterClauseHiddenInput.val(), ) : []; // Update the hidden field with a JSON string // of the filter clauses const updateHiddenField = function() { const filterClauses = []; $filterClauseFields.find('.filter-clause-row').each(function( _index, el, ) { const $el = $(el); const filterClause = $el.find('.filter-clause').val(); const filterClauseOperator = $el.find('.filter-clause-operator').val(); const filterClauseCriteria = $el.find('.filter-clause-criteria').val(); const filterClauseValue = $el.find('.filter-clause-value').val(); if (filterClause) { filterClauses.push({ filterClause: filterClause, filterClauseOperator: filterClauseOperator, filterClauseCriteria: filterClauseCriteria, filterClauseValue: filterClauseValue, }); } }); // Update the hidden field with a JSON string $filterClauseHiddenInput.val(JSON.stringify(filterClauses)) .trigger('change'); }; // Clear existing fields $filterClauseFields.empty(); // Get template const filterClauseTemplate = formHelpers.getTemplate('dataSetFilterClauseTemplate'); const filterOptions = datasetQueryBuilderTranslations.filterOptions; const filterOperatorOptions = datasetQueryBuilderTranslations.filterOperatorOptions; if (filterClauseValues.length == 0) { // Add a template row const context = { columns: datasetCols, filterOptions: filterOptions, filterOperatorOptions: filterOperatorOptions, title: '1', filterClause: '', filterClauseOperator: 'AND', filterClauseCriteria: '', filterClauseValue: '', buttonGlyph: 'fa-plus', }; $filterClauseFields.append(filterClauseTemplate(context)); } else { // For each of the existing codes, create form components let j = 0; $.each(filterClauseValues, function(_index, field) { j++; const context = { columns: datasetCols, filterOptions: filterOptions, filterOperatorOptions: filterOperatorOptions, title: j, filterClause: field.filterClause, filterClauseOperator: field.filterClauseOperator, filterClauseCriteria: field.filterClauseCriteria, filterClauseValue: field.filterClauseValue, buttonGlyph: ((j == 1) ? 'fa-plus' : 'fa-minus'), }; $filterClauseFields.append(filterClauseTemplate(context)); }); } // Show only on non read only mode if (!readOnlyMode) { // Nabble the resulting buttons $filterClauseFields.on('click', 'button', function(e) { e.preventDefault(); // find the gylph if ($(e.currentTarget).find('i').hasClass('fa-plus')) { const context = { columns: datasetCols, filterOptions: filterOptions, filterOperatorOptions: filterOperatorOptions, title: $filterClauseFields.find('.form-inline').length + 1, filterClause: '', filterClauseOperator: 'AND', filterClauseCriteria: '', filterClauseValue: '', buttonGlyph: 'fa-minus', }; $filterClauseFields.append(filterClauseTemplate(context)); } else { // Remove this row $(e.currentTarget).closest('.form-inline').remove(); } updateHiddenField(); }); // Update the hidden field when the filter clause changes $el.on('change', 'select:not([type="hidden"]), input:not([type="hidden"])', function(ev) { updateHiddenField(); }); } else { forms.makeFormReadOnly($el); } }).fail(function(jqXHR, textStatus, errorThrown) { console.error(jqXHR, textStatus, errorThrown); }); } }); // Article tag selector findElements( '.ticker-tag-selector', target, ).each(function(_k, el) { const $el = $(el); // Get the columns const tickerTagsMap = { title: 'text', summary: 'text', content: 'text', author: 'text', permalink: 'text', link: 'text', date: 'date', publishedDate: 'date', image: 'image', }; // Tag containers const $tagsOutContainer = $el.find('#tagsOut'); const $tagsInContainer = $el.find('#tagsIn'); if ($tagsOutContainer.length == 0 || $tagsInContainer.length == 0) { return; } const $selectHiddenInput = $el.find('#input_' + $el.data('select-id')); const tagsValue = $selectHiddenInput.val() ? JSON.parse( $selectHiddenInput.val(), ) : []; const selectedTags = Object.keys(tagsValue); // Update the hidden field with a JSON string // of the tags const updateHiddenField = _.debounce(function(skipSave = false) { const selectedTags = []; const tagsValue = $selectHiddenInput.val() ? JSON.parse( $selectHiddenInput.val(), ) : []; $tagsInContainer.find('li').each(function(_index, tagEl) { const tagId = $(tagEl).attr('id'); selectedTags.push(tagId); }); // Clear all previous style controls $el.find('.ticker-tag-styles .ticker-tag-style').remove(); // Add styles for each selected tag selectedTags.forEach((tag) => { const $newStyle = $(templates.forms.tickerTagStyle({ id: tag, type: tickerTagsMap[tag], value: tagsValue[tag], trans: tickerTagSelectorTranslations, fontsSearchUrl: getFontsUrl + '?length=10000', })); $newStyle.appendTo($el.find('.ticker-tag-styles')); // Update hidden field on input change $newStyle.off().on('change inputChange', updateTagsStylesHiddenField); }); // Init fields forms.initFields($el.find('.ticker-tag-styles')); updateTagsStylesHiddenField(); }, 100); const updateTagsStylesHiddenField = _.debounce(function() { const selectedTagStylesValue = {}; $el.find('.ticker-tag-style').each(function(_index, tagContainer) { const $container = $(tagContainer); const tagId = $container.data('component-id'); selectedTagStylesValue[tagId] = {}; const $tagProperties = $(tagContainer).find('.ticker-tag-style-property'); // save tag type selectedTagStylesValue[tagId]['tagType'] = tickerTagsMap[tagId]; $tagProperties.find('select, input').each(function(_index, tagInput) { const inputType = $(tagInput).data('tag-style-input'); const value = ($(tagInput).attr('type') === 'checkbox') ? $(tagInput).is(':checked') : $(tagInput).val(); selectedTagStylesValue[tagId][inputType] = value; }); }); // Update the hidden field with a JSON string $selectHiddenInput.val(JSON.stringify(selectedTagStylesValue)) .trigger('change'); }, 100); // Clear existing fields $tagsOutContainer.empty(); $tagsInContainer.empty(); const tagAvailableTitle = tickerTagSelectorTranslations.tagAvailable; const tagSelectedTitle = tickerTagSelectorTranslations.tagSelected; // Set titles $el.find('.col-out-title').text(tagAvailableTitle); $el.find('.col-in-title').text(tagSelectedTitle); // Get the selected tags const tickerTagsOut = []; const tickerTagsIn = []; // If the column is in the dataset // add it to the selected tags // if not add it to the remaining tags $.each(Object.keys(tickerTagsMap), function(_index, tag) { if (selectedTags.includes(tag)) { tickerTagsIn.push(tag); } else { tickerTagsOut.push(tag); } }); // Populate the available tags const $tagsOut = $el.find('#tagsOut'); $.each(tickerTagsOut, function(_index, tag) { const $tag = $( '
  • ' + tickerTagSelectorTranslations[tag] + '
  • ', ).data('tag-type', tickerTagsMap[tag]); $tagsOut.append($tag); }); // Populate the selected tags const $tagsIn = $el.find('#tagsIn'); $.each(tickerTagsIn, function(_index, tag) { const $tag = $( '
  • ' + tickerTagSelectorTranslations[tag] + '
  • ', ); $tagsIn.append($tag); }); if (!readOnlyMode) { // Setup lists drag and sort ( with double click ) $el.find('#tagsIn, #tagsOut').sortable({ connectWith: '.connectedSortable', dropOnEmpty: true, receive: updateHiddenField, update: updateHiddenField, }).disableSelection(); // Double click to switch lists $el.find('.li-sortable').on('dblclick', function(ev) { const $this = $(ev.currentTarget); $this.appendTo($this.parent().is('#tagsIn') ? $tagsOut : $tagsIn); updateHiddenField(); }); } // Update hidden field on start updateHiddenField(true); }); // Dataset col selector findElements( '.dataset-column-style-selector', target, ).each(function(_k, el) { const $el = $(el); const datasetId = $el.data('depends-on-value'); // Initialise the dataset column style selector // if the dataset id is not empty if (datasetId) { // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, }).done(function(data) { // Get the columns const datasetCols = data.data[0].columns; const datasetTypeMap = { 3: 'date', 5: 'image', }; const getColStyle = function(col) { return (datasetTypeMap[col]) ? datasetTypeMap[col] : 'text'; }; const getColById = function(colId) { return datasetCols.find((col) => col.dataSetColumnId == colId); }; // Column containers const $colsOutContainer = $el.find('#colsOut'); const $colsInContainer = $el.find('#colsIn'); if ($colsOutContainer.length == 0 || $colsInContainer.length == 0) { return; } const $selectHiddenInput = $el.find('#input_' + $el.data('select-id')); const colsValue = $selectHiddenInput.val() ? JSON.parse( $selectHiddenInput.val(), ) : []; const selectedCols = Object.keys(colsValue); // Update the hidden field with a JSON string // of the columns const updateHiddenField = _.debounce(function(skipSave = false) { const selectedCols = []; const colsValue = $selectHiddenInput.val() ? JSON.parse( $selectHiddenInput.val(), ) : []; $colsInContainer.find('li').each(function(_index, colEl) { const colId = $(colEl).attr('id'); const colType = getColStyle($(colEl).data('colType')); const colObj = getColById(colId); selectedCols.push({ id: colId, type: colType, name: colObj.heading, }); }); // Clear all previous style controls $el.find('.dataset-column-styles .dataset-col-style').remove(); // Add styles for each selected col selectedCols.forEach((col) => { const $newStyle = $(templates.forms.datasetColStyle({ id: col.id, type: col.type, name: col.name, value: colsValue[col.id], trans: datasetColStyleSelectorTranslations, fontsSearchUrl: getFontsUrl + '?length=10000', })); $newStyle.appendTo($el.find('.dataset-column-styles')); // Update hidden field on input change $newStyle.off().on( 'change inputChange', function() { updateColStylesHiddenField(); }, ); }); // Init fields forms.initFields($el.find('.dataset-column-styles')); updateColStylesHiddenField(); }, 100); const updateColStylesHiddenField = _.debounce(function() { const selectedColStylesValue = {}; let playOrder = 0; $el.find('.dataset-col-style').each(function(_index, colContainer) { const $container = $(colContainer); const colId = $container.data('component-id'); selectedColStylesValue[colId] = {}; const $colProperties = $(colContainer).find('.dataset-col-style-property'); // set play order selectedColStylesValue[colId].playOrder = playOrder; playOrder++; // save colId selectedColStylesValue[colId].colId = colId; $colProperties.find('select, input') .each(function(_index, colsInput) { const inputType = $(colsInput).data('dataset-col-style-input'); const value = ($(colsInput).attr('type') === 'checkbox') ? $(colsInput).is(':checked') : $(colsInput).val(); selectedColStylesValue[colId][inputType] = value; }); }); // Update the hidden field with a JSON string $selectHiddenInput.val(JSON.stringify(selectedColStylesValue)) .trigger('change'); }, 100); // Clear existing fields $colsOutContainer.empty(); $colsInContainer.empty(); const colsAvailableTitle = datasetColStyleSelectorTranslations.colAvailable; const colsSelectedTitle = datasetColStyleSelectorTranslations.colSelected; // Set titles $el.find('.col-out-title').text(colsAvailableTitle); $el.find('.col-in-title').text(colsSelectedTitle); // Get the selected cols const colsOut = []; const colsIn = []; // If the column is in the dataset // add it to the selected cols // if not add it to the remaining cols $.each(Object.keys(datasetCols), function(_index, col) { const colId = datasetCols[col].dataSetColumnId; if (selectedCols.find((column) => colId == column)) { colsIn.push(colId); } else { colsOut.push(colId); } }); // Populate the available columns const $colsOut = $el.find('#colsOut'); $.each(colsOut, function(_index, col) { const columnObj = getColById(col); const $col = $( '
  • ' + columnObj.heading + '
  • ', ).data('col-type', columnObj.dataTypeId); $colsOut.append($col); }); // Populate the selected columns const $colsIn = $el.find('#colsIn'); $.each(colsIn, function(_index, col) { const columnObj = getColById(col); const $col = $( '
  • ' + columnObj.heading + '
  • ', ); $colsIn.append($col); }); if (!readOnlyMode) { // Setup lists drag and sort ( with double click ) $el.find('#colsIn, #colsOut').sortable({ connectWith: '.connectedSortable', dropOnEmpty: true, receive: updateHiddenField, update: updateHiddenField, }).disableSelection(); // Double click to switch lists $el.find('.li-sortable').on('dblclick', function(ev) { const $this = $(ev.currentTarget); $this.appendTo($this.parent().is('#colsIn') ? $colsOut : $colsIn); updateHiddenField(); }); } // Update hidden field on start updateHiddenField(true); }).fail(function(jqXHR, textStatus, errorThrown) { console.error(jqXHR, textStatus, errorThrown); }); } }); findElements( '.languageSelect', target, ).each(function(_k, el) { const $select = $(el).find('select.form-control'); const isMomentLocaleSelect = $(el).hasClass('momentLocales'); if (isMomentLocaleSelect) { const momentLocales = moment.locales().sort(); const selectValue = $select.val(); // Remove all previous options $select.find('option').remove(); // Add empty option to the start of the array momentLocales.unshift(''); // Add options from moment for (let index = 0; index < momentLocales.length; index++) { const locale = momentLocales[index]; // Get translation if exists const name = momentLocalesTrans[locale] || locale; // Create new option const $newOption = $(``); // Check if it's selected if (selectValue == locale) { $newOption.prop('selected', true); } $newOption.appendTo($select); } } makeLocalSelect($select, container); }); // Playlist mixer findElements( '.playlist-mixer', target, ).each(function(_k, el) { const $el = $(el); /** * Initialise a new row * @param {object} $form * @param {object} $row */ function subplaylistInitRow($form, $row) { const $select = $row.find('.subplaylist-id'); // Get the initial value. if ($select.data('fieldId')) { $.ajax({ method: 'GET', url: urlsForApi.playlist.get.url + '?playlistId=' + $select.data('fieldId'), success: function(response) { if (response.data && response.data.length > 0) { // Append our initial option $select.append(''); subplaylistInitSelect2($form, $select); } else { // No permissions. $select.parent().append( '' + '' + ' ' + playlistMixerTranslations.playlistId + ' ' + $select.data('fieldId') + ''); $select.remove(); } // Disable fields on non read only mode if (readOnlyMode) { forms.makeFormReadOnly($form); } }, error: function() { $select.parent().append( '{% trans "An unknown error has occurred. Please refresh" %}', ); }, }); } else { subplaylistInitSelect2($form, $select); } $row.find('select[name="spotFill[]"]').select2({ templateResult: function(state) { if (!state.id) { return state.text; } return $(state.element).data().templateResult; }, dropdownAutoWidth: true, minimumResultsForSearch: -1, }); } /** * Initialise the select2 * @param {object} $form * @param {object} $el */ function subplaylistInitSelect2($form, $el) { $el.select2({ dropdownAutoWidth: true, ajax: { url: urlsForApi.playlist.get.url + '?notPlaylistId=' + $form.data('playlistId'), dataType: 'json', delay: 250, data: function(params) { const query = { start: 0, length: 10, name: params.term, }; if (params.page != null) { query.start = (params.page - 1) * 10; } return query; }, processResults: function(data, params) { const results = []; $.each(data.data, function(index, el) { results.push({ id: el.playlistId, text: el.name, }); }); let page = params.page || 1; page = (page > 1) ? page - 1 : page; if (page === 1 && results.length <= 0) { $form.find('.sub-playlist-no-playlists-message') .removeClass('d-none'); } return { results: results, pagination: { more: (page * 10 < data.recordsTotal), }, }; }, }, }); } const $mixerHiddenInput = $el.find('#input_' + $el.data('mixer-id')); const mixerValues = $mixerHiddenInput.val() ? JSON.parse( $mixerHiddenInput.val(), ) : []; // Filter Clause const $playlistItemsContainer = $el.find('.mixer-playlist-container'); if ($playlistItemsContainer.length == 0) { return; } // Update the hidden field with a JSON string // of the filter clauses const updateHiddenField = function() { const mixerItems = []; $playlistItemsContainer.find('.subplaylist-item-row').each(function( _index, el, ) { const $el = $(el); const playlistId = $el.find('.subplaylist-id').val(); const spots = $el.find('.subplaylist-spots').val(); const spotLength = $el.find('.subplaylist-spots-length').val(); const spotFill = $el.find('.subplaylist-spots-fill').val(); if (playlistId) { mixerItems.push({ rowNo: _index + 1, playlistId: playlistId, spots: spots, spotLength: spotLength, spotFill: spotFill, }); } }); // Update the hidden field with a JSON string $mixerHiddenInput.val(JSON.stringify(mixerItems)) .trigger('xiboInputChange'); }; // Clear existing fields $playlistItemsContainer.empty(); // Add header template const containerTemplate = formHelpers.getTemplate('subPlaylistContainerTemplate'); $playlistItemsContainer.append(containerTemplate({ trans: playlistMixerTranslations, })); // Get template const subPlaylistFormTemplate = formHelpers.getTemplate('subPlaylistFormTemplate'); if (mixerValues.length == 0) { // Add a template row const context = { playlistId: '', spots: '', spotLength: '', spotFill: '', fillTitle: playlistMixerTranslations.fillTitle, padTitle: playlistMixerTranslations.padTitle, repeatTitle: playlistMixerTranslations.repeatTitle, fillHelpText: playlistMixerTranslations.fillHelpText, padHelpText: playlistMixerTranslations.padHelpText, repeatHelpText: playlistMixerTranslations.repeatHelpText, }; $playlistItemsContainer.find('.subplaylist-items-content') .append(subPlaylistFormTemplate(context)); } else { // For each of the existing codes, create form components $.each(mixerValues, function(_index, field) { const context = { playlistId: field.playlistId, spots: field.spots, spotLength: field.spotLength, spotFill: field.spotFill, fillTitle: playlistMixerTranslations.fillTitle, padTitle: playlistMixerTranslations.padTitle, repeatTitle: playlistMixerTranslations.repeatTitle, fillHelpText: playlistMixerTranslations.fillHelpText, padHelpText: playlistMixerTranslations.padHelpText, repeatHelpText: playlistMixerTranslations.repeatHelpText, }; $playlistItemsContainer.find('.subplaylist-items-content') .append(subPlaylistFormTemplate(context)); }); } // Add or remove playlist item $playlistItemsContainer.on('click', '.subplaylist-item-btn', function(e) { e.preventDefault(); // find the gylph if ($(e.currentTarget).find('i').hasClass('fa-plus')) { const context = { playlistId: '', spots: '', spotLength: '', subPlaylistIdSpotFill: '', fillTitle: playlistMixerTranslations.fillTitle, padTitle: playlistMixerTranslations.padTitle, repeatTitle: playlistMixerTranslations.repeatTitle, fillHelpText: playlistMixerTranslations.fillHelpText, padHelpText: playlistMixerTranslations.padHelpText, repeatHelpText: playlistMixerTranslations.repeatHelpText, }; const $newRow = $(subPlaylistFormTemplate(context)) .appendTo( $playlistItemsContainer.find('.subplaylist-items-content'), ); // Initialise row subplaylistInitRow($el, $newRow); } else { // Remove this row $(e.currentTarget).closest('.subplaylist-item-row').remove(); } updateHiddenField(); }); // Initialise all rows $playlistItemsContainer.find('.subplaylist-item-row') .each(function(_index, element) { subplaylistInitRow($el, $(element)); }); // Update the hidden field when the item changes $el.on('change', 'select, input', function() { updateHiddenField(); }); // Make the playlist items sortable $playlistItemsContainer.sortable({ axis: 'y', items: '.subplaylist-item-row', handle: '.subplaylist-item-sort', containment: 'parent', update: function() { updateHiddenField(); }, }); }); // Code editor findElements( '.xibo-code-input', target, ).each(function(_k, el) { const $container = $(el); const $textArea = $container.find('.code-input'); const inputValue = $textArea.val(); const codeType = $textArea.data('codeType'); let valueChanged = false; // Get code type, defaults to HTML const codeTypeFunc = (CodeMirror[codeType]) ? CodeMirror[codeType] : CodeMirror.html; new CodeMirror.EditorView({ doc: inputValue, extensions: [ CodeMirror.basicSetup, CodeMirror.keymap.of([CodeMirror.indentWithTab]), codeTypeFunc(), CodeMirror.EditorView.updateListener.of((update) => { if ( update.docChanged && update.state.doc.toString() != $textArea.val() ) { $textArea.val(update.state.doc.toString()).trigger('inputChange'); // Trigger to enable auto save on element blur $textArea.trigger('editorFocus'); // Mark value as changed valueChanged = true; } // If value and focus changed if (valueChanged && update.focusChanged) { valueChanged = false; $textArea.trigger('change'); } }), ], parent: $container.find('.code-input-editor')[0], }); // Expand functionality const $codeEditor = $container.find('.code-input-editor-container'); const toggleEditor = function() { const isOpened = $codeEditor.hasClass('code-input-fs'); const codeEditorHeight = $codeEditor.height(); // Toggle class $codeEditor.toggleClass('code-input-fs'); // If it's opened if (isOpened) { // Remove overlay $('.code-fs-overlay').remove(); // Move editor back to original container $codeEditor.appendTo($container); // Remove placeholder $container.find('.code-fs-placeholder').remove(); } else { // Add overlay const $codeFSOverlay = $('.custom-overlay:first').clone(); $codeFSOverlay .removeClass('custom-overlay') .addClass('code-fs-overlay custom-overlay-clone') .appendTo($('body')) .on('click', toggleEditor); $codeFSOverlay .show(); // Move editor to body $codeEditor.appendTo($('body')); // Add placeholder const $codeFSPlaceholder = $('
    ') .addClass('code-fs-placeholder') .css('height', codeEditorHeight); $codeFSPlaceholder.appendTo($container); } }; $container.find('.code-input-fs-btn').on('click', toggleEditor); }); // Colour picker findElements( '.colorpicker-input.colorpicker-form-element', target, ).each(function(_k, el) { // Init the colour picker $(el).colorpicker({ container: $(el).find('.picker-container'), align: 'left', format: ($(el).data('colorFormat') !== undefined) ? $(el).data('colorFormat') : false, }); const $inputElement = $(el).find('input'); $inputElement.on('focusout', function() { // If the input is empty, set the default value // or clear the color preview if ($inputElement.val() == '') { // If we have a default value if ($(el).data('default') !== undefined) { const defaultValue = $(el).data('default'); $(el).colorpicker('setValue', defaultValue); } else { // Clear the color preview $(el).find('.input-group-addon').css('background-color', ''); } } }); }); // Colour gradient findElements( '.color-gradient', target, ).each(function(_k, el) { // Init the colour pickers $(el).find('.colorpicker-input.colorpicker-form-element').colorpicker({ container: $(el).find('.picker-container'), align: 'left', format: ($(el).data('colorFormat') !== undefined) ? $(el).data('colorFormat') : false, }); // Defaults const defaults = { color1: '#222', color2: '#eee', angle: 0, }; const $inputElements = $(el).find('[name]:not(.color-gradient-hidden)'); const $hiddenInput = $(el).find('.color-gradient-hidden'); const $gradientType = $inputElements.filter('[name="gradientType"]'); const $gradientAngle = $inputElements.filter('[name="gradientAngle"]'); const $gradientColor1 = $inputElements.filter('[name="gradientColor1"]'); const $gradientColor2 = $inputElements.filter('[name="gradientColor2"]'); // Load values into inputs if ($hiddenInput.val() != '') { const initialValue = JSON.parse($hiddenInput.val()); $gradientType.val(initialValue.type).trigger('change'); $gradientColor1.parents('.colorpicker-input') .colorpicker('setValue', initialValue.color1); $gradientColor2.parents('.colorpicker-input') .colorpicker('setValue', initialValue.color2); $gradientAngle.val(initialValue.angle); } // Update fields visibility and default values const updateFields = function() { if ($gradientColor1.val() == '') { $gradientColor1.parents('.colorpicker-input') .colorpicker('setValue', defaults.color1); } if ($gradientColor2.val() == '') { $gradientColor2.parents('.colorpicker-input') .colorpicker('setValue', defaults.color2); } if ($gradientAngle.val() == '') { $gradientAngle.val(defaults.angle); } $gradientAngle.parent().toggle($gradientType.val() === 'linear'); }; // Mark inputs to skip saving in properties panel $inputElements.addClass('skip-save'); // When changing the inputs, save to hidden input $inputElements.on('change', function() { const gradientType = $gradientType.val(); const color1Val = $gradientColor1.val(); const color2Val = $gradientColor2.val(); const angleVal = $gradientAngle.val(); // Gradient object to be saved const gradient = { type: gradientType, color1: (color1Val != '') ? color1Val : defaults.color1, color2: (color2Val != '') ? color2Val : defaults.color2, }; // If gradient type is linear, save angle if (gradientType === 'linear') { gradient.angle = (angleVal != '') ? angleVal : defaults.angle; } updateFields(); // Save value to hidden input $hiddenInput.val(JSON.stringify(gradient)).trigger('change'); }); updateFields(); }); // Date picker - date only findElements( '.dateControl.date:not(.datePickerHelper)', target, ).each(function(_k, el) { if (calendarType == 'Jalali') { initDatePicker( $(el), systemDateFormat, jsDateOnlyFormat, { altFieldFormatter: function(unixTime) { const newDate = moment.unix(unixTime / 1000); newDate.set('hour', 0); newDate.set('minute', 0); newDate.set('second', 0); return newDate.format(systemDateFormat); }, }, ); } else { initDatePicker( $(el), systemDateFormat, jsDateOnlyFormat, ); } }); // Date picker - date and time findElements( '.dateControl.dateTime:not(.datePickerHelper)', target, ).each(function(_k, el) { const enableSeconds = dateFormat.includes('s'); const enable24 = !dateFormat.includes('A'); if (calendarType == 'Jalali') { initDatePicker( $(el), systemDateFormat, jsDateFormat, { timePicker: { enabled: true, second: { enabled: enableSeconds, }, }, }, ); } else { initDatePicker( $(el), systemDateFormat, jsDateFormat, { enableTime: true, time_24hr: enable24, enableSeconds: enableSeconds, altFormat: $(el).data('customFormat') ? $(el).data('customFormat') : jsDateFormat, }, ); } }); // Date picker - month only findElements( '.dateControl.month:not(.datePickerHelper)', target, ).each(function(_k, el) { if (calendarType == 'Jalali') { initDatePicker( $(el), systemDateFormat, jsDateFormat, { format: $(el).data('customFormat') ? $(el).data('customFormat') : 'MMMM YYYY', viewMode: 'month', dayPicker: { enabled: false, }, altFieldFormatter: function(unixTime) { const newDate = moment.unix(unixTime / 1000); newDate.set('date', 1); newDate.set('hour', 0); newDate.set('minute', 0); newDate.set('second', 0); return newDate.format(systemDateFormat); }, }, ); } else { initDatePicker( $(el), systemDateFormat, jsDateFormat, { plugins: [new flatpickrMonthSelectPlugin({ shorthand: false, dateFormat: systemDateFormat, altFormat: $(el).data('customFormat') ? $(el).data('customFormat') : 'MMMM Y', parseDate: function(datestr, format) { return moment(datestr, format, true).toDate(); }, formatDate: function(date, format, locale) { return moment(date).format(format); }, })], }, ); } }); // Date picker - time only findElements( '.dateControl.time:not(.datePickerHelper)', target, ).each(function(_k, el) { const enableSeconds = dateFormat.includes('s'); if (calendarType == 'Jalali') { initDatePicker( $(el), systemTimeFormat, jsTimeFormat, { onlyTimePicker: true, format: jsTimeFormat, timePicker: { second: { enabled: enableSeconds, }, }, altFieldFormatter: function(unixTime) { const newDate = moment.unix(unixTime / 1000); newDate.set('second', 0); return newDate.format(systemTimeFormat); }, }, ); } else { initDatePicker( $(el), systemTimeFormat, jsTimeFormat, { enableTime: true, noCalendar: true, enableSeconds: enableSeconds, time_24hr: true, altFormat: $(el).data('customFormat') ? $(el).data('customFormat') : jsTimeFormat, }, ); } }); // Rich text input findElements( '.rich-text', target, ).each(function(_k, el) { formHelpers.setupCKEditor( container, $(el).attr('id'), true, null, false, true); }); // World clock control findElements( '.world-clock-control', target, ).each(function(_k, el) { // Get clocks container const $clocksContainer = $(el).find('.clocksContainer'); // Get hidden input const $hiddenInput = $(el).find('.world-clock-value'); /** * Configure the multiple world clock form * @param {*} container * @return {void} */ function configureMultipleWorldClocks(container) { if (container.length == 0) { return; } const worldClockTemplate = formHelpers.getTemplate('worldClockTemplate'); const worldClocks = $hiddenInput.attr('value') ? JSON.parse($hiddenInput.attr('value')) : []; if (worldClocks.length == 0) { // Add a template row const context = { title: '1', clockTimezone: '', timezones: timezones, buttonGlyph: 'fa-plus', }; $(worldClockTemplate(context)).appendTo($clocksContainer); initClockRows(el); } else { // For each of the existing codes, create form components let i = 0; $.each(worldClocks, function(_index, field) { i++; const context = { title: i, clockTimezone: field.clockTimezone, clockHighlight: field.clockHighlight, clockLabel: field.clockLabel, timezones: timezones, buttonGlyph: ((i == 1) ? 'fa-plus' : 'fa-minus'), }; $clocksContainer.append(worldClockTemplate(context)); }); initClockRows(el); } // Nabble the resulting buttons $clocksContainer.on('click', 'button', function(e) { e.preventDefault(); // find the gylph if ($(e.currentTarget).find('i').hasClass('fa-plus')) { const context = { title: $clocksContainer.find('.form-clock').length + 1, clockTimezone: '', timezones: timezones, buttonGlyph: 'fa-minus', }; $clocksContainer.append(worldClockTemplate(context)); initClockRows(el); } else { // Remove this row $(e.currentTarget).closest('.form-clock').remove(); } }); } /** * Update the hidden input with the current clock values * @param {object} container */ function updateClocksHiddenInput(container) { const worldClocks = []; $(container).find('.form-clock').each(function(_k, el2) { // Only add if the timezone is set if ($(el2).find('.localSelect select').val() != '') { worldClocks.push({ clockTimezone: $(el2).find('.localSelect select').val(), clockHighlight: $(el2).find('.clockHighlight').is(':checked'), clockLabel: $(el2).find('.clockLabel').val(), }); } }); // Update the hidden input $hiddenInput.attr('value', JSON.stringify(worldClocks)); } /** * Initialise the select2 elements * @param {object} container */ function initClockRows(container) { // Initialise select2 elements $(container).find('.localSelect select.form-control') .each(function(_k, el2) { makeLocalSelect( $(el2), ($(container).hasClass('modal') ? $(container) : $('body')), ); }); // Update the hidden input when the clock values change $(container).find('input[type="checkbox"]').on('click', function() { updateClocksHiddenInput(container); }); $(container).find('input[type="text"], select') .on('change', function() { updateClocksHiddenInput(container); }); } // Setup multiple clocks configureMultipleWorldClocks($(el)); initClockRows(el); }); // Font selector findElements( '.font-selector', target, ).each(function(_k, el) { // Populate the font list with options const $el = $(el).find('select'); $.ajax({ method: 'GET', url: $el.data('searchUrl'), success: function(res) { if (res.data !== undefined && res.data.length > 0) { $.each(res.data, function(_index, element) { if ($el.data('value') === element.familyName) { $el.append( $('')); // Trigger change event $el.trigger( 'change', [{ skipSave: true, }]); } else { $el.append( $('')); } }); } // Disable fields on non read only mode if (readOnlyMode) { forms.makeFormReadOnly($(el)); } }, }); }); // Snippet selector findElements( '.snippet-selector', target, ).each(function(_k, el) { // Get select element const $select = $(el).find('select'); // Get target field const targetFieldId = $select.data('target'); const $targetField = $('[name=' + targetFieldId + ']'); // Snippet mode const snippetMode = $select.data('mode'); // Set normal snippet const setupSnippets = function($select) { formHelpers.setupSnippetsSelector( $select, function(e) { const value = $(e.currentTarget).val(); // If there is no value, or target field is not found, do nothing if (value == undefined || value == '' || $targetField.length == 0) { return; } // Text to be inserted const text = '[' + value + ']'; // Check if there is a CKEditor instance const ckeditorInstance = formHelpers .getCKEditorInstance('input_' + targetId + '_' + targetFieldId); if (ckeditorInstance) { // CKEditor formHelpers.insertToCKEditor( 'input_' + targetId + '_' + targetFieldId, text, ); } else if ($targetField.hasClass('code-input')) { // Code editor const editor = CodeMirror.EditorView.findFromDOM( $targetField.next('.code-input-editor-container')[0], ); // Get range const range = editor.viewState.state.selection.ranges[0]; // Insert text editor.dispatch({ changes: { from: range.from, to: range.to, insert: text, }, selection: {anchor: range.from + 1}, }); // Trigger change event $targetField.trigger('change'); } else { // Text area const cursorPosition = $targetField[0].selectionStart; const previousText = $targetField.val(); // Insert text to the cursor position $targetField.val( previousText.substring(0, cursorPosition) + text + previousText.substring(cursorPosition)); // Trigger change event $targetField.trigger('change'); } }, ); }; // Setup media snippet selector const setupMediaSnippets = function($select) { // Add URL to the select element $select.data('searchUrl', urlsForApi.library.get.url); // Add library download URL to the select element $select.data('imageUrl', urlsForApi.library.download.url); formHelpers.setupMediaSelector( $select, function(e) { const value = $(e.currentTarget).val(); // If there is no value, do nothing if (value == undefined || value == '') { return; } // Check if there is a CKEditor instance const ckeditorInstance = formHelpers .getCKEditorInstance('input_' + targetId + '_' + targetFieldId); // Text to be inserted const textURL = (ckeditorInstance) ? urlsForApi.library.download.url.replace( ':id', value, ) + '?preview=1' : '[' + value + ']'; const text = ''; if (ckeditorInstance) { // CKEditor formHelpers.insertToCKEditor( 'input_' + targetId + '_' + targetFieldId, text, ); } else if ($targetField.length > 0) { // Text area const cursorPosition = $targetField[0].selectionStart; const previousText = $targetField.val(); $targetField.val( previousText.substring(0, cursorPosition) + text + previousText.substring(cursorPosition)); $targetField.trigger('change'); } }, ); }; if (snippetMode == 'dataSet') { const dataSetFied = $(el).data('dependsOn'); const datasetId = $('[name=' + dataSetFied + ']').val(); // Initialise the dataset order clause // if the dataset id is not empty if (datasetId) { // Get the dataset columns $.ajax({ url: urlsForApi.dataset.search.url, type: 'GET', data: { dataSetId: datasetId, }, success: function(response) { const data = response.data[0]; if ( data && data.columns && data.columns.length > 0) { // Clear select options $select.find('option[value]').remove(); // Add data to the select options $.each(data.columns, function(_index, col) { $select.append( $('')); }); // If there are no options, hide if (data.columns.length == 0) { $select.parent().hide(); } else { // Setup the snippet selector setupSnippets($select); } // Disable fields on non read only mode if (readOnlyMode) { forms.makeFormReadOnly($select.parent()); } } else { $select.parent().hide(); } }, error: function() { $select.parent().append( '{% trans "An unknown error has occurred. Please refresh" %}', ); }, }); } } else if (snippetMode == 'dataType') { // Get request path const requestPath = urlsForApi.widget.getDataType.url.replace(':id', targetId); // Get the data type snippets $.ajax({ method: 'GET', url: requestPath, success: function(response) { if (response && response.fields && response.fields.length > 0) { // Clear select options $select.find('option[value]').remove(); // Add data to the select options $.each(response.fields, function(_index, element) { $select.append( $('')); }); // If there are no options, hide if (response.fields.length == 0) { $select.parent().hide(); } else { // Setup the snippet selector setupSnippets($select); } // Disable fields on non read only mode if (readOnlyMode) { forms.makeFormReadOnly($select.parent()); } } else { $select.parent().hide(); } }, error: function() { $select.parent().append( '{% trans "An unknown error has occurred. Please refresh" %}', ); }, }); } else if (snippetMode == 'options') { // Setup the snippet selector setupSnippets($select); } else if (snippetMode == 'media') { // Setup the media snippet selector setupMediaSnippets($select); } }); // Effect selector findElements( '.effect-selector', target, ).each(function(_k, el) { // Populate the effect list with options const $el = $(el).find('select'); const effectsType = $el.data('effects-type').split(' '); // Show option groups if we show all const showOptionGroups = (effectsType.indexOf('all') != -1); // Effects const effects = [ {effect: 'none', group: 'showAll'}, {effect: 'marqueeLeft', group: 'showAll'}, {effect: 'marqueeRight', group: 'showAll'}, {effect: 'marqueeUp', group: 'showAll'}, {effect: 'marqueeDown', group: 'showAll'}, {effect: 'noTransition', group: 'showPaged'}, {effect: 'fade', group: 'showPaged'}, {effect: 'fadeout', group: 'showPaged'}, {effect: 'scrollHorz', group: 'showPaged'}, {effect: 'scrollVert', group: 'showPaged'}, {effect: 'flipHorz', group: 'showPaged'}, {effect: 'flipVert', group: 'showPaged'}, {effect: 'shuffle', group: 'showPaged'}, {effect: 'tileSlide', group: 'showPaged'}, {effect: 'tileBlind', group: 'showPaged'}, ]; // Add the options $.each(effects, function(_index, element) { // Don't add effect if it's none and // the target is a element or element-group if ( effectsType.indexOf('noNone') != -1 && element.effect === 'none' ) { return; } // Don't add effect if the target effect type isn't all or a valid type if ( effectsType.indexOf('all') === -1 && effectsType.indexOf(element.group) === -1 ) { return; } // Show option group if it doesn't exist let $optionGroup = $el.find(`optgroup[data-type=${element.group}]`); if ( showOptionGroups && $optionGroup.length == 0 ) { $optionGroup = $(` `); $el.append($optionGroup); } // Check if we add to option group or main select const $appendTo = showOptionGroups ? $optionGroup : $el; $appendTo.append( $('')); }); // If we have a value, select it if ($el.data('value') !== undefined) { $el.val($el.data('value')); // Trigger change $el.trigger( 'change', [{ skipSave: true, }]); } if ($(el).next().find('input[name="speed"]').length === 1) { let currentSelected = null; const speedInput = $(el).next().find('input[name="speed"]'); const setEffectSpeed = function(prevEffect, newEffect, currSpeed) { const marqueeDefaultSpeed = 1; const noneMarqueeDefaultSpeed = 1000; const currIsMarquee = PlayerHelper.isMarquee(prevEffect); const isMarquee = PlayerHelper.isMarquee(newEffect); if (currIsMarquee && !isMarquee) { return noneMarqueeDefaultSpeed; } else if (!currIsMarquee && isMarquee) { return marqueeDefaultSpeed; } else { if (String(currSpeed).length === 0) { if (isMarquee) { return marqueeDefaultSpeed; } else { return noneMarqueeDefaultSpeed; } } else { return currSpeed; } } }; $el.on('select2:open', function(e) { currentSelected = e.currentTarget.value; }); speedInput.val(setEffectSpeed( currentSelected, currentSelected, speedInput.val(), )); $el.on('select2:select', function(e) { const data = e.params.data; const effect = data.id; speedInput.val(setEffectSpeed( currentSelected, effect, speedInput.val(), )); }); } }); // Font selector findElements( '.menu-board-category-selector', target, ).each(function(_k, el) { // Replace search url with the menuId const $el = $(el); const $elSelect = $(el).find('select'); const dependsOnName = $el.data('depends-on'); const $menu = $(container).find('[name=' + dependsOnName + ']'); // Get menu value const menuId = $menu.val(); if (menuId != '') { // Replace URL $elSelect.data( 'search-url', $elSelect.data('search-url-base').replace(':id', menuId), ); // Reset the select2 to update ajax url // $elSelect.select2('destroy'); makePagedSelect($elSelect); } }); // Select2 dropdown findElements( '.select2-hidden-accessible', target, ).each(function(_k, el) { const $el = $(el); $el.on('select2:open', function(event) { const $search = $(event.target).data('select2').dropdown?.$search; setTimeout(function() { if ($search) { $($search.get(0)).trigger('focus'); } }, 10); }); }); // Key capture findElements( '.key-capture-input', target, ).each(function(_k, el) { const $target = $(container) .find('#input_' + $(el).data('captureTargetId')); const $input = $(el).find('.key-capture-area'); const $error = $(el).find('.key-capture-error'); const $clear = $(el).find('.clear-key-button'); const allowedKeyCodes = [ // Letters 'KeyA', 'KeyB', 'KeyC', 'KeyD', 'KeyE', 'KeyF', 'KeyG', 'KeyH', 'KeyI', 'KeyJ', 'KeyK', 'KeyL', 'KeyM', 'KeyN', 'KeyO', 'KeyP', 'KeyQ', 'KeyR', 'KeyS', 'KeyT', 'KeyU', 'KeyV', 'KeyW', 'KeyX', 'KeyY', 'KeyZ', // Top-Row Numbers 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0', // Numpad Numbers 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad4', 'Numpad5', 'Numpad6', 'Numpad7', 'Numpad8', 'Numpad9', 'Numpad0', // Special Keys 'Enter', 'Backspace', 'Space', 'Delete', ]; // Save inital placeholder text const initialText = propertiesPanelTrans.keyCapture.clickToSetKey; const resetField = function(triggerBlur = true) { // Get back to initial placeholder $input.attr('placeholder', initialText); // Unset value $target.val(''); // Hide clear button $clear.hide(); // Unfocus field (triggerBlur) && $input.trigger('blur'); // Clear error $error.html('').hide(); }; const setValue = function(value = '', triggerBlur = true) { // If no value, reset field if (value === '') { resetField(triggerBlur); } else if (!allowedKeyCodes.includes(value)) { $error.html( propertiesPanelTrans.keyCapture.codeNotAllowed .replace(':code', value), ).show(); } else { // Set target $target.val(value); // Show key on capture $input.attr( 'placeholder', formHelpers.formatKeyCodeToReadableFormat(value), ); // Show clear button $clear.show(); // Clear error $error.html('').hide(); // Unfocus field (triggerBlur) && $input.trigger('blur'); } }; $input.on('focus', (ev) => { $input.attr('placeholder', propertiesPanelTrans.keyCapture.pressAKey); // Clear error $error.html('').hide(); // Hide clear button $clear.hide(); }); $input.on('blur', (ev) => { // Set value on blur setValue($target.val(), false); }); $input.on('keydown', (ev) => { ev.preventDefault(); // Set value to target setValue(ev.code); }); // Handle click to reset $clear.on('click', resetField); // On start, set value setValue($target.val()); }); let countExec = 0; // Stocks symbol search // Initialize tags input for properties panel with connectorProperties field findElements( 'input[data-role=panelTagsInput]', target, ).each(function(_k, el) { const self = $(el); const autoCompleteUrl = self.data('autoCompleteUrl'); const termKey = self.data('searchTermKey'); if (autoCompleteUrl !== undefined && autoCompleteUrl !== '') { countExec++; // Tags input with autocomplete const tags = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace, initialize: false, remote: { url: autoCompleteUrl, prepare: function(query, settings) { settings.data = {tag: query}; if (termKey !== undefined && termKey !== '') { settings.data[termKey] = query; } return settings; }, transform: function(list) { return $.map(list.data, function(tagObj) { if (countExec === 1) { return {tag: tagObj.type}; } return tagObj.type; }); }, }, }); const promise = tags.initialize(); promise .done(function() { if (countExec > 1 || self.prev().is('.bootstrap-tagsinput')) { // Destroy tagsinput instance if (typeof self.tagsinput === 'function') { self.tagsinput('destroy'); } countExec = 0; } const tagsInputOptions = { name: 'tags', source: tags.ttAdapter(), }; if (countExec === 1) { tagsInputOptions.displayKey = 'tag'; tagsInputOptions.valueKey = 'tag'; } // Initialise tagsinput with autocomplete self.tagsinput({ typeaheadjs: [{ hint: true, highlight: true, }, tagsInputOptions], }); }) .fail(function() { console.info('Auto-complete for tag failed! Using default...'); self.tagsinput(); }); } else { self.tagsinput(); } }); // Handle field dependencies for the container // only if we don't have a target if (!target) { $(container).find( '.xibo-form-input[data-depends-on]', ).each(function(_k, el) { const $target = $(el); let dependency = $target.data('depends-on'); // If the dependency has already been added, skip if ($target.data('depends-on-added')) { return; } // Mark dependency as added to the target $target.attr('data-depends-on-added', true); // Check if the dependency value comes as an array value ( value[1]) let dependencyArrayIndex = null; if (dependency.indexOf('[') !== -1 && dependency.indexOf(']') !== -1) { const dependencyArray = dependency.split('['); dependency = dependencyArray[0]; dependencyArrayIndex = dependencyArray[1].replace(']', ''); } // Add event listener to the dependency const base = '[name="' + dependency + '"]'; // Add event listener to the $base element $(container).find(base).on('change', function(ev) { let valueToSet = null; const $base = $(ev.currentTarget); // If $base is a dropdown if ($base.is('select')) { // Get selected option const $selectedOption = $base.find( 'option:selected', ); // Check if the selected option has a data-set value // if not, use the value of the dropdown if ($selectedOption.data('set')) { valueToSet = $selectedOption.data('set'); } else { valueToSet = $selectedOption.val(); } } else if ($base.is('input[type="checkbox"]')) { // If $base is a checkbox valueToSet = $base.is(':checked'); } else { valueToSet = $base.val(); } // Check if value to set is a string or an array of values if (dependencyArrayIndex !== null && valueToSet !== null) { valueToSet = valueToSet.split(',')[dependencyArrayIndex]; } // If the target is a checkbox, set the checked property if ($target.children('input[type="checkbox"]').length > 0) { // Set checked property value $target.children('input[type="checkbox"]') .prop('checked', valueToSet); } else if ($target.hasClass('colorpicker-input')) { // If the value is empty, clear the color picker if (valueToSet === '') { // Clear the color picker value $target.find('input').val(''); // Also update the background color of the input group $target.find('.input-group-addon').css('background-color', ''); } else if ( valueToSet !== null && valueToSet !== undefined && Color(valueToSet).valid ) { // Add the color to the color picker $target.find('input') .colorpicker('setValue', valueToSet); // Also update the background color of the input group $target.find('.input-group-addon') .css('background-color', valueToSet); } } else if ($target.children('input[type="text"]').length > 0) { // If the target is a text input, set the value $target.children('input[type="text"]').val(valueToSet); } else { // For the remaining cases, set the // value of the dependency to the target data attribute $target.data('dependsOnValue', valueToSet); // Reset the target form field forms.initFields(container, $target, targetId, readOnlyMode); } }); }); } }, /** * Handle form field replacements * @param {*} container - The form container * @param {*} baseObject - The base object to replace */ handleFormReplacements: function(container, baseObject) { const regExpMatchBetweenPercent = /%([^%\s]+)%/g; const replaceHTML = function(htmlString) { htmlString = htmlString.replace( regExpMatchBetweenPercent, function(_m, group) { // Replace trimmed match with the value of the base object return group.split('.').reduce((a, b) => { return (a[b]) || `%${b}%`; }, baseObject); }); return htmlString; }; // Replace title and alternative title for the elements that have them $(container).find('.xibo-form-input > [title], .xibo-form-btn[title]') .each(function(_idx, el) { const $element = $(el); const elementTitle = $element.attr('title'); const elementAlternativeTitle = $element.attr('data-original-title'); // If theres title and it contains a replacement special character if (elementTitle) { const matches = elementTitle.match(regExpMatchBetweenPercent); if (matches) { $element.attr('title', replaceHTML(elementTitle)); } } // If theres an aletrnative title and it // contains a replacement special character if ( elementAlternativeTitle && elementAlternativeTitle.indexOf('%') > -1 ) { const matches = elementAlternativeTitle .match(regExpMatchBetweenPercent); if (matches) { $element.attr( 'data-original-title', replaceHTML(elementAlternativeTitle)); } } }); // Replace inner html for input direct children $(container).find('.xibo-form-input > *, .xibo-form-btn') .each(function(_idx, el) { const $element = $(el); const elementInnerHTML = $element.html(); // If theres inner html and it contains a replacement special character if (elementInnerHTML) { const matches = elementInnerHTML.match(regExpMatchBetweenPercent); if (matches) { $element.html(replaceHTML(elementInnerHTML)); } } }); }, /** * Set the form conditions * @param {object} container - The form container * @param {object} baseObject - The base object * @param {string} targetId - The target id * @param {boolean} isTopLevel - Is the target parent top level * @param {boolean} isFormGroup - Should we hide the form-group */ setConditions: function( container, baseObject, targetId, isTopLevel = true, isFormGroup = false, ) { $(container).find('.xibo-form-input[data-visibility]') .each(function(_idx, el) { let visibility = $(el).data('visibility'); // Handle replacements for visibilty rules visibility = JSON.parse( JSON.stringify(visibility).replace(/\$(.*?)\$/g, function(_m, group) { // Replace match with the value of the base object return group.split('.').reduce((a, b) => a[b], baseObject); }), ); // Check if target element for condition exists const isConditionTargetExists = function(test) { if (test?.field && targetId) { const $conditionTargetElem = $(container).find( `[name="${test.field}"]`); return $conditionTargetElem.length !== 0; } if (test.conditions.length > 0 && targetId) { for (let i = 0; i < test.conditions.length; i++) { const conditionField = test.conditions[i].field; const $conditionTargetElem = $(container) .find(`[name="${conditionField}"]`); if ($conditionTargetElem.length === 0) { return false; } } return true; } }; // Handle a single condition const buildTest = function(test, $testContainer) { let testTargets = ''; const testType = test.type; const testConditions = test.conditions; // Check test const checkTest = function() { let testResult; for (let i = 0; i < testConditions.length; i++) { const condition = testConditions[i]; const fieldId = `[name="${condition.field}"]`; const $conditionTarget = $(container).find(fieldId); // Get condition target value based on type const conditionTargetValue = ($conditionTarget.length !== 0 && $conditionTarget.attr('type') === 'checkbox') ? $conditionTarget.is(':checked') : $conditionTarget.val(); const newTestResult = checkCondition( condition.type, condition.value, conditionTargetValue, isTopLevel, ); // If there are multiple conditions // we need to add the joining logic to them if (i > 0) { if (testType === 'and') { testResult = testResult && newTestResult; } else if (testType === 'or') { testResult = testResult || newTestResult; } } else { testResult = newTestResult; } } // If the test is true, show the element if (isFormGroup) { $testContainer.closest('.form-group').toggle(testResult); } else { $testContainer.toggle(testResult); } }; // Get all the targets for the test for (let i = 0; i < test.conditions.length; i++) { // Add the target to the list const fieldId = `[name="${test.conditions[i].field}"]`; testTargets += fieldId; // If there are multiple conditions, add a comma if (i < test.conditions.length - 1) { testTargets += ','; } } if (String(testTargets).length > 0) { // Check test when any of the targets change $(container).find(testTargets).on('change', checkTest); } // Run on first load checkTest(); }; // If visibility tests are an array, process each one of the options if (Array.isArray(visibility)) { for (let i = 0; i < visibility.length; i++) { const test = visibility[i]; buildTest(test, $(el)); } } else { // Otherwise, process the single condition isConditionTargetExists(visibility) && buildTest({ conditions: [visibility], test: '', }, $(el)); } }); }, /** * Check for spacing issues on the form inputs * @param {object} $container - The form container */ checkForSpacingIssues: function($container) { $container.find('input[type=text]').each(function(_idx, el) { formRenderDetectSpacingIssues(el); $(el).on('keyup', _.debounce(function() { formRenderDetectSpacingIssues(el); }, 500)); }); }, /** * Disable all the form inputs and make it read only * @param {object} $formContainer - The form container */ makeFormReadOnly: function($formContainer) { // Disable inputs, select, textarea and buttons $formContainer .find( 'input, select, ' + 'textarea, button:not(.copyTextAreaButton)', ).attr('disabled', 'disabled'); // Disable color picker plugin $formContainer .find('.colorpicker-input i.input-group-addon') .off('click'); // Hide buttons $formContainer.find( 'button:not(.copyTextAreaButton), ' + '.date-clear-button', ).hide(); // Hide bootstrap switch $formContainer.find('.bootstrap-switch').hide(); }, /** * Reload Rich text fields * @param {object} $formContainer - The form container */ reloadRichTextFields: function($formContainer) { $formContainer.find('.rich-text').each(function(_k, el) { const elId = $(el).attr('id'); const $parent = $(el).parent(); // Hide elements in editor to avoid glitch $parent.addClass('loading'); // Destroy CKEditor formHelpers.destroyCKEditor(elId); // Reload formHelpers.setupCKEditor( $formContainer, elId, true, null, false, true); // Hide text area to prevent flicker $parent.removeClass('loading'); }); }, /** * Create form fields for fallback data * @param {object} dataType * @param {object} container * @param {object} fallbackData * @param {object} widget * @param {callback} reloadWidgetCallback */ createFallbackDataForm: function( dataType, container, fallbackData, widget, reloadWidgetCallback, ) { const dataFields = dataType.fields; const createField = function(field, data) { const fieldClone = {...field}; const auxId = Math.floor(Math.random() * 1000000); if (data != undefined) { fieldClone.value = data; } // Set date variant if ( ['datetime', 'date', 'time', 'month'].indexOf(fieldClone.type) != -1 ) { fieldClone.variant = (fieldClone.type === 'datetime') ? 'dateTime' : fieldClone.type; fieldClone.type = 'date'; } // Set image variant if (fieldClone.type === 'image') { fieldClone.type = 'mediaSelector'; fieldClone.mediaSearchUrl = urlsForApi.library.get.url; fieldClone.initialValue = fieldClone.value; fieldClone.initialKey = 'mediaId'; } // If field is type string, change it to text if (fieldClone.type === 'string') { fieldClone.type = 'text'; } // Set name as id and give a unique id fieldClone.name = fieldClone.id; fieldClone.id = fieldClone.id + '_' + auxId; // Add custom class to prevent auto save fieldClone.customClass = 'fallback-property'; let $newField; if (templates.forms.hasOwnProperty(fieldClone.type)) { $newField = $(templates.forms[fieldClone.type](fieldClone)); } // Add helper to required fields if ($newField && $newField.is('[data-is-required="true"]')) { $newField.find('label') .append(`*`); } return $newField; }; const updateRecordPreview = function($record, recordData) { const $previewsContainer = $record.find('.fallback-data-record-previews'); // Empty container $previewsContainer.empty(); dataFields.forEach((field) => { if (recordData && recordData[field.id]) { const fieldData = recordData[field.id]; // New field preview const $recordPreview = $(templates.forms.fallbackDataRecordPreview({ trans: fallbackDataTrans, field: field, data: fieldData, })); $record.find('.fallback-data-record-previews') .append($recordPreview); } }); }; const saveRecord = function($record) { const recordData = {}; let invalidRequired = false; // Get input fields and build data to be saved dataFields.forEach((field) => { const $field = $record.find('[name="' + field.id + '"]'); const value = $field.val(); const required = $field.parents('.fallback-property').is('[data-is-required="true"]'); if (value == '' && required) { invalidRequired = true; } else if (value != '') { recordData[field.id] = value; } }); // If record data is empty or invalid, thow error and don't save if ($.isEmptyObject(recordData)) { toastr.error(fallbackDataTrans.invalidRecordEmpty); return; } else if (invalidRequired) { toastr.error(fallbackDataTrans.invalidRecordRequired); return; } const updateRecord = function() { // Update preview updateRecordPreview($record, recordData); reloadWidgetCallback(); $record.removeClass('editing'); }; // Save or add new record const recordId = $record.data('recordId'); const displayOrder = $record.index(); if (recordId) { widget.editFallbackDataRecord(recordId, recordData, displayOrder) .then((_res) => { if (_res.success) { updateRecord(); } }); } else { widget.addFallbackData(recordData, displayOrder) .then((_res) => { if (_res.success) { // Add id to record object $record.data('recordId', _res.id); updateRecord(); } }); } }; const editRecord = function($record) { $record.addClass('editing'); }; const deleteRecord = function($record) { const recordId = $record.data('recordId'); if (recordId) { widget.deleteFallbackDataRecord(recordId) .then((_res) => { if (_res.success) { // Remove object $record.remove(); reloadWidgetCallback(); } }); } else { // Remove object $record.remove(); } }; const createRecord = function(data = {}) { const recordData = data.data; const displayOrder = data.displayOrder; const recordId = data.id; const $recordContainer = $(templates.forms.fallbackDataRecord({ trans: fallbackDataTrans, })); // Add record id to data if exists if (recordId != undefined) { $recordContainer.data('recordId', recordId); } // Add display order to data if exists if (displayOrder != undefined) { $recordContainer.data('displayOrder', displayOrder); } // Add properties from data dataFields.forEach((field) => { const fieldData = (recordData) && recordData[field.id]; const $newField = createField(field, fieldData); // New field input $recordContainer.find('.fallback-data-record-fields') .append($newField); }); // Initialize fields forms.initFields($recordContainer); // Record preview updateRecordPreview($recordContainer, recordData); // Mark new field as editing if it's a new record if ($.isEmptyObject(data)) { $recordContainer.addClass('editing'); } // Handle buttons $recordContainer.find('button').on('click', function(ev) { const actionType = $(ev.currentTarget).data('action'); if (actionType == 'save-record') { saveRecord($recordContainer); } else if (actionType == 'delete-record') { deleteRecord($recordContainer); } else if (actionType == 'edit-record') { editRecord($recordContainer); } }); return $recordContainer; }; // Create main content $(container).append(templates.forms.fallbackDataContent({ data: dataType, trans: fallbackDataTrans, showFallback: widget.getOptions().showFallback, })); const $recordsContainer = $(container).find('.fallback-data-records'); // If we have existing data, add it to the control if (fallbackData) { // Sort array by display order fallbackData.sort((a, b) => { return a.displayOrder - b.displayOrder; }); fallbackData.forEach((data) => { $recordsContainer.append( createRecord(data), ); }); // Call Xibo Init for the records container XiboInitialise('.fallback-data-records'); } // Handle create record button $(container).find('[data-action="add-new-record"]').on('click', function() { $recordsContainer.append( createRecord(), ); // Call Xibo Init for the records container XiboInitialise('.fallback-data-records'); }); // Init sortable $recordsContainer.sortable({ axis: 'y', items: '.fallback-data-record', containment: 'parent', update: function() { // Create records structure let idxAux = 0; const records = []; $recordsContainer.find('.fallback-data-record').each((_idx, record) => { const recordData = $(record).data(); if (recordData.recordId) { records.push({ dataId: recordData.recordId, displayOrder: idxAux, }); // Update data on record $(record).data('displayOrder', idxAux); idxAux++; } }); widget.saveFallbackDataOrder(records) .then((_res) => { if (_res.success) { reloadWidgetCallback(); } }); }, }); }, /** * Callback for all membership forms * @param {object} dialog Dialog object */ membersFormOpen: function(dialog) { const control = $(dialog).find('.controlDiv'); // This contains the changes made since the form open if (control.data().members == undefined) { control.data().members = { displays: {}, displayGroups: {}, users: {}, userGroups: {}, }; } if (control.data().displayGroups) { createDisplayGroupMembersTable(dialog); } if (control.data().display) { createDisplayMembersTable(dialog); } if (control.data().userGroups) { createUserGroupMembersTable(dialog); } if (control.data().user) { createUserMembersTable(dialog); } // Bind to the checkboxes change event control.on('change', '.checkbox', function(ev) { const $checkbox = $(ev.currentTarget); // Update our global members data with this const memberId = $checkbox.data().memberId; const memberType = $checkbox.data().memberType; const value = $checkbox.is(':checked'); if (memberType === 'display') { control.data().members.displays[memberId] = (value) ? 1 : 0; } else if (memberType === 'displayGroup') { control.data().members.displayGroups[memberId] = (value) ? 1 : 0; } else if (memberType === 'user') { control.data().members.users[memberId] = (value) ? 1 : 0; } else if (memberType === 'userGroup') { control.data().members.userGroups[memberId] = (value) ? 1 : 0; } }); }, permissionsFormOpen: function() { const grid = $('#permissionsTable').closest('.XiboGrid'); // initialise the permissions array if (grid.data().permissions.length <= 0) { grid.data().permissions = {}; } const table = $('#permissionsTable').DataTable({ language: dataTablesLanguage, serverSide: true, stateSave: true, filter: false, searchDelay: 3000, order: [[0, 'asc']], ajax: { url: grid.data().url, data: function(d) { $.extend( d, grid.find('.permissionsTableFilter form').serializeObject(), ); }, }, columns: [ { data: 'group', render: function(data, type, row, meta) { if (type != 'display') { return data; } if (row.isUser == 1) { return data; } else { return '' + data + ''; } }, }, { data: 'view', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in grid.data().permissions) { const cache = grid.data().permissions[row.groupId]; checked = (cache.view !== undefined && cache.view === 1) ? 1 : 0; } else { checked = data; } // Cached changes to this field? return ''; }, }, { data: 'edit', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in grid.data().permissions) { const cache = grid.data().permissions[row.groupId]; checked = (cache.edit !== undefined && cache.edit === 1) ? 1 : 0; } else { checked = data; } return ''; }, }, { data: 'delete', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in grid.data().permissions) { const cache = grid.data().permissions[row.groupId]; checked = (cache.delete !== undefined && cache.delete === 1) ? 1 : 0; } else { checked = data; } return ''; }, }, ], }); table.on('draw', function(e, settings) { dataTableDraw(e, settings); // Bind to the checkboxes change event const target = $('#' + e.target.id); target.find('input[type=checkbox]').on('change', function(ev) { const $checkbox = $(ev.currentTarget); // Update our global permissions data with this const groupId = $checkbox.data().groupId; const permission = $checkbox.data().permission; const value = $checkbox.is(':checked'); if (grid.data().permissions[groupId] === undefined) { grid.data().permissions[groupId] = {}; } grid.data().permissions[groupId][permission] = (value) ? 1 : 0; }); }); table.on('processing.dt', dataTableProcessing); // Bind our filter grid.find( '.permissionsTableFilter form input, .permissionsTableFilter form select', ).on('change', function() { table.ajax.reload(); }); }, permissionsFormSubmit: function(id) { const form = $('#' + id); const $formContainer = form.closest('.permissions-form'); const permissions = { groupIds: $(form).data().permissions, ownerId: $formContainer.find('select[name=ownerId]').val(), }; const data = $.param(permissions); $.ajax({ type: 'POST', url: form.data().url, cache: false, dataType: 'json', data: data, success: function(xhr, textStatus, error) { XiboSubmitResponse(xhr, form); }, error: function(xhr, textStatus, errorThrown) { SystemMessage(xhr.responseText, false); }, }); }, permissionsMultiFormOpen: function(dialog) { const $permissionsTable = $(dialog).find('#permissionsMultiTable'); const $grid = $permissionsTable.closest('.XiboGrid'); const table = $permissionsTable.DataTable({ language: dataTablesLanguage, serverSide: true, stateSave: true, filter: false, searchDelay: 3000, order: [[0, 'asc']], ajax: { url: $grid.data().url, data: function(d) { $.extend(d, $grid.find('.permissionsMultiTableFilter form') .serializeObject()); $.extend(d, { ids: $grid.data().targetIds, }); }, dataSrc: function(json) { const newData = json.data; for (const dataKey in newData) { if (newData.hasOwnProperty(dataKey)) { const permissionGrouped = { view: null, edit: null, delete: null, }; for (const key in newData[dataKey].permissions) { if (newData[dataKey].permissions.hasOwnProperty(key)) { const permission = newData[dataKey].permissions[key]; if (permission.view != permissionGrouped.view) { if (permissionGrouped.view != null) { permissionGrouped.view = 2; } else { permissionGrouped.view = permission.view; } } if (permission.edit != permissionGrouped.edit) { if (permissionGrouped.edit != null) { permissionGrouped.edit = 2; } else { permissionGrouped.edit = permission.edit; } } if (permission.delete != permissionGrouped.delete) { if (permissionGrouped.delete != null) { permissionGrouped.delete = 2; } else { permissionGrouped.delete = permission.delete; } } } } newData[dataKey] = Object.assign(permissionGrouped, newData[dataKey]); delete newData[dataKey].permissions; } } // merge the permission and start permissions arrays $grid.data().permissions = Object.assign({}, $grid.data().permissions, newData); $grid.data().startPermissions = Object.assign( {}, $grid.data().startPermissions, JSON.parse(JSON.stringify(newData)), ); // init save permissions if undefined if ($grid.data().savePermissions == undefined) { $grid.data().savePermissions = {}; } // Return an array of permissions return Object.values(newData); }, }, columns: [ { data: 'group', render: function(data, type, row, meta) { if (type != 'display') { return data; } if (row.isUser == 1) { return data; } else { return '' + data + ''; } }, }, { data: 'view', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in $grid.data().permissions) { const cache = $grid.data().permissions[row.groupId]; checked = (cache.view !== undefined && cache.view !== 0) ? cache.view : 0; } else { checked = data; } // Cached changes to this field? return ''; }, }, { data: 'edit', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in $grid.data().permissions) { const cache = $grid.data().permissions[row.groupId]; checked = (cache.edit !== undefined && cache.edit !== 0) ? cache.edit : 0; } else { checked = data; } return ''; }, }, { data: 'delete', render: function(data, type, row, meta) { if (type != 'display') { return data; } let checked; if (row.groupId in $grid.data().permissions) { const cache = $grid.data().permissions[row.groupId]; checked = (cache.delete !== undefined && cache.delete !== 0) ? cache.delete : 0; } else { checked = data; } return ''; }, }, ], }); table.on('draw', function(e, settings) { dataTableDraw(e, settings); // Bind to the checkboxes change event const target = $('#' + e.target.id); target.find('input[type=checkbox]').on('change', function(ev) { const $checkbox = $(ev.currentTarget); // Update our global permissions data with this const groupId = $checkbox.data().groupId; const permission = $checkbox.data().permission; const value = $checkbox.is(':checked'); const valueNumeric = (value) ? 1 : 0; // Update main permission object if ($grid.data().permissions[groupId] === undefined) { $grid.data().permissions[groupId] = {}; } $grid.data().permissions[groupId][permission] = valueNumeric; // Update save permissions object if ($grid.data().savePermissions[groupId] === undefined) { $grid.data().savePermissions[groupId] = {}; $grid.data().savePermissions[groupId][permission] = valueNumeric; } else { if ( $grid.data().startPermissions[groupId][permission] === valueNumeric ) { // if changed value is the same as the initial permission object // remove it from the save permissions object delete $grid.data().savePermissions[groupId][permission]; // Remove group if it's an empty object if ($.isEmptyObject($grid.data().savePermissions[groupId])) { delete $grid.data().savePermissions[groupId]; } } else { // Add new change to the save permissions object $grid.data().savePermissions[groupId][permission] = valueNumeric; } } // Enable save button only if we have permission changes to save $(dialog).find('.save-button').toggleClass( 'disabled', $.isEmptyObject($grid.data().savePermissions), ); }); // Mark indeterminate checkboxes and add title target.find('input[type=checkbox].indeterminate') .prop('indeterminate', true).prop('title', translations.indeterminate); }); // Disable save button by default $(dialog).find('.save-button').addClass('disabled'); table.on('processing.dt', dataTableProcessing); // Bind our filter $grid.find( '.permissionsMultiTableFilter form input, ' + '.permissionsMultiTableFilter form select', ).on('change', function() { table.ajax.reload(); }); }, permissionsMultiFormSubmit: function(id) { const form = $('#' + id); const permissions = $(form).data().savePermissions; const targetIds = $(form).data().targetIds; const data = $.param({ groupIds: permissions, ids: targetIds, }); $.ajax({ type: 'POST', url: form.data().url, cache: false, dataType: 'json', data: data, success: function(xhr, textStatus, error) { XiboSubmitResponse(xhr, form); }, error: function(xhr, textStatus, errorThrown) { SystemMessage(xhr.responseText, false); }, }); }, /** * Submit membership form * @param {string} id The form id */ membersFormSubmit: function(id) { const form = $('#' + id); const members = form.data().members; // There may not have been any changes if (members == undefined) { // No changes XiboDialogClose(); return; } // Create a new queue. window.queue = $.jqmq({ // Next item will be processed only when // queue.next() is called in callback. delay: -1, // Process queue items one-at-a-time. batch: 1, // For each queue item, execute this function // making an AJAX request. Only continue processing // the queue once the AJAX request's callback executes. callback: function(data) { // Make an AJAX call $.ajax({ type: 'POST', url: data.url, cache: false, dataType: 'json', data: $.param(data.data), success: function(response, textStatus, error) { if (response.success) { // Success - what do we do now? if (response.message != '') { SystemMessage(response.message, true); } // Process the next item queue.next(); } else { // Why did we fail? if (response.login) { // We were logged out LoginBox(response.message); } else { // Likely just an error that we want to report on // Remove the saving cog form.closest('.modal-dialog').find('.saving').remove(); SystemMessageInline(response.message, form.closest('.modal')); } } }, error: function(responseText) { // Remove the saving cog form.closest('.modal-dialog').find('.saving').remove(); SystemMessage(responseText, false); }, }); }, // When the queue completes naturally, execute this function. complete: function() { // Remove the save button form.closest('.modal-dialog').find('.saving').parent().remove(); // Refresh the grids // (this is a global refresh) XiboRefreshAllGrids(); if (form.data('nextFormUrl') !== undefined) { XiboFormRender(form.data().nextFormUrl); } // Close the dialog XiboDialogClose(); }, }); let addedToQueue = false; // Build an array of id's to assign and an array to unassign const assign = []; const unassign = []; $.each(members.displays, function(name, value) { if (value == 1) { assign.push(name); } else { unassign.push(name); } }); if (assign.length > 0 || unassign.length > 0) { const dataDisplays = { data: {}, url: form.data().displayUrl, }; dataDisplays.data[form.data().displayParam] = assign; dataDisplays.data[form.data().displayParamUnassign] = unassign; // Queue queue.add(dataDisplays); addedToQueue = true; } // Build an array of id's to assign and an array to unassign const assignDisplayGroup = []; const unassignDisplayGroup = []; $.each(members.displayGroups, function(name, value) { if (value == 1) { assignDisplayGroup.push(name); } else { unassignDisplayGroup.push(name); } }); if (assignDisplayGroup.length > 0 || unassignDisplayGroup.length > 0) { const dataDisplayGroups = { data: {}, url: form.data().displayGroupsUrl, }; dataDisplayGroups.data[form.data().displayGroupsParam] = assignDisplayGroup; dataDisplayGroups.data[form.data().displayGroupsParamUnassign] = unassignDisplayGroup; // Queue queue.add(dataDisplayGroups); addedToQueue = true; } // Build an array of id's to assign and an array to unassign const assignUser = []; const unassignUser = []; $.each(members.users, function(name, value) { if (value == 1) { assignUser.push(name); } else { unassignUser.push(name); } }); if (assignUser.length > 0 || unassignUser.length > 0) { const dataUsers = { data: {}, url: form.data().userUrl, }; dataUsers.data[form.data().userParam] = assignUser; dataUsers.data[form.data().userParamUnassign] = unassignUser; // Queue queue.add(dataUsers); addedToQueue = true; } // Build an array of id's to assign and an array to unassign const assignUserGroup = []; const unassignUserGroup = []; $.each(members.userGroups, function(name, value) { if (value == 1) { assignUserGroup.push(name); } else { unassignUserGroup.push(name); } }); if (assignUserGroup.length > 0 || unassignUserGroup.length > 0) { const dataUserGroups = { data: {}, url: form.data().userGroupsUrl, }; dataUserGroups.data[form.data().userGroupsParam] = assignUserGroup; dataUserGroups.data[form.data().userGroupsParamUnassign] = unassignUserGroup; // Queue queue.add(dataUserGroups); addedToQueue = true; } if (!addedToQueue) { XiboDialogClose(); } else { // Start the queue queue.start(); } }, // Callback for the media form mediaDisplayGroupFormCallBack: function() { const container = $('#FileAssociationsAssign'); if (container.data().media == undefined) { container.data().media = {}; } // Get starting items let includedItems = []; $('#FileAssociationsSortable').find('[data-media-id]').each((_i, el) => { includedItems.push($(el).data('mediaId')); }); const mediaTable = $('#mediaAssignments').DataTable({ language: dataTablesLanguage, serverSide: true, stateSave: true, searchDelay: 3000, order: [[0, 'asc']], filter: false, ajax: { url: $('#mediaAssignments').data().url, data: function(d) { $.extend(d, $('#mediaAssignments').closest('.XiboGrid') .find('.FilterDiv form').serializeObject()); }, }, columns: [ {data: 'name'}, {data: 'mediaType'}, { sortable: false, data: function(data, type, row, meta) { if (type != 'display') { return ''; } // If media id is already added to the container // Create span with disabled if (includedItems.indexOf(data.mediaId) != -1) { // Create a disabled span return '' + ''; } else { // Create a click-able span return '' + ''; } }, }, ], }); mediaTable.on('draw', function(e, settings) { dataTableDraw(e, settings); // Clicky on the +spans $('.assignItem:not(.disabled)', '#mediaAssignments') .on('click', function(ev) { const $target = $(ev.currentTarget); // Get the row that this is in. const data = mediaTable.row($target.closest('tr')).data(); // Append to our media list container.data().media[data.mediaId] = 1; // Add to aux array includedItems.push(data.mediaId); // Disable add button $target.parents('tr').addClass('disabled'); // Hide plus button $target.hide(); // Construct a new list item for the lower list and append it. const newItem = $('
  • ', { text: data.name, 'data-media-id': data.mediaId, class: 'btn btn-sm btn-white', }); newItem.appendTo('#FileAssociationsSortable'); // Add a span to that new item $('', { class: 'fa fa-minus ml-1', }).appendTo(newItem); }); }); mediaTable.on('processing.dt', dataTableProcessing); // Make our little list sortable $('#FileAssociationsSortable').sortable(); // Bind to the existing items in the list $('#FileAssociationsSortable').on('click', 'li span', function(ev) { const $target = $(ev.currentTarget); const mediaId = $target.parent().data().mediaId; container.data().media[mediaId] = 0; $target.parent().remove(); // Remove from aux array includedItems = includedItems.filter((item) => item != mediaId); // Reload table mediaTable.ajax.reload(); }); // Bind to the filter $('#mediaAssignments').closest('.XiboGrid') .find('.FilterDiv input, .FilterDiv select').on('change', function() { mediaTable.ajax.reload(); }); }, mediaAssignSubmit: function() { // Collect our media const container = $('#FileAssociationsAssign'); // Build an array of id's to assign and an array to unassign const assign = []; const unassign = []; $.each(container.data().media, function(name, value) { if (value == 1) { assign.push(name); } else { unassign.push(name); } }); assignMediaToCampaign(container.data().url, assign, unassign); }, // Callback for the media form layoutFormCallBack: function() { const container = $('#FileAssociationsAssign'); if (container.data().layout == undefined) { container.data().layout = {}; } const layoutTable = $('#layoutAssignments').DataTable({ language: dataTablesLanguage, serverSide: true, stateSave: true, searchDelay: 3000, order: [[0, 'asc']], filter: false, ajax: { url: $('#layoutAssignments').data().url, data: function(d) { $.extend( d, $('#layoutAssignments').closest('.XiboGrid') .find('.FilterDiv form').serializeObject(), ); }, }, columns: [ {data: 'layout'}, { sortable: false, data: function(data, type, row, meta) { if (type != 'display') { return ''; } // Create a click-able span return '' + ''; }, }, ], }); layoutTable.on('draw', function(e, settings) { dataTableDraw(e, settings); // Clicky on the +spans $('.assignItem', '#layoutAssignments').on('click', function(ev) { // Get the row that this is in. const data = layoutTable.row($(ev.currentTarget).closest('tr')).data(); // Append to our layout list container.data().layout[data.layoutId] = 1; // Construct a new list item for the lower list and append it. const newItem = $('
  • ', { text: data.layout, 'data-layout-id': data.layoutId, class: 'btn btn-sm btn-white', }); newItem.appendTo('#FileAssociationsSortable'); // Add a span to that new item $('', { class: 'fa fa-minus', click: function(ev) { container.data().layout[$(ev.currentTarget) .parent().data().layoutId] = 0; $(ev.currentTarget).parent().remove(); }, }).appendTo(newItem); }); }); layoutTable.on('processing.dt', dataTableProcessing); // Make our little list sortable $('#FileAssociationsSortable').sortable(); // Bind to the existing items in the list $('#FileAssociationsSortable').find('li span').on('click', function(ev) { container.data().layout[$(ev.currentTarget).parent().data().layoutId] = 0; $(ev.currentTarget).parent().remove(); }); // Bind to the filter $('#layoutAssignments').closest('.XiboGrid') .find('.FilterDiv input, .FilterDiv select').on('change', function() { layoutTable.ajax.reload(); }); }, layoutAssignSubmit: function() { // Collect our layout const container = $('#FileAssociationsAssign'); // Build an array of id's to assign and an array to unassign const assign = []; const unassign = []; $.each(container.data().layout, function(name, value) { if (value == 1) { assign.push(name); } else { unassign.push(name); } }); assignLayoutToCampaign(container.data().url, assign, unassign); }, userProfileEditFormOpen: function() { $('#qRCode').addClass('d-none'); $('#recoveryButtons').addClass('d-none'); $('#recoveryCodes').addClass('d-none'); $('#twoFactorTypeId').on('change', function(e) { e.preventDefault(); if ( $('#twoFactorTypeId').val() == 2 && $('#userEditProfileForm').data().currentuser != 2 ) { $.ajax({ url: $('#userEditProfileForm').data().setup, type: 'GET', beforeSend: function() { $('#qr').addClass('fa fa-spinner fa-spin loading-icon'); }, success: function(response) { const qRCode = response.data.qRUrl; $('#qrImage').attr('src', qRCode); }, complete: function() { $('#qr').removeClass('fa fa-spinner fa-spin loading-icon'); }, }); $('#qRCode').removeClass('d-none'); } else { $('#qRCode').addClass('d-none'); } if ($('#twoFactorTypeId').val() == 0) { $('#recoveryButtons').addClass('d-none'); $('#recoveryCodes').addClass('d-none'); } if ( $('#userEditProfileForm').data().currentuser != 0 && $('#twoFactorTypeId').val() != 0 ) { $('#recoveryButtons').removeClass('d-none'); } }); if ($('#userEditProfileForm').data().currentuser != 0) { $('#recoveryButtons').removeClass('d-none'); } let generatedCodes = ''; $('#generateCodesBtn').on('click', function(e) { $('#codesList').html(''); $('#recoveryCodes').removeClass('d-none'); $('.recBtn').attr('disabled', true).addClass('disabled'); generatedCodes = ''; $.ajax({ url: $('#userEditProfileForm').data().generate, async: false, type: 'GET', beforeSend: function() { $('#codesList').removeClass('card') .addClass('fa fa-spinner fa-spin loading-icon'); }, success: function(response) { generatedCodes = JSON.parse(response.data.codes); $('#recoveryCodes').addClass('d-none'); $('.recBtn').attr('disabled', false).removeClass('disabled'); $('#showCodesBtn').trigger('click'); }, complete: function() { $('#codesList').removeClass('fa fa-spinner fa-spin loading-icon'); }, }); }); $('#showCodesBtn').on('click', function(e) { $('.recBtn').attr('disabled', true).addClass('disabled'); $('#codesList').html(''); $('#recoveryCodes').toggleClass('d-none'); let codesList = []; $.ajax({ url: $('#userEditProfileForm').data().show, type: 'GET', data: { generatedCodes: generatedCodes, }, success: function(response) { if (generatedCodes != '') { codesList = generatedCodes; } else { codesList = response.data.codes; } $('#twoFactorRecoveryCodes').val(JSON.stringify(codesList)); $.each(codesList, function(index, value) { $('#codesList').append(value + '
    '); }); $('#codesList').addClass('card'); $('.recBtn').attr('disabled', false).removeClass('disabled'); }, }); }); }, tagsWithValues: function(formId) { $('#tagValue, label[for="tagValue"], #tagValueRequired').addClass('d-none'); $('#tagValueContainer').hide(); let tag; let tagWithOption = ''; let tagN = ''; let tagV = ''; let tagOptions = []; let tagIsRequired = 0; const formSelector = '#' + formId + ' input#tags' + ', #' + formId + ' input#tagsToAdd'; $(formSelector).on('beforeItemAdd', function(event) { $('#tagValue').html(''); $('#tagValueInput').val(''); tag = event.item; tagOptions = []; tagIsRequired = 0; tagN = tag.split('|')[0]; tagV = tag.split('|')[1]; if ($(formSelector).val().indexOf(tagN) >= 0) { // if we entered a Tag that already exists there are two options // exists without value and entered without value // handled automatically allowDuplicates = false // as we allow entering Tags with value // we need additional handling for that // go through tagsinput items and return the // one that matches Tag name about to be added const item = $(formSelector).tagsinput('items').filter((item) => { return item.split('|')[0].toLowerCase() === tagN.toLowerCase(); }); // remove the existing Tag from tagsinput before adding the new one $(formSelector).tagsinput('remove', item.toString()); } if ($(formSelector).val().indexOf(tagN) === -1 && tagV === undefined) { $.ajax({ url: $('form#' + formId).data().gettag, type: 'GET', data: { name: tagN, }, beforeSend: function() { $('#loadingValues').addClass('fa fa-spinner fa-spin loading-icon'); }, success: function(response) { if (response.success) { if (response.data.tag != null) { tagOptions = JSON.parse(response.data.tag.options); tagIsRequired = response.data.tag.isRequired; if (tagOptions != null && tagOptions != []) { $('#tagValue, label[for="tagValue"]').removeClass('d-none'); if ($('#tagValue option[value=""]').length <= 0) { $('#tagValue') .append($('') .attr('value', '') .text('')); } $.each(tagOptions, function(key, value) { if ( $('#tagValue option[value=' + value + ']').length <= 0 ) { $('#tagValue') .append($('') .attr('value', value) .text(value)); } }); $('#tagValue').trigger('focus'); } else { // existing Tag without specified options (values) $('#tagValueContainer').show(); // if the isRequired flag is set to 0 change // the helpText to be more user friendly. if (tagIsRequired === 0) { $('#tagValueInput').parent().find('span.help-block') .text(translations.tagInputValueHelpText); } else { $('#tagValueInput').parent().find('span.help-block') .text(translations.tagInputValueRequiredHelpText); } $('#tagValueInput').trigger('focus'); } } else { // new Tag $('#tagValueContainer').show(); $('#tagValueInput').trigger('focus'); // isRequired flag is set to 0 (new Tag) change // the helpText to be more user friendly. $('#tagValueInput').parent().find('span.help-block') .text(translations.tagInputValueHelpText); } } }, complete: function() { $('#loadingValues').removeClass( 'fa fa-spinner fa-spin loading-icon', ); }, error: function(jqXHR, textStatus, errorThrown) { console.error(jqXHR, textStatus, errorThrown); }, }); } }); $(formSelector).on('itemAdded', function(event) { if (tagOptions != null && tagOptions != []) { $('#tagValue').trigger('focus'); } }); $(formSelector).on('itemRemoved', function(event) { if (tagN === event.item) { $('#tagValueRequired, label[for="tagValue"]').addClass('d-none'); $('.save-button').prop('disabled', false); $('#tagValue').html('').addClass('d-none'); $('#tagValueInput').val(''); $('#tagValueContainer').hide(); tagN = ''; } else if ($('.save-button').is(':disabled')) { // do nothing with jQuery } else { $('#tagValue').html('').addClass('d-none'); $('#tagValueInput').val(''); $('#tagValueContainer').hide(); $('label[for="tagValue"]').addClass('d-none'); } }); $('#tagValue').on('change', function(e) { e.preventDefault(); tagWithOption = tagN + '|' + $(e.currentTarget).val(); // additional check, helpful for multi tagging. if (tagN != '') { if ( tagIsRequired === 0 || (tagIsRequired === 1 && $(e.currentTarget).val() !== '') ) { $(formSelector).tagsinput('add', tagWithOption); $(formSelector).tagsinput('remove', tagN); $('#tagValue').html('').addClass('d-none'); $('#tagValueRequired, label[for="tagValue"]').addClass('d-none'); $('.save-button').prop('disabled', false); } else { $('#tagValueRequired').removeClass('d-none'); $('#tagValue').trigger('focus'); } } }); $('#tagValue').on('blur', function(ev) { if ($(ev.currentTarget).val() === '' && tagIsRequired === 1) { $('#tagValueRequired').removeClass('d-none'); $('#tagValue').trigger('focus'); $('.save-button').prop('disabled', true); } else { $('#tagValue').html('').addClass('d-none'); $('label[for="tagValue"]').addClass('d-none'); } }); $('#tagValueInput').on('keypress focusout', function(event) { if ((event.keyCode === 13 || event.type === 'focusout') && tagN != '') { event.preventDefault(); const tagInputValue = $(event.currentTarget).val(); tagWithOption = (tagInputValue !== '') ? tagN + '|' + tagInputValue : tagN; if ( tagIsRequired === 0 || (tagIsRequired === 1 && tagInputValue !== '') ) { $(formSelector).tagsinput('add', tagWithOption); // remove only if we have value (otherwise it would be left empty) if (tagInputValue !== '') { $(formSelector).tagsinput('remove', tagN); } $('#tagValueInput').val(''); $('#tagValueContainer').hide(); $('#tagValueRequired').addClass('d-none'); $('.save-button').prop('disabled', false); } else { $('#tagValueContainer').show(); $('#tagValueRequired').removeClass('d-none'); $('#tagValueInput').trigger('focus'); } } }); }, /** * Called when the ACL form is opened on Users/User Groups * @param dialog */ featureAclFormOpen: function(dialog) { // Start everything collapsed. $(dialog).find('tr.feature-row').hide(); // Bind to clicking on the feature header cells $(dialog).find('td.feature-group-header-cell').on('click', function(ev) { // Toggle state const $header = $(ev.currentTarget); const isOpen = $header.hasClass('open'); if (isOpen) { // Make closed $header.find('.feature-group-description').show(); $header.find('i.fa').removeClass('fa-arrow-circle-up') .addClass('fa fa-arrow-circle-down'); $header.closest('tbody.feature-group').find('tr.feature-row').hide(); $header.removeClass('open').addClass('closed'); } else { // Make open $header.find('.feature-group-description').hide(); $header.find('i.fa').removeClass('fa-arrow-circle-down') .addClass('fa fa-arrow-circle-up'); $header.closest('tbody.feature-group').find('tr.feature-row').show(); $header.removeClass('closed').addClass('open'); } }).each(function(index, el) { // Set the initial state of the 3 way checkboxes setFeatureGroupCheckboxState($(el)); }); // Bind to checkbox change event $(dialog).find('input[name=\'features[]\']').on('click', function(ev) { setFeatureGroupCheckboxState($(ev.currentTarget)); }); // Bind to group checkboxes to check/uncheck all below. $(dialog).find('input.feature-select-all').on('click', function(ev) { // Force this down to all child checkboxes $(ev.currentTarget) .closest('tbody.feature-group') .find('input[name=\'features[]\']') .prop('checked', $(ev.currentTarget).is(':checked')); }); }, userApprovedApplicationsFormOpen: function(dialog) { $('.revokeAccess').on('click', function(e) { const $this = $(e.currentTarget); const clientKey = $this.data('applicationKey'); const userId = $this.data('applicationUser'); $.ajax({ url: revokeApplicationAccess.replace(':id', clientKey) .replace(':userId', userId), type: 'DELETE', success: function(res) { if (res.success) { $this.closest('tr').remove(); toastr.success(res.message); } else { toastr.error(res.message); } }, }); }); }, folderMoveSubmit: function() { XiboFormSubmit($('#moveFolderForm'), null, function(xhr, form) { if (xhr.success) { $('#container-folder-tree').jstree(true).refresh(); } }); }, validateForm: function( form, container, options, ) { const defaultOptions = { submitHandler: options.submitHandler, errorElement: 'span', // Ignore the date picker helpers ignore: '.datePickerHelper', errorPlacement: function(error, element) { if ($(element).hasClass('dateControl')) { // Places the error label date controller error.insertAfter(element.parent()); } else if ( $(element).siblings('.validation-error-container').length > 0 ) { error.appendTo($(element).siblings('.validation-error-container')); } else { // Places the error label after the invalid element error.insertAfter(element); } }, highlight: function(element) { $(element).closest('.form-group') .removeClass('has-success') .addClass('has-error'); }, success: function(element) { $(element).closest('.form-group') .removeClass('has-error') .addClass('has-success'); }, invalidHandler: function(event, validator) { // Mark non active tabs with error if they have an invalid element $('.nav-item a.nav-link').removeClass('has-error'); validator.errorList.forEach((error) => { const element = error.element; $(element).parents('.tab-pane').each((_id, tab) => { $('.nav-item a.nav-link[href="#' + $(tab).attr('id') + '"]:not(.active)') .addClass('has-error'); }); }); // Remove the spinner $(this).closest('.modal-dialog').find('.saving').remove(); // https://github.com/xibosignage/xibo/issues/1589 $(this).closest('.modal-dialog').find('.save-button') .removeClass('disabled'); }, }; // Merge options with defaults Object.assign( defaultOptions, options, ); // Init validator const validatorObj = form.validate(defaultOptions); // If we are in a modal, validate on tab change container.find('.nav-link').on('shown.bs.tab', () => { validatorObj.form(); }); }, };