First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,30 @@
CRM.$(function($) {
'use strict';
function assignAutoComplete(id_field, profileids) {
$('#' + id_field).on('change', function (event, data) {
var contactID = $(this).val();
CRM.api3('profile', 'get', {'profile_id': profileids, 'contact_id': contactID})
.done(function (result) {
$.each(result.values, function (id, value) {
$.each(value, function (fieldname, fieldvalue) {
$('#' + fieldname).val(fieldvalue).change();
$("[name=" + fieldname + "]").val([fieldvalue]);
if ($.isArray(fieldvalue)) {
$.each(fieldvalue, function (index, val) {
$("#" + fieldname + "_" + val).prop('checked', true);
});
}
});
});
}
);
});
}
$(CRM.form.autocompletes).each(function (index, autocomplete) {
assignAutoComplete(autocomplete.id_field, CRM.ids.profile || []);
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
// crmResource: Given a templateUrl "~/mymodule/myfile.html", load the matching HTML.
// This implementation loads all partials and strings in one batch.
(function(angular, $, _) {
angular.module('crmResource', []);
angular.module('crmResource').factory('crmResource', function($q, $http) {
var deferreds = {}; // null|object; deferreds[url][idx] = Deferred;
var templates = null; // null|object; templates[url] = HTML;
var notify = function notify() {
var oldDfrds = deferreds;
deferreds = null;
angular.forEach(oldDfrds, function(dfrs, url) {
if (templates[url]) {
angular.forEach(dfrs, function(dfr) {
dfr.resolve({
status: 200,
headers: function(name) {
var headers = {'Content-type': 'text/html'};
return name ? headers[name] : headers;
},
data: templates[url]
});
});
}
else {
angular.forEach(dfrs, function(dfr) {
dfr.reject({status: 500}); // FIXME
});
}
});
};
var moduleUrl = CRM.angular.bundleUrl;
$http.get(moduleUrl)
.success(function httpSuccess(data) {
templates = [];
angular.forEach(data, function(module) {
if (module.partials) {
angular.extend(templates, module.partials);
}
if (module.strings) {
CRM.addStrings(module.domain, module.strings);
}
});
notify();
})
.error(function httpError() {
templates = [];
notify();
});
return {
// @return string|Promise<string>
getUrl: function getUrl(url) {
if (templates !== null) {
return templates[url];
}
else {
var deferred = $q.defer();
if (!deferreds[url]) {
deferreds[url] = [];
}
deferreds[url].push(deferred);
return deferred.promise;
}
}
};
});
angular.module('crmResource').config(function($provide) {
$provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) {
var origGet = $delegate.get;
var urlPat = /^~\//;
$delegate.get = function(url) {
if (urlPat.test(url)) {
return crmResource.getUrl(url);
}
else {
return origGet.call(this, url);
}
};
return $delegate;
});
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,137 @@
// crmResource: Given a templateUrl "~/mymodule/myfile.html", load the matching HTML.
// This implementation loads partials and strings in per-module batches.
// FIXME: handling of CRM.strings not well tested; may be racy
(function(angular, $, _) {
angular.module('crmResource', []);
angular.module('crmResource').factory('crmResource', function($q, $http) {
var modules = {}; // moduleQueue[module] = 'loading'|Object;
var templates = {}; // templates[url] = HTML;
function CrmResourceModule(name) {
this.name = name;
this.status = 'new'; // loading|loaded|error
this.data = null;
this.deferreds = [];
}
angular.extend(CrmResourceModule.prototype, {
createDeferred: function createDeferred() {
var deferred = $q.defer();
switch (this.status) {
case 'new':
case 'loading':
this.deferreds.push(deferred);
break;
case 'loaded':
deferred.resolve(this.data);
break;
case 'error':
deferred.reject();
break;
default:
throw 'Unknown status: ' + this.status;
}
return deferred.promise;
},
load: function load() {
var module = this;
this.status = 'loading';
var moduleUrl = CRM.url('civicrm/ajax/angular-modules', {modules: module.name, l: CRM.config.lcMessages, r: CRM.angular.cacheCode});
$http.get(moduleUrl)
.success(function httpSuccess(data) {
if (data[module.name]) {
module.onSuccess(data[module.name]);
}
else {
module.onError();
}
})
.error(function httpError() {
module.onError();
});
},
onSuccess: function onSuccess(data) {
var module = this;
this.data = data;
this.status = 'loaded';
if (this.data.partials) {
angular.extend(templates, this.data.partials);
}
if (this.data.strings) {
CRM.addStrings(this.data.domain, this.data.strings);
}
angular.forEach(this.deferreds, function(deferred) {
deferred.resolve(module.data);
});
delete this.deferreds;
},
onError: function onError() {
this.status = 'error';
angular.forEach(this.deferreds, function(deferred) {
deferred.reject();
});
delete this.deferreds;
}
});
return {
// @return Promise<ModuleData>
getModule: function getModule(name) {
if (!modules[name]) {
modules[name] = new CrmResourceModule(name);
modules[name].load();
}
return modules[name].createDeferred();
},
// @return string|Promise<string>
getUrl: function getUrl(url) {
if (templates[url]) {
return templates[url];
}
var parts = url.split('/');
var deferred = $q.defer();
this.getModule(parts[1]).then(
function() {
if (templates[url]) {
deferred.resolve({
status: 200,
headers: function(name) {
var headers = {'Content-type': 'text/html'};
return name ? headers[name] : headers;
},
data: templates[url]
});
}
else {
deferred.reject({status: 500}); // FIXME
}
},
function() {
deferred.reject({status: 500}); // FIXME
}
);
return deferred.promise;
}
};
});
angular.module('crmResource').config(function($provide) {
$provide.decorator('$templateCache', function($delegate, $http, $q, crmResource) {
var origGet = $delegate.get;
var urlPat = /^~\//;
$delegate.get = function(url) {
if (urlPat.test(url)) {
return crmResource.getUrl(url);
}
else {
return origGet.call(this, url);
}
};
return $delegate;
});
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,8 @@
// http://civicrm.org/licensing
// Opens the "new item" dialog after creating a container/set
CRM.$(function($) {
var emptyMsg = $('.crm-empty-table');
if (emptyMsg.length) {
$('.action-link a.button', '#crm-container').click();
}
});

View file

@ -0,0 +1,21 @@
// https://civicrm.org/licensing
(function($) {
"use strict";
$(document)
.on('crmLoad', function(e) {
$('.crm-icon-picker', e.target).not('.iconpicker-widget').each(function() {
var $el = $(this);
CRM.loadScript(CRM.config.resourceBase + 'js/jquery/jquery.crmIconPicker.js').done(function() {
$el.crmIconPicker();
});
// Hack to get the strings in this lazy-loaded file translated
ts('None');
ts('Normal');
ts('Rotate right');
ts('Rotate left');
ts('Rotate 180');
ts('Flip horizontal');
ts('Flip vertical');
});
});
})(CRM.$);

View file

@ -0,0 +1,588 @@
// https://civicrm.org/licensing
/**
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface
* @see https://wiki.civicrm.org/confluence/display/CRMDOC/Ajax+Pages+and+Forms
*/
(function($, CRM, undefined) {
/**
* @param string path
* @param string|object query
* @param string mode - optionally specify "front" or "back"
*/
var tplURL;
CRM.url = function (path, query, mode) {
if (typeof path === 'object') {
tplURL = path;
return path;
}
if (!tplURL) {
CRM.console('error', 'Error: CRM.url called before initialization');
}
if (!mode) {
mode = CRM.config && CRM.config.isFrontend ? 'front' : 'back';
}
query = query || '';
var frag = path.split('?');
var url = tplURL[mode].replace("*path*", frag[0]);
if (!query) {
url = url.replace(/[?&]\*query\*/, '');
}
else {
url = url.replace("*query*", typeof query === 'string' ? query : $.param(query));
}
if (frag[1]) {
url += (url.indexOf('?') < 0 ? '?' : '&') + frag[1];
}
return url;
};
$.fn.crmURL = function () {
return this.each(function() {
if (this.href) {
this.href = CRM.url(this.href);
}
});
};
/**
* AJAX api
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/AJAX+Interface#AJAXInterface-CRM.api3
*/
CRM.api3 = function(entity, action, params, status) {
if (typeof(entity) === 'string') {
params = {
entity: entity,
action: action.toLowerCase(),
json: JSON.stringify(params || {})
};
} else {
params = {
entity: 'api3',
action: 'call',
json: JSON.stringify(entity)
};
status = action;
}
var ajax = $.ajax({
url: CRM.url('civicrm/ajax/rest'),
dataType: 'json',
data: params,
type: params.action.indexOf('get') < 0 ? 'POST' : 'GET'
});
if (status) {
// Default status messages
if (status === true) {
status = {success: params.action === 'delete' ? ts('Removed') : ts('Saved')};
if (params.action.indexOf('get') === 0) {
status.start = ts('Loading...');
status.success = null;
}
}
var messages = status === true ? {} : status;
CRM.status(status, ajax);
}
return ajax;
};
/**
* @deprecated
* AJAX api
*/
CRM.api = function(entity, action, params, options) {
// Default settings
var settings = {
context: null,
success: function(result, settings) {
return true;
},
error: function(result, settings) {
$().crmError(result.error_message, ts('Error'));
return false;
},
callBack: function(result, settings) {
if (result.is_error == 1) {
return settings.error.call(this, result, settings);
}
return settings.success.call(this, result, settings);
},
ajaxURL: 'civicrm/ajax/rest'
};
action = action.toLowerCase();
// Default success handler
switch (action) {
case "update":
case "create":
case "setvalue":
case "replace":
settings.success = function() {
CRM.status(ts('Saved'));
return true;
};
break;
case "delete":
settings.success = function() {
CRM.status(ts('Removed'));
return true;
};
}
params = {
entity: entity,
action: action,
json: JSON.stringify(params)
};
// Pass copy of settings into closure to preserve its value during multiple requests
(function(stg) {
$.ajax({
url: stg.ajaxURL.indexOf('http') === 0 ? stg.ajaxURL : CRM.url(stg.ajaxURL),
dataType: 'json',
data: params,
type: action.indexOf('get') < 0 ? 'POST' : 'GET',
success: function(result) {
stg.callBack.call(stg.context, result, stg);
}
});
})($.extend({}, settings, options));
};
$.widget('civi.crmSnippet', {
options: {
url: null,
block: true,
crmForm: null
},
_originalContent: null,
_originalUrl: null,
isOriginalUrl: function() {
var
args = {},
same = true,
newUrl = this._formatUrl(this.options.url),
oldUrl = this._formatUrl(this._originalUrl);
// Compare path
if (newUrl.split('?')[0] !== oldUrl.split('?')[0]) {
return false;
}
// Compare arguments
$.each(newUrl.split('?')[1].split('&'), function(k, v) {
var arg = v.split('=');
args[arg[0]] = arg[1];
});
$.each(oldUrl.split('?')[1].split('&'), function(k, v) {
var arg = v.split('=');
if (args[arg[0]] !== undefined && arg[1] !== args[arg[0]]) {
same = false;
}
});
return same;
},
resetUrl: function() {
this.options.url = this._originalUrl;
},
_create: function() {
this.element.addClass('crm-ajax-container');
if (!this.element.is('.crm-container *')) {
this.element.addClass('crm-container');
}
this._handleOrderLinks();
// Set default if not supplied
this.options.url = this.options.url || document.location.href;
this._originalUrl = this.options.url;
},
_onFailure: function(data, status) {
var msg, title = ts('Network Error');
if (this.options.block) this.element.unblock();
this.element.trigger('crmAjaxFail', data);
switch (status) {
case 'Forbidden':
title = ts('Access Denied');
msg = ts('Ensure you are still logged in and have permission to access this feature.');
break;
default:
msg = ts('Unable to reach the server. Please refresh this page in your browser and try again.');
}
CRM.alert(msg, title, 'error');
},
_onError: function(data) {
this.element.attr('data-unsaved-changes', 'false').trigger('crmAjaxError', data);
if (this.options.crmForm && this.options.crmForm.autoClose && this.element.data('uiDialog')) {
this.element.dialog('close');
}
},
_formatUrl: function(url, snippetType) {
// Strip hash
url = url.split('#')[0];
// Add snippet argument to url
if (snippetType) {
if (url.search(/[&?]snippet=/) < 0) {
url += (url.indexOf('?') < 0 ? '?' : '&') + 'snippet=' + snippetType;
} else {
url = url.replace(/snippet=[^&]*/, 'snippet=' + snippetType);
}
}
return url;
},
// Hack to deal with civicrm legacy sort functionality
_handleOrderLinks: function() {
var that = this;
$('a.crm-weight-arrow', that.element).click(function(e) {
if (that.options.block) that.element.block();
$.getJSON(that._formatUrl(this.href, 'json')).done(function() {
that.refresh();
});
e.stopImmediatePropagation();
return false;
});
},
refresh: function() {
var that = this;
var url = this._formatUrl(this.options.url, 'json');
if (this.options.crmForm) $('form', this.element).ajaxFormUnbind();
if (this.options.block) this.element.block();
$.getJSON(url, function(data) {
if (data.status === 'redirect') {
that.options.url = data.userContext;
return that.refresh();
}
if (that.options.block) that.element.unblock();
if (!$.isPlainObject(data)) {
that._onFailure(data);
return;
}
if (data.status === 'error') {
that._onError(data);
return;
}
data.url = url;
that.element.trigger('crmUnload').trigger('crmBeforeLoad', data);
that._beforeRemovingContent();
that.element.html(data.content);
that._handleOrderLinks();
that.element.trigger('crmLoad', data);
if (that.options.crmForm) that.element.trigger('crmFormLoad', data);
// This is only needed by forms that load via ajax but submit without ajax, e.g. configure contribution page tabs
// TODO: remove this when those forms have been converted to use ajax submit
if (data.status === 'form_error' && $.isPlainObject(data.errors)) {
that.element.trigger('crmFormError', data);
$.each(data.errors, function(formElement, msg) {
$('[name="'+formElement+'"]', that.element).crmError(msg);
});
}
}).fail(function(data, msg, status) {
that._onFailure(data, status);
});
},
// Perform any cleanup needed before removing/replacing content
_beforeRemovingContent: function() {
var that = this;
// Save original content to be restored if widget is destroyed
if (this._originalContent === null) {
$('.blockUI', this.element).remove();
this._originalContent = this.element.contents().detach();
}
if (this.options.crmForm) $('form', this.element).ajaxFormUnbind();
},
_destroy: function() {
this.element.removeClass('crm-ajax-container').trigger('crmUnload');
this._beforeRemovingContent();
if (this._originalContent !== null) {
this.element.empty().append(this._originalContent);
}
}
});
var dialogCount = 0,
exclude = '[href^=#], [href^=javascript], [onclick], .no-popup, .cancel';
CRM.loadPage = function(url, options) {
var settings = {
target: '#crm-ajax-dialog-' + (dialogCount++),
dialog: (options && options.target) ? false : {}
};
if (options) $.extend(true, settings, options);
settings.url = url;
// Create new dialog
if (settings.dialog) {
settings.dialog = CRM.utils.adjustDialogDefaults(settings.dialog);
$('<div id="' + settings.target.substring(1) + '"></div>')
.dialog(settings.dialog)
.parent().find('.ui-dialog-titlebar')
.append($('<a class="crm-dialog-titlebar-print ui-dialog-titlebar-close" title="'+ts('Print window')+'" target="_blank" style="right:3.8em;"/>')
.button({icons: {primary: 'fa-print'}, text: false}));
}
// Add handlers to new or existing dialog
if ($(settings.target).data('uiDialog')) {
$(settings.target)
.on('dialogclose', function() {
if (settings.dialog && $(this).attr('data-unsaved-changes') !== 'true') {
$(this).crmSnippet('destroy').dialog('destroy').remove();
}
})
.on('crmLoad', function(e, data) {
// Set title
if (e.target === $(settings.target)[0] && data && !settings.dialog.title && data.title) {
$(this).dialog('option', 'title', data.title);
}
// Update print url
$(this).parent().find('a.crm-dialog-titlebar-print').attr('href', $(this).data('civiCrmSnippet')._formatUrl($(this).crmSnippet('option', 'url'), '2'));
});
}
$(settings.target).crmSnippet(settings).crmSnippet('refresh');
return $(settings.target);
};
CRM.loadForm = function(url, options) {
var formErrors = [], settings = {
crmForm: {
ajaxForm: {},
autoClose: true,
validate: true,
refreshAction: ['next_new', 'submit_savenext', 'upload_new'],
cancelButton: '.cancel',
openInline: 'a.open-inline, a.button, a.action-item, a.open-inline-noreturn',
onCancel: function(event) {}
}
};
// Move options that belong to crmForm. Others will be passed through to crmSnippet
if (options) $.each(options, function(key, value) {
if (typeof(settings.crmForm[key]) !== 'undefined') {
settings.crmForm[key] = value;
}
else {
settings[key] = value;
}
});
var widget = CRM.loadPage(url, settings).off('.crmForm');
// CRM-14353 - Warn of unsaved changes for all forms except those which have opted out
function cancelAction() {
var dirty = CRM.utils.initialValueChanged($('form:not([data-warn-changes=false])', widget));
widget.attr('data-unsaved-changes', dirty ? 'true' : 'false');
if (dirty) {
var id = widget.attr('id') + '-unsaved-alert',
title = widget.dialog('option', 'title'),
alert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: title}) + '</p><p><a href="#" id="' + id + '">' + ts('Restore') + '</a></p>', ts('Unsaved Changes'), 'alert unsaved-dialog', {expires: 60000});
$('#' + id).button({icons: {primary: 'fa-undo'}}).click(function(e) {
widget.attr('data-unsaved-changes', 'false').dialog('open');
e.preventDefault();
});
}
}
if (widget.data('uiDialog')) widget.on('dialogbeforeclose', function(e) {
// CRM-14353 - Warn unsaved changes if user clicks close button or presses "esc"
if (e.originalEvent) {
cancelAction();
}
});
widget.on('crmFormLoad.crmForm', function(event, data) {
var $el = $(this).attr('data-unsaved-changes', 'false'),
settings = $el.crmSnippet('option', 'crmForm');
if (settings.cancelButton) $(settings.cancelButton, this).click(function(e) {
e.preventDefault();
var returnVal = settings.onCancel.call($el, e);
if (returnVal !== false) {
$el.trigger('crmFormCancel', e);
if ($el.data('uiDialog') && settings.autoClose) {
cancelAction();
$el.dialog('close');
}
else if (!settings.autoClose) {
$el.crmSnippet('resetUrl').crmSnippet('refresh');
}
}
});
if (settings.validate) {
$("form", this).crmValidate();
}
$("form:not('[data-no-ajax-submit=true]')", this).ajaxForm($.extend({
url: data.url.replace(/reset=1[&]?/, ''),
dataType: 'json',
success: function(response) {
if (response.content === undefined) {
$el.trigger('crmFormSuccess', response);
// Reset form for e.g. "save and new"
if (response.userContext && (response.status === 'redirect' || (settings.refreshAction && $.inArray(response.buttonName, settings.refreshAction) >= 0))) {
// Force reset of original url
$el.data('civiCrmSnippet')._originalUrl = response.userContext;
$el.crmSnippet('resetUrl').crmSnippet('refresh');
}
// Close if we are on the original url or the action was "delete" (in which case returning to view may be inappropriate)
else if ($el.data('uiDialog') && (settings.autoClose || response.action === 8)) {
$el.dialog('close');
}
else if (settings.autoClose === false) {
$el.crmSnippet('resetUrl').crmSnippet('refresh');
}
}
else {
if ($el.crmSnippet('option', 'block')) $el.unblock();
response.url = data.url;
$el.html(response.content).trigger('crmLoad', response).trigger('crmFormLoad', response);
if (response.status === 'form_error') {
formErrors = [];
$el.trigger('crmFormError', response);
$.each(response.errors || [], function(formElement, msg) {
formErrors.push($('[name="'+formElement+'"]', $el).crmError(msg));
});
}
}
},
beforeSubmit: function(submission) {
$.each(formErrors, function() {
if (this && this.close) this.close();
});
if ($el.crmSnippet('option', 'block')) $el.block();
$el.trigger('crmFormSubmit', submission);
}
}, settings.ajaxForm));
if (settings.openInline) {
settings.autoClose = $el.crmSnippet('isOriginalUrl');
$(this).off('.openInline').on('click.openInline', settings.openInline, function(e) {
if ($(this).is(exclude + ', .crm-popup')) {
return;
}
if ($(this).hasClass('open-inline-noreturn')) {
// Force reset of original url
$el.data('civiCrmSnippet')._originalUrl = $(this).attr('href');
}
$el.crmSnippet('option', 'url', $(this).attr('href')).crmSnippet('refresh');
e.preventDefault();
});
}
if ($el.data('uiDialog')) {
// Show form buttons as part of the dialog
var buttonContainers = '.crm-submit-buttons, .action-link',
buttons = [],
added = [];
$(buttonContainers, $el).find('input.crm-form-submit, a.button').each(function() {
var $el = $(this),
label = $el.is('input') ? $el.attr('value') : $el.text(),
identifier = $el.attr('name') || $el.attr('href');
if (!identifier || identifier === '#' || $.inArray(identifier, added) < 0) {
var $icon = $el.find('.icon, .crm-i'),
button = {'data-identifier': identifier, text: label, click: function() {
$el[0].click();
}};
if ($icon.length) {
button.icons = {primary: $icon.attr('class')};
} else {
var action = $el.attr('crm-icon') || ($el.hasClass('cancel') ? 'fa-times' : 'fa-check');
button.icons = {primary: action};
}
buttons.push(button);
added.push(identifier);
}
// display:none causes the form to not submit when pressing "enter"
$el.parents(buttonContainers).css({height: 0, padding: 0, margin: 0, overflow: 'hidden'}).find('.crm-button-icon').hide();
});
$el.dialog('option', 'buttons', buttons);
}
// Allow a button to prevent ajax submit
$('input[data-no-ajax-submit=true]').click(function() {
$(this).closest('form').ajaxFormUnbind();
});
// For convenience, focus the first field
$('input[type=text], textarea, select', this).filter(':visible').first().not('.dateplugin').focus();
});
return widget;
};
/**
* Handler for jQuery click event e.g. $('a').click(CRM.popup);
*/
CRM.popup = function(e) {
var $el = $(this).first(),
url = $el.attr('href'),
popup = $el.data('popup-type') === 'page' ? CRM.loadPage : CRM.loadForm,
settings = $el.data('popup-settings') || {},
formData = false;
settings.dialog = settings.dialog || {};
if (e.isDefaultPrevented() || !CRM.config.ajaxPopupsEnabled || !url || $el.is(exclude + ', .open-inline, .open-inline-noreturn')) {
return;
}
// Sized based on css class
if ($el.hasClass('small-popup')) {
settings.dialog.width = 400;
settings.dialog.height = 300;
}
else if ($el.hasClass('medium-popup')) {
settings.dialog.width = settings.dialog.height = '50%';
}
var dialog = popup(url, settings);
// Trigger events from the dialog on the original link element
$el.trigger('crmPopupOpen', [dialog]);
// Listen for success events and buffer them so we only trigger once
dialog.on('crmFormSuccess.crmPopup crmPopupFormSuccess.crmPopup', function(e, data) {
formData = data;
});
dialog.on('dialogclose.crmPopup', function(e, data) {
if (formData) {
$el.trigger('crmPopupFormSuccess', [dialog, formData]);
}
$el.trigger('crmPopupClose', [dialog, data]);
});
e.preventDefault();
};
/**
* An event callback for CRM.popup or a standalone function to refresh the content around a given element
* @param e {event|selector}
*/
CRM.refreshParent = function(e) {
// Use e.target if input smells like an event, otherwise assume it's a jQuery selector
var $el = (e.stopPropagation && e.target) ? $(e.target) : $(e),
$table = $el.closest('.dataTable');
// Call native refresh method on ajax datatables
if ($table.length && $.fn.DataTable.fnIsDataTable($table[0]) && $table.dataTable().fnSettings().sAjaxSource) {
// Refresh ALL datatables - needed for contact relationship tab
$.each($.fn.dataTable.fnTables(), function() {
if ($(this).dataTable().fnSettings().sAjaxSource) $(this).unblock().dataTable().fnDraw();
});
}
// Otherwise refresh the nearest crmSnippet
else {
$el.closest('.crm-ajax-container, #crm-main-content-wrapper').crmSnippet().crmSnippet('refresh');
}
};
$(function($) {
$('body')
.on('click', 'a.crm-popup', CRM.popup)
// Close unsaved dialog messages
.on('dialogopen', function(e) {
$('.alert.unsaved-dialog .ui-notify-cross', '#crm-notification-container').click();
})
// Destroy old unsaved dialog
.on('dialogcreate', function(e) {
$('.ui-dialog-content.crm-ajax-container:hidden[data-unsaved-changes=true]').crmSnippet('destroy').dialog('destroy').remove();
})
// Ensure wysiwyg content is updated prior to ajax submit
.on('form-pre-serialize', function(e) {
$('.crm-wysiwyg-enabled', e.target).each(function() {
CRM.wysiwyg.updateElement(this);
});
})
// Auto-resize dialogs when loading content
.on('crmLoad dialogopen', 'div.ui-dialog.ui-resizable.crm-container', function(e) {
var
$wrapper = $(this),
$dialog = $wrapper.children('.ui-dialog-content');
// small delay to allow contents to render
window.setTimeout(function() {
var currentHeight = $wrapper.outerHeight(),
padding = currentHeight - $dialog.height(),
newHeight = $dialog.prop('scrollHeight') + padding,
menuHeight = $('#civicrm-menu').outerHeight(),
maxHeight = $(window).height() - menuHeight;
newHeight = newHeight > maxHeight ? maxHeight : newHeight;
if (newHeight > (currentHeight + 15)) {
$dialog.dialog('option', {
position: {my: 'center', at: 'center center+' + (menuHeight / 2), of: window},
height: newHeight
});
}
}, 500);
});
});
}(jQuery, CRM));

View file

@ -0,0 +1,7 @@
(function (angular, $, _) {
// DEPRECATED: A variant of angular.module() which uses a dependency list provided by the server.
// REMOVE circa v4.7.22.
angular.crmDepends = function crmDepends(name) {
return angular.module(name, CRM.angRequires(name));
};
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,572 @@
(function($, _) {
if (!CRM.Backbone) CRM.Backbone = {};
/**
* Backbone.sync provider which uses CRM.api() for I/O.
* To support CRUD operations, model classes must be defined with a "crmEntityName" property.
* To load collections using API queries, set the "crmCriteria" property or override the
* method "toCrmCriteria".
*
* @param method Accepts normal Backbone.sync methods; also accepts "crm-replace"
* @param model
* @param options
* @see tests/qunit/crm-backbone
*/
CRM.Backbone.sync = function(method, model, options) {
var isCollection = _.isArray(model.models);
var apiOptions, params;
if (isCollection) {
apiOptions = {
success: function(data) {
// unwrap data
options.success(_.toArray(data.values));
},
error: function(data) {
// CRM.api displays errors by default, but Backbone.sync
// protocol requires us to override "error". This restores
// the default behavior.
$().crmError(data.error_message, ts('Error'));
options.error(data);
}
};
switch (method) {
case 'read':
CRM.api(model.crmEntityName, model.toCrmAction('get'), model.toCrmCriteria(), apiOptions);
break;
// replace all entities matching "x.crmCriteria" with new entities in "x.models"
case 'crm-replace':
params = this.toCrmCriteria();
params.version = 3;
params.values = this.toJSON();
CRM.api(model.crmEntityName, model.toCrmAction('replace'), params, apiOptions);
break;
default:
apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for collections"});
break;
}
} else {
// callback options to pass to CRM.api
apiOptions = {
success: function(data) {
// unwrap data
var values = _.toArray(data.values);
if (data.count == 1) {
options.success(values[0]);
} else {
data.is_error = 1;
data.error_message = ts("Expected exactly one response");
apiOptions.error(data);
}
},
error: function(data) {
// CRM.api displays errors by default, but Backbone.sync
// protocol requires us to override "error". This restores
// the default behavior.
$().crmError(data.error_message, ts('Error'));
options.error(data);
}
};
switch (method) {
case 'create': // pass-through
case 'update':
params = model.toJSON();
if (!params.options) params.options = {};
params.options.reload = 1;
if (!model._isDuplicate) {
CRM.api(model.crmEntityName, model.toCrmAction('create'), params, apiOptions);
} else {
CRM.api(model.crmEntityName, model.toCrmAction('duplicate'), params, apiOptions);
}
break;
case 'read':
case 'delete':
var apiAction = (method == 'delete') ? 'delete' : 'get';
params = model.toCrmCriteria();
if (!params.id) {
apiOptions.error({is_error: 1, error_message: 'Missing ID for ' + model.crmEntityName});
return;
}
CRM.api(model.crmEntityName, model.toCrmAction(apiAction), params, apiOptions);
break;
default:
apiOptions.error({is_error: 1, error_message: "CRM.Backbone.sync(" + method + ") not implemented for models"});
}
}
};
/**
* Connect a "model" class to CiviCRM's APIv3
*
* @code
* // Setup class
* var ContactModel = Backbone.Model.extend({});
* CRM.Backbone.extendModel(ContactModel, "Contact");
*
* // Use class
* c = new ContactModel({id: 3});
* c.fetch();
* @endcode
*
* @param Class ModelClass
* @param string crmEntityName APIv3 entity name, such as "Contact" or "CustomField"
* @see tests/qunit/crm-backbone
*/
CRM.Backbone.extendModel = function(ModelClass, crmEntityName) {
// Defaults - if specified in ModelClass, preserve
_.defaults(ModelClass.prototype, {
crmEntityName: crmEntityName,
crmActions: {}, // map: string backboneActionName => string serverSideActionName
crmReturn: null, // array: list of fields to return
toCrmAction: function(action) {
return this.crmActions[action] ? this.crmActions[action] : action;
},
toCrmCriteria: function() {
var result = (this.get('id')) ? {id: this.get('id')} : {};
if (!_.isEmpty(this.crmReturn)) {
result.return = this.crmReturn;
}
return result;
},
duplicate: function() {
var newModel = new ModelClass(this.toJSON());
newModel._isDuplicate = true;
if (newModel.setModified) newModel.setModified();
newModel.listenTo(newModel, 'sync', function(){
// may get called on subsequent resaves -- don't care!
delete newModel._isDuplicate;
});
return newModel;
}
});
// Overrides - if specified in ModelClass, replace
_.extend(ModelClass.prototype, {
sync: CRM.Backbone.sync
});
};
/**
* Configure a model class to track whether a model has unsaved changes.
*
* Methods:
* - setModified() - flag the model as modified/dirty
* - isSaved() - return true if there have been no changes to the data since the last fetch or save
* Events:
* - saved(object model, bool is_saved) - triggered whenever isSaved() value would change
*
* Note: You should not directly call isSaved() within the context of the success/error/sync callback;
* I haven't found a way to make isSaved() behave correctly within these callbacks without patching
* Backbone. Instead, attach an event listener to the 'saved' event.
*
* @param ModelClass
*/
CRM.Backbone.trackSaved = function(ModelClass) {
// Retain references to some of the original class's functions
var Parent = _.pick(ModelClass.prototype, 'initialize', 'save', 'fetch');
// Private callback
var onSyncSuccess = function() {
this._modified = false;
if (this._oldModified.length > 0) {
this._oldModified.pop();
}
this.trigger('saved', this, this.isSaved());
};
var onSaveError = function() {
if (this._oldModified.length > 0) {
this._modified = this._oldModified.pop();
this.trigger('saved', this, this.isSaved());
}
};
// Defaults - if specified in ModelClass, preserve
_.defaults(ModelClass.prototype, {
isSaved: function() {
var result = !this.isNew() && !this.isModified();
return result;
},
isModified: function() {
return this._modified;
},
_saved_onchange: function(model, options) {
if (options.parse) return;
// console.log('change', model.changedAttributes(), model.previousAttributes());
this.setModified();
},
setModified: function() {
var oldModified = this._modified;
this._modified = true;
if (!oldModified) {
this.trigger('saved', this, this.isSaved());
}
}
});
// Overrides - if specified in ModelClass, replace
_.extend(ModelClass.prototype, {
initialize: function(options) {
this._modified = false;
this._oldModified = [];
this.listenTo(this, 'change', this._saved_onchange);
this.listenTo(this, 'error', onSaveError);
this.listenTo(this, 'sync', onSyncSuccess);
if (Parent.initialize) {
return Parent.initialize.apply(this, arguments);
}
},
save: function() {
// we'll assume success
this._oldModified.push(this._modified);
return Parent.save.apply(this, arguments);
},
fetch: function() {
this._oldModified.push(this._modified);
return Parent.fetch.apply(this, arguments);
}
});
};
/**
* Configure a model class to support client-side soft deletion.
* One can call "model.setDeleted(BOOLEAN)" to flag an entity for
* deletion (or not) -- however, deletion will be deferred until save()
* is called.
*
* Methods:
* setSoftDeleted(boolean) - flag the model as deleted (or not-deleted)
* isSoftDeleted() - determine whether model has been soft-deleted
* Events:
* softDelete(model, is_deleted) -- change value of is_deleted
*
* @param ModelClass
*/
CRM.Backbone.trackSoftDelete = function(ModelClass) {
// Retain references to some of the original class's functions
var Parent = _.pick(ModelClass.prototype, 'save');
// Defaults - if specified in ModelClass, preserve
_.defaults(ModelClass.prototype, {
is_soft_deleted: false,
setSoftDeleted: function(is_deleted) {
if (this.is_soft_deleted != is_deleted) {
this.is_soft_deleted = is_deleted;
this.trigger('softDelete', this, is_deleted);
if (this.setModified) this.setModified(); // FIXME: ugly interaction, trackSoftDelete-trackSaved
}
},
isSoftDeleted: function() {
return this.is_soft_deleted;
}
});
// Overrides - if specified in ModelClass, replace
_.extend(ModelClass.prototype, {
save: function(attributes, options) {
if (this.isSoftDeleted()) {
return this.destroy(options);
} else {
return Parent.save.apply(this, arguments);
}
}
});
};
/**
* Connect a "collection" class to CiviCRM's APIv3
*
* Note: the collection supports a special property, crmCriteria, which is an array of
* query options to send to the API.
*
* @code
* // Setup class
* var ContactModel = Backbone.Model.extend({});
* CRM.Backbone.extendModel(ContactModel, "Contact");
* var ContactCollection = Backbone.Collection.extend({
* model: ContactModel
* });
* CRM.Backbone.extendCollection(ContactCollection);
*
* // Use class (with passive criteria)
* var c = new ContactCollection([], {
* crmCriteria: {contact_type: 'Organization'}
* });
* c.fetch();
* c.get(123).set('property', 'value');
* c.get(456).setDeleted(true);
* c.save();
*
* // Use class (with active criteria)
* var criteriaModel = new SomeModel({
* contact_type: 'Organization'
* });
* var c = new ContactCollection([], {
* crmCriteriaModel: criteriaModel
* });
* c.fetch();
* c.get(123).set('property', 'value');
* c.get(456).setDeleted(true);
* c.save();
* @endcode
*
*
* @param Class CollectionClass
* @see tests/qunit/crm-backbone
*/
CRM.Backbone.extendCollection = function(CollectionClass) {
var origInit = CollectionClass.prototype.initialize;
// Defaults - if specified in CollectionClass, preserve
_.defaults(CollectionClass.prototype, {
crmEntityName: CollectionClass.prototype.model.prototype.crmEntityName,
crmActions: {}, // map: string backboneActionName => string serverSideActionName
toCrmAction: function(action) {
return this.crmActions[action] ? this.crmActions[action] : action;
},
toCrmCriteria: function() {
var result = (this.crmCriteria) ? _.extend({}, this.crmCriteria) : {};
if (!_.isEmpty(this.crmReturn)) {
result.return = this.crmReturn;
} else if (this.model && !_.isEmpty(this.model.prototype.crmReturn)) {
result.return = this.model.prototype.crmReturn;
}
return result;
},
/**
* Get an object which represents this collection's criteria
* as a live model. Any changes to the model will be applied
* to the collection, and the collection will be refreshed.
*
* @param criteriaModelClass
*/
setCriteriaModel: function(criteriaModel) {
var collection = this;
this.crmCriteria = criteriaModel.toJSON();
this.listenTo(criteriaModel, 'change', function() {
collection.crmCriteria = criteriaModel.toJSON();
collection.debouncedFetch();
});
},
debouncedFetch: _.debounce(function() {
this.fetch({reset: true});
}, 100),
/**
* Reconcile the server's collection with the client's collection.
* New/modified items from the client will be saved/updated on the
* server. Deleted items from the client will be deleted on the
* server.
*
* @param Object options - accepts "success" and "error" callbacks
*/
save: function(options) {
if (!options) options = {};
var collection = this;
var success = options.success;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
collection.reset(resp, options);
if (success) success(collection, resp, options);
// collection.trigger('sync', collection, resp, options);
};
wrapError(collection, options);
return this.sync('crm-replace', this, options);
}
});
// Overrides - if specified in CollectionClass, replace
_.extend(CollectionClass.prototype, {
sync: CRM.Backbone.sync,
initialize: function(models, options) {
if (!options) options = {};
if (options.crmCriteriaModel) {
this.setCriteriaModel(options.crmCriteriaModel);
} else if (options.crmCriteria) {
this.crmCriteria = options.crmCriteria;
}
if (options.crmActions) {
this.crmActions = _.extend(this.crmActions, options.crmActions);
}
if (origInit) {
return origInit.apply(this, arguments);
}
},
toJSON: function() {
var result = [];
// filter models list, excluding any soft-deleted items
this.each(function(model) {
// if model doesn't track soft-deletes
// or if model tracks soft-deletes and wasn't soft-deleted
if (!model.isSoftDeleted || !model.isSoftDeleted()) {
result.push(model.toJSON());
}
});
return result;
}
});
};
/**
* Find a single record, or create a new record.
*
* @param Object options:
* - CollectionClass: class
* - crmCriteria: Object values to search/default on
* - defaults: Object values to put on newly created model (if needed)
* - success: function(model)
* - error: function(collection, error)
*/
CRM.Backbone.findCreate = function(options) {
if (!options) options = {};
var collection = new options.CollectionClass([], {
crmCriteria: options.crmCriteria
});
collection.fetch({
success: function(collection) {
if (collection.length === 0) {
var attrs = _.extend({}, collection.crmCriteria, options.defaults || {});
var model = collection._prepareModel(attrs, options);
options.success(model);
} else if (collection.length == 1) {
options.success(collection.first());
} else {
options.error(collection, {
is_error: 1,
error_message: 'Too many matches'
});
}
},
error: function(collection, errorData) {
if (options.error) {
options.error(collection, errorData);
}
}
});
};
CRM.Backbone.Model = Backbone.Model.extend({
/**
* Return JSON version of model -- but only include fields that are
* listed in the 'schema'.
*
* @return {*}
*/
toStrictJSON: function() {
var schema = this.schema;
var result = this.toJSON();
_.each(result, function(value, key) {
if (!schema[key]) {
delete result[key];
}
});
return result;
},
setRel: function(key, value, options) {
this.rels = this.rels || {};
if (this.rels[key] != value) {
this.rels[key] = value;
this.trigger("rel:" + key, value);
}
},
getRel: function(key) {
return this.rels ? this.rels[key] : null;
}
});
CRM.Backbone.Collection = Backbone.Collection.extend({
/**
* Store 'key' on this.rel and automatically copy it to
* any children.
*
* @param key
* @param value
* @param initialModels
*/
initializeCopyToChildrenRelation: function(key, value, initialModels) {
this.setRel(key, value, {silent: true});
this.on('reset', this._copyToChildren, this);
this.on('add', this._copyToChild, this);
},
_copyToChildren: function() {
var collection = this;
collection.each(function(model) {
collection._copyToChild(model);
});
},
_copyToChild: function(model) {
_.each(this.rels, function(relValue, relKey) {
model.setRel(relKey, relValue, {silent: true});
});
},
setRel: function(key, value, options) {
this.rels = this.rels || {};
if (this.rels[key] != value) {
this.rels[key] = value;
this.trigger("rel:" + key, value);
}
},
getRel: function(key) {
return this.rels ? this.rels[key] : null;
}
});
/*
CRM.Backbone.Form = Backbone.Form.extend({
validate: function() {
// Add support for form-level validators
var errors = Backbone.Form.prototype.validate.apply(this, []) || {};
var self = this;
if (this.validators) {
_.each(this.validators, function(validator) {
var modelErrors = validator(this.getValue());
// The following if() has been copied-pasted from the parent's
// handling of model-validators. They are similar in that the errors are
// probably keyed by field names... but not necessarily, so we use _others
// as a fallback.
if (modelErrors) {
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
//If errors are not in object form then just store on the error object
if (!isDictionary) {
errors._others = errors._others || [];
errors._others.push(modelErrors);
}
//Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
if (isDictionary) {
_.each(modelErrors, function(val, key) {
//Set error on field if there isn't one already
if (self.fields[key] && !errors[key]) {
self.fields[key].setError(val);
errors[key] = val;
}
else {
//Otherwise add to '_others' key
errors._others = errors._others || [];
var tmpErr = {};
tmpErr[key] = val;
errors._others.push(tmpErr);
}
});
}
}
});
}
return _.isEmpty(errors) ? null : errors;
}
});
*/
// Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) {
var error = options.error;
options.error = function(resp) {
if (error) error(model, resp, optio);
model.trigger('error', model, resp, options);
};
};
})(CRM.$, CRM._);

View file

@ -0,0 +1,10 @@
// http://civicrm.org/licensing
CRM.$(function($) {
$('#admin-bar').css('display', 'none');
$('.crm-hidemenu').click(function(e) {
$('#admin-bar').css('display', 'block');
});
$('#crm-notification-container').on('click', '#crm-restore-menu', function() {
$('#admin-bar').css('display', 'none');
});
});

View file

@ -0,0 +1,33 @@
(function ($, _) {
$(function () {
/**
* FIXME we depend on this being a global singleton, mainly to facilitate vents
*
* vents:
* - resize: the size/position of widgets should be adjusted
* - ufUnsaved: any part of a UFGroup was changed; args: (is_changed:bool)
* - formOpened: a toggleable form (such as a UFFieldView or a UFGroupView) has been opened
*/
CRM.designerApp = new Backbone.Marionette.Application();
/**
* FIXME: Workaround for problem that having more than one instance
* of a profile on the page will result in duplicate DOM ids.
* @see CRM-12188
*/
CRM.designerApp.clearPreviewArea = function () {
$('.crm-profile-selector-preview-pane > *').each(function () {
var parent = $(this).parent();
CRM.designerApp.DetachedProfiles.push({
parent: parent,
item: $(this).detach()
});
});
};
CRM.designerApp.restorePreviewArea = function () {
$.each(CRM.designerApp.DetachedProfiles, function () {
$(this.parent).append(this.item);
});
};
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,22 @@
// http://civicrm.org/licensing
CRM.$(function($) {
$(document)
.on('dialogopen', function(e) {
// D7 hack to get the toolbar out of the way (CRM-15341)
$('#toolbar').css('z-index', '100');
})
.on('dialogclose', function(e) {
if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
// D7 hack, restore toolbar position (CRM-15341)
$('#toolbar').css('z-index', '');
}
})
// d8 Hack to hide title when it should be (CRM-19960)
.ready(function() {
var pageTitle = $('.page-title');
var title = $('.page-title').text();
if ('<span id="crm-remove-title" style="display:none">CiviCRM</span>' == title) {
pageTitle.hide();
}
});
});

View file

@ -0,0 +1,19 @@
// http://civicrm.org/licensing
CRM.$(function($) {
$('body')
.off('.crmExpandRow')
.on('click.crmExpandRow', 'a.crm-expand-row', function(e) {
var $row = $(this).closest('tr');
if ($(this).hasClass('expanded')) {
$row.next('.crm-child-row').children('td').children('div.crm-ajax-container')
.slideUp('fast', function() {$(this).closest('.crm-child-row').remove();});
} else {
var count = $('td', $row).length,
$newRow = $('<tr class="crm-child-row"><td colspan="' + count + '"><div></div></td></tr>')
.insertAfter($row);
CRM.loadPage(this.href, {target: $('div', $newRow).animate({minHeight: '3em'}, 'fast')});
}
$(this).toggleClass('expanded');
e.preventDefault();
});
});

View file

@ -0,0 +1,52 @@
// https://civicrm.org/licensing
CRM.$(function($) {
var $form = $('form.CRM_Core_Form_ShortCode');
function changeComponent() {
var component = $(this).val(),
entities = $(this).data('entities');
$('.shortcode-param[data-components]', $form).each(function() {
$(this).toggle($.inArray(component, $(this).data('components')) > -1);
if (entities[component]) {
$('input[name=entity]')
.val('')
.data('key', entities[component].key)
.data('select-params', null)
.data('api-params', null)
.crmEntityRef(entities[component]);
}
});
}
function close() {
$form.closest('.ui-dialog-content').dialog('close');
}
function insert() {
var code = '[civicrm';
$('.shortcode-param:visible', $form).each(function() {
var $el = $('input:checked, select, input.crm-form-entityref', this);
code += ' ' + $el.data('key') + '="' + $el.val() + '"';
});
window.send_to_editor(code + ']');
close();
}
$('select[name=component]', $form).each(changeComponent).change(changeComponent);
$form.closest('.ui-dialog-content').dialog('option', 'buttons', [
{
text: ts("Insert"),
icons: {primary: "fa-check"},
click: insert
},
{
text: ts("Cancel"),
icons: {primary: "fa-times"},
click: close
}
]);
});

View file

@ -0,0 +1,14 @@
// http://civicrm.org/licensing
CRM.$(function($) {
$(document)
.on('dialogopen', function(e) {
// J3 - Make footer admin bar hide behind popup windows (CRM-15723)
$('#status').css('z-index', '100');
})
.on('dialogclose', function(e) {
if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
// J3 - restore footer admin bar position (CRM-15723)
$('#status').css('z-index', '');
}
});
});

View file

@ -0,0 +1,12 @@
// http://civicrm.org/licensing
// Adds ajaxy behavior to a simple CiviCRM page
CRM.$(function($) {
var active = 'a.button, a.action-item:not(.crm-enable-disable), a.crm-popup';
$('#crm-main-content-wrapper')
// Widgetize the content area
.crmSnippet()
// Open action links in a popup
.off('.crmLivePage')
.on('click.crmLivePage', active, CRM.popup)
.on('crmPopupFormSuccess.crmLivePage', active, CRM.refreshParent);
});

View file

@ -0,0 +1,26 @@
// http://civicrm.org/licensing
// JS needed for multilingual installations
CRM.$(function($) {
// This is partially redundant with what the CRM.popup function would do,
// with a small amount of added functionality,
// and the difference that this loads unconditionally regardless of ajaxPopupsEnabled setting
$('body').on('click', 'a.crm-multilingual-edit-button', function(e) {
var $el = $(this),
$form = $el.closest('form'),
$field = $('#' + $el.data('field'), $form);
CRM.loadForm($el.attr('href'), {
dialog: {width: '50%', height: '50%'}
})
// Sync the primary language field with what the user has entered on the main form
.on('crmFormLoad', function() {
$('.default-lang', this).val($field.val());
})
.on('crmFormSubmit', function() {
// Sync the primary language field with what the user has entered in the popup
$field.val($('.default-lang', this).val());
$el.trigger('crmPopupFormSuccess');
});
e.preventDefault();
});
});

View file

@ -0,0 +1,16 @@
// https://civicrm.org/licensing
jQuery(function($) {
$('body')
// Enable administrators to edit option lists in a dialog
.on('click', 'a.crm-option-edit-link', CRM.popup)
.on('crmPopupFormSuccess', 'a.crm-option-edit-link', function() {
$(this).trigger('crmOptionsEdited');
var $elects = $('select[data-option-edit-path="' + $(this).data('option-edit-path') + '"]');
if ($elects.data('api-entity') && $elects.data('api-field')) {
CRM.api3($elects.data('api-entity'), 'getoptions', {sequential: 1, field: $elects.data('api-field')})
.done(function (data) {
CRM.utils.setOptions($elects, data.values);
});
}
});
});

View file

@ -0,0 +1,204 @@
// http://civicrm.org/licensing
(function($, _, undefined) {
"use strict";
var selected = 0,
form = 'form.crm-search-form',
active = 'a.action-item:not(.crm-enable-disable), a.crm-popup';
function clearTaskMenu() {
$('select#task', form).val('').select2('val', '').prop('disabled', true).select2('disable');
}
function enableTaskMenu() {
if (selected || $('[name=radio_ts][value=ts_all]', form).is(':checked')) {
$('select#task', form).prop('disabled', false).select2('enable');
}
}
function displayCount() {
$('label[for*=ts_sel] span', form).text(selected);
}
function countCheckboxes() {
return $('input.select-row:checked', form).length;
}
function clearSelections(e) {
/* jshint validthis: true */
if (selected) {
var $form = $(this).closest('form');
$('input.select-row, input.select-rows', $form).prop('checked', false).closest('tr').removeClass('crm-row-selected');
if (usesAjax()) {
phoneHome(false, $(this));
} else {
selected = 0;
displayCount();
}
}
}
function usesAjax() {
return $(form).hasClass('crm-ajax-selection-form');
}
// Use ajax to store selection server-side
function phoneHome(single, $el, event) {
var url = CRM.url('civicrm/ajax/markSelection');
var params = {qfKey: 'civicrm search ' + $('input[name=qfKey]', form).val()};
if (!$el.is(':checked') || $el.is('input[name=radio_ts][value=ts_all]')) {
params.action = 'unselect';
params.state = 'unchecked';
}
if (single) {
params.name = $el.attr('id');
} else {
params.variableType = 'multiple';
// "Reset all" button
if ($el.is('a')) {
event.preventDefault();
$("input.select-row, input.select-rows", form).prop('checked', false).closest('tr').removeClass('crm-row-selected');
}
// Master checkbox
else if ($el.hasClass('select-rows')) {
params.name = $('input.select-row').map(function() {return $(this).attr('id');}).get().join('-');
}
}
$.post(url, params, function(data) {
if (data && data.getCount !== undefined) {
selected = data.getCount;
displayCount();
enableTaskMenu();
}
}, 'json');
}
/**
* Refresh the current page
*/
function refresh() {
// Clear cached search results using force=1 argument
var location = $('#crm-main-content-wrapper').crmSnippet().crmSnippet('option', 'url');
if (!(location.match(/[?&]force=1/))) {
location += '&force=1';
}
$('#crm-main-content-wrapper').crmSnippet({url: location}).crmSnippet('refresh');
}
/**
* When initially loading and reloading (paging) the results
*/
function initForm() {
clearTaskMenu();
if (usesAjax()) {
selected = parseInt($('label[for*=ts_sel] span', form).text(), 10);
} else {
selected = countCheckboxes();
displayCount();
}
enableTaskMenu();
}
$(function() {
initForm();
// Focus first search field
$('.crm-form-text:input:visible:first', 'form.crm-search-form').focus();
// Handle user interactions with search results
$('#crm-container')
// When toggling between "all records" and "selected records only"
.on('change', '[name=radio_ts]', function() {
clearTaskMenu();
enableTaskMenu();
})
.on('click', 'input[name=radio_ts][value=ts_all]', clearSelections)
// When making a selection
.on('click', 'input.select-row, input.select-rows, a.crm-selection-reset', function(event) {
var $el = $(this),
$form = $el.closest('form'),
single = $el.is('input.select-row');
clearTaskMenu();
$('input[name=radio_ts][value=ts_sel]', $form).prop('checked', true);
if (!usesAjax()) {
if (single) {
selected = countCheckboxes();
} else {
selected = $el.is(':checked') ? $('input.select-row', $form).length : 0;
}
displayCount();
enableTaskMenu();
} else {
phoneHome(single, $el, event);
}
})
// When selecting a task
.on('change', 'select#task', function(e) {
var $form = $(this).closest('form'),
$go = $('input.crm-search-go-button', $form);
var $selectedOption = $(this).find(':selected');
if (!$selectedOption.val()) {
// do not blank refresh the empty option.
return;
}
if ($selectedOption.data('is_confirm')) {
var confirmed = false;
var refresh_fields = $selectedOption.data('confirm_refresh_fields');
var $message = '<tr>' + (($selectedOption.data('confirm_message') !== undefined) ? $selectedOption.data('confirm_message') : '') + '</tr>';
if (refresh_fields === undefined) {
refresh_fields = {};
}
$.each(refresh_fields, function (refreshIndex, refreshValue) {
var $refresh_field = $(refreshValue.selector);
var prependText = (refreshValue.prepend !== undefined) ? refreshValue.prepend : '';
var existingInput = $refresh_field.find('input').val();
$message = $message + '<tr>' + $refresh_field.html().replace(existingInput, prependText + existingInput) + '</tr>';
});
CRM.confirm({
title: $selectedOption.data('confirm_title') ? $selectedOption.data('confirm_title') : ts('Confirm action'),
message: '<table class="form-layout">' + $message + '</table>'
})
.on('crmConfirm:yes', function() {
confirmed = true;
$.each(refresh_fields, function (refreshIndex, refreshValue) {
$('#' + refreshIndex).val($('.crm-confirm #' + refreshIndex).val());
});
$go.click();
})
.on('crmConfirm:no', function() {
$('#task').val('').change();
return;
});
}
else if (!$(this).find(':selected').data('supports_modal')) {
$go.click();
$('#task').val('').select2('val', '');
}
// The following code can load the task in a popup, however not all tasks function correctly with this
// So it's a per-task opt-in mechanism.
else {
var data = $form.serialize() + '&' + $go.attr('name') + '=' + $go.attr('value');
var url = $form.attr('action');
url += (url.indexOf('?') < 0 ? '?' : '&') + 'snippet=json';
clearTaskMenu();
$.post(url, data, function(data) {
CRM.loadForm(data.userContext).on('crmFormSuccess', refresh);
enableTaskMenu();
}, 'json');
}
});
// Add a specialized version of livepage functionality
$('#crm-main-content-wrapper')
.on('crmLoad', function(e) {
if ($(e.target).is(this)) {
initForm();
}
})
// Open action links in a popup
.off('.crmLivePage')
.on('click.crmLivePage', active, CRM.popup)
.on('crmPopupFormSuccess.crmLivePage', active, refresh);
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,14 @@
// http://civicrm.org/licensing
CRM.$(function($) {
$(document)
.on('dialogopen', function(e) {
// Make admin bar hide behind popup windows
$('#adminmenuwrap').css('z-index', '100');
})
.on('dialogclose', function(e) {
if ($('.ui-dialog-content:visible').not(e.target).length < 1) {
// Restore admin bar position
$('#adminmenuwrap').css('z-index', '');
}
});
});

View file

@ -0,0 +1,59 @@
// https://civicrm.org/licensing
(function($, _) {
"use strict";
/* jshint validthis: true */
$.fn.crmAjaxTable = function() {
// Strip the ids from ajax urls to make pageLength storage more generic
function simplifyUrl(ajax) {
// Datatables ajax prop could be a url string or an object containing the url
var url = typeof ajax === 'object' ? ajax.url : ajax;
return typeof url === 'string' ? url.replace(/[&?]\w*id=\d+/g, '') : null;
}
return $(this).each(function() {
// Recall pageLength for this table
var url = simplifyUrl($(this).data('ajax'));
if (url && window.localStorage && localStorage['dataTablePageLength:' + url]) {
$(this).data('pageLength', localStorage['dataTablePageLength:' + url]);
}
// Declare the defaults for DataTables
var defaults = {
"processing": true,
"serverSide": true,
"order": [],
"dom": '<"crm-datatable-pager-top"lfp>rt<"crm-datatable-pager-bottom"ip>',
"pageLength": 25,
"pagingType": "full_numbers",
"drawCallback": function(settings) {
//Add data attributes to cells
$('thead th', settings.nTable).each( function( index ) {
$.each(this.attributes, function() {
if(this.name.match("^cell-")) {
var cellAttr = this.name.substring(5);
var cellValue = this.value;
$('tbody tr', settings.nTable).each( function() {
$('td:eq('+ index +')', this).attr( cellAttr, cellValue );
});
}
});
});
//Reload table after draw
$(settings.nTable).trigger('crmLoad');
}
};
//Include any table specific data
var settings = $.extend(true, defaults, $(this).data('table'));
// Remember pageLength
$(this).on('length.dt', function(e, settings, len) {
if (settings.ajax && window.localStorage) {
localStorage['dataTablePageLength:' + simplifyUrl(settings.ajax)] = len;
}
});
//Make the DataTables call
$(this).DataTable(settings);
});
};
})(CRM.$, CRM._);

View file

@ -0,0 +1,244 @@
// https://civicrm.org/licensing
(function($, _) {
"use strict";
/* jshint validthis: true */
// TODO: We'll need a way to clear this cache if options are edited.
// Maybe it should be stored in the CRM object so other parts of the app can use it.
// Note that if we do move it, we should also change the format of option lists to our standard sequential arrays
var optionsCache = {};
/**
* Helper fn to retrieve semantic data from markup
*/
$.fn.crmEditableEntity = function() {
var
el = this[0],
ret = {},
$row = this.first().closest('.crm-entity');
ret.entity = $row.data('entity') || $row[0].id.split('-')[0];
ret.id = $row.data('id') || $row[0].id.split('-')[1];
ret.action = $row.data('action') || 'create';
if (!ret.entity || !ret.id) {
return false;
}
$('.crm-editable, [data-field]', $row).each(function() {
var fieldName = $(this).data('field') || this.className.match(/crmf-(\S*)/)[1];
if (fieldName) {
ret[fieldName] = $(this).text();
if (this === el) {
ret.field = fieldName;
}
}
});
return ret;
};
/**
* @see http://wiki.civicrm.org/confluence/display/CRMDOC/Structure+convention+for+automagic+edit+in+place
*/
$.fn.crmEditable = function(options) {
function checkable() {
$(this).off('.crmEditable').on('change.crmEditable', function() {
var $el = $(this),
info = $el.crmEditableEntity();
if (!info.field) {
return false;
}
var params = {
sequential: 1,
id: info.id,
field: info.field,
value: $el.is(':checked') ? 1 : 0
};
CRM.api3(info.entity, info.action, params, true);
});
}
return this.each(function() {
var $i,
fieldName = "",
defaults = {
error: function(entity, field, value, data) {
restoreContainer();
$(this).html(originalValue || settings.placeholder).click();
var msg = $.isPlainObject(data) && data.error_message;
errorMsg = $(':input', this).first().crmError(msg || ts('Sorry an error occurred and your information was not saved'), ts('Error'));
},
success: function(entity, field, value, data, settings) {
restoreContainer();
if ($i.data('refresh')) {
CRM.refreshParent($i);
} else {
value = value === '' ? settings.placeholder : _.escape(value);
$i.html(value);
}
}
},
originalValue = '',
errorMsg,
editableSettings = $.extend({}, defaults, options);
if ($(this).hasClass('crm-editable-enabled')) {
return;
}
if (this.nodeName == "INPUT" && this.type == "checkbox") {
checkable.call(this, this);
return;
}
// Table cell needs something inside it to look right
if ($(this).is('td')) {
$(this)
.removeClass('crm-editable')
.wrapInner('<div class="crm-editable" />');
$i = $('div.crm-editable', this)
.data($(this).data());
var field = this.className.match(/crmf-(\S*)/);
if (field) {
$i.data('field', field[1]);
}
}
else {
$i = $(this);
}
var settings = {
tooltip: $i.data('tooltip') || ts('Click to edit'),
placeholder: $i.data('placeholder') || '<i class="crm-i fa-pencil crm-editable-placeholder"></i>',
onblur: 'cancel',
cancel: '<button type="cancel"><i class="crm-i fa-times"></i></button>',
submit: '<button type="submit"><i class="crm-i fa-check"></i></button>',
cssclass: 'crm-editable-form',
data: getData,
onreset: restoreContainer
};
if ($i.data('type')) {
settings.type = $i.data('type');
if (settings.type == 'boolean') {
settings.type = 'select';
$i.data('options', {'0': ts('No'), '1': ts('Yes')});
}
}
if (settings.type == 'textarea') {
$i.addClass('crm-editable-textarea-enabled');
}
$i.addClass('crm-editable-enabled');
function callback(value, settings) {
$i.addClass('crm-editable-saving');
var
info = $i.crmEditableEntity(),
$el = $($i),
params = {},
action = $i.data('action') || info.action;
if (!info.field) {
return false;
}
if (info.id && info.id !== 'new') {
params.id = info.id;
}
if (action === 'setvalue') {
params.field = info.field;
params.value = value;
}
else {
params[info.field] = value;
}
CRM.api3(info.entity, action, params, {error: null})
.done(function(data) {
if (data.is_error) {
return editableSettings.error.call($el[0], info.entity, info.field, value, data);
}
if ($el.data('options')) {
value = $el.data('options')[value] || '';
}
else if ($el.data('optionsHashKey')) {
var options = optionsCache[$el.data('optionsHashKey')];
value = options && options[value] ? options[value] : '';
}
$el.trigger('crmFormSuccess', [value]);
editableSettings.success.call($el[0], info.entity, info.field, value, data, settings);
})
.fail(function(data) {
editableSettings.error.call($el[0], info.entity, info.field, value, data);
});
}
CRM.loadScript(CRM.config.resourceBase + 'packages/jquery/plugins/jquery.jeditable.min.js').done(function() {
$i.editable(callback, settings);
});
// CRM-15759 - Workaround broken textarea handling in jeditable 1.7.1
$i.click(function() {
$('textarea', this).off()
// Fix cancel-on-blur
.on('blur', function(e) {
if (!e.relatedTarget || !$(e.relatedTarget).is('.crm-editable-form button')) {
$i.find('button[type=cancel]').click();
}
})
// Add support for ctrl-enter shortcut key
.on('keydown', function (e) {
if (e.ctrlKey && e.keyCode == 13) {
$i.find('button[type=submit]').click();
e.preventDefault();
}
});
});
function getData(value, settings) {
// Add css class to wrapper
// FIXME: This should be a response to an event instead of coupled with this function but jeditable 1.7.1 doesn't trigger any events :(
$i.addClass('crm-editable-editing');
originalValue = value;
if ($i.data('type') == 'select' || $i.data('type') == 'boolean') {
if ($i.data('options')) {
return formatOptions($i.data('options'));
}
var result,
info = $i.crmEditableEntity(),
// Strip extra id from multivalued custom fields
custom = info.field.match(/(custom_\d+)_\d+/),
field = custom ? custom[1] : info.field,
hash = info.entity + '.' + field,
params = {
field: field,
context: 'create'
};
$i.data('optionsHashKey', hash);
if (!optionsCache[hash]) {
$.ajax({
url: CRM.url('civicrm/ajax/rest'),
data: {entity: info.entity, action: 'getoptions', json: JSON.stringify(params)},
async: false, // jeditable lacks support for async options lookup
success: function(data) {optionsCache[hash] = data.values;}
});
}
return formatOptions(optionsCache[hash]);
}
// Unwrap contents then replace html special characters with plain text
return _.unescape(value.replace(/<(?:.|\n)*?>/gm, ''));
}
function formatOptions(options) {
if (typeof $i.data('emptyOption') === 'string') {
// Using 'null' because '' is broken in jeditable 1.7.1
return $.extend({'null': $i.data('emptyOption')}, options);
}
return options;
}
function restoreContainer() {
if (errorMsg && errorMsg.close) errorMsg.close();
$i.removeClass('crm-editable-saving crm-editable-editing');
}
});
};
})(jQuery, CRM._);

View file

@ -0,0 +1,120 @@
// https://civicrm.org/licensing
(function($, _) {
"use strict";
/* jshint validthis: true */
var icons = [], loaded;
$.fn.crmIconPicker = function() {
function loadIcons() {
if (!loaded) {
loaded = $.Deferred();
CRM.$.get(CRM.config.resourceBase + 'bower_components/font-awesome/css/font-awesome.css').done(function(data) {
var match,
regex = /\.(fa-[-a-zA-Z0-9]+):before {/g;
while((match = regex.exec(data)) !== null) {
icons.push(match[1]);
}
loaded.resolve();
});
}
return loaded;
}
return this.each(function() {
if ($(this).hasClass('iconpicker-widget')) {
return;
}
var $input = $(this),
$button = $('<a class="crm-icon-picker-button" href="#" />').button().removeClass('ui-corner-all').attr('title', $input.attr('title')),
$style = $('<select class="crm-form-select"></select>'),
options = [
{key: 'fa-rotate-90', value: ts('Rotate right')},
{key: 'fa-rotate-270', value: ts('Rotate left')},
{key: 'fa-rotate-180', value: ts('Rotate 180')},
{key: 'fa-flip-horizontal', value: ts('Flip horizontal')},
{key: 'fa-flip-vertical', value: ts('Flip vertical')}
];
function formatButton() {
var val = $input.val().replace('fa ', '');
val = val.replace('crm-i ', '');
var split = val.split(' ');
$button.button('option', {
label: split[0] || ts('None'),
icons: {primary: val ? val : 'fa-'}
});
$style.toggle(!!split[0]).val(split[1] || '');
}
$input.hide().addClass('iconpicker-widget').after($style).after('&nbsp;').after($button).change(formatButton);
CRM.utils.setOptions($style, options, ts('Normal'));
formatButton();
$style.change(function() {
if ($input.val()) {
var split = $input.val().split(' '),
style = $style.val();
$input.val(split[0] + (style ? ' ' + style : '')).change();
}
});
$button.click(function(e) {
var dialog;
function displayIcons() {
var term = $('input[name=search]', dialog).val().replace(/-/g, '').toLowerCase(),
$place = $('div.icons', dialog).html('');
$.each(icons, function(i, icon) {
if (!term.length || icon.replace(/-/g, '').indexOf(term) > -1) {
var item = $('<a href="#" title="' + icon + '"/>').button({
icons: {primary: icon}
});
$place.append(item);
}
});
}
function displayDialog() {
dialog.append('<style type="text/css">' +
'#crmIconPicker {font-size: 2em;}' +
'#crmIconPicker .icon-search input {font-family: FontAwesome; padding-left: .5em; margin-bottom: 1em;}' +
'#crmIconPicker a.ui-button {width: 2em; height: 2em; color: #222;}' +
'#crmIconPicker a.ui-button .ui-icon {margin-top: -0.5em; width: auto; height: auto;}' +
'</style>' +
'<div class="icon-search"><input class="crm-form-text" name="search" placeholder="&#xf002"/></div>' +
'<div class="icons"></div>'
);
displayIcons();
dialog.unblock();
}
function pickIcon(e) {
var newIcon = $(this).attr('title'),
style = $style.val();
$input.val(newIcon + (style ? ' ' + style : '')).change();
dialog.dialog('close');
e.preventDefault();
}
dialog = $('<div id="crmIconPicker"/>').dialog({
title: $input.attr('title'),
width: '80%',
height: 400,
modal: true
}).block()
.on('click', 'a', pickIcon)
.on('keyup', 'input[name=search]', displayIcons)
.on('dialogclose', function() {
$(this).dialog('destroy').remove();
});
loadIcons().done(displayDialog);
e.preventDefault();
});
});
};
}(CRM.$, CRM._));

View file

@ -0,0 +1,99 @@
(function($, _) {
var ufGroupCollection = new CRM.UF.UFGroupCollection(_.sortBy(CRM.initialProfileList.values, 'title'));
//var ufGroupCollection = new CRM.UF.UFGroupCollection(CRM.initialProfileList.values, {
// comparator: 'title' // no point, this doesn't work with subcollections
//});
ufGroupCollection.unshift(new CRM.UF.UFGroupModel({
id: '',
title: ts('- select -')
}));
/**
* Example:
* <input type="text" value="{$profileId}" class="crm-profile-selector" />
* ...
* cj('.crm-profile-selector').crmProfileSelector({
* groupTypeFilter: "Contact,Individual,Activity;;ActivityType:7",
* entities: "contact_1:IndividualModel,activity_1:ActivityModel"
* });
*
* Note: The system does not currently support dynamic entities -- it only supports
* a couple of entities named "contact_1" and "activity_1". See also
* CRM.UF.guessEntityName().
*/
$.fn.crmProfileSelector = function(options) {
return this.each(function() {
// Hide the existing <SELECT> and instead construct a ProfileSelector view.
// Keep them synchronized.
var matchingUfGroups,
$select = $(this).hide().addClass('rendered');
var validTypesId = [];
var usedByFilter = null;
if (options.groupTypeFilter) {
matchingUfGroups = ufGroupCollection.subcollection({
filter: function(ufGroupModel) {
//CRM-16915 - filter with module used by the profile
if (options.usedByFilter && options.usedByFilter.length) {
usedByFilter = options.usedByFilter;
}
return ufGroupModel.checkGroupType(options.groupTypeFilter, options.allowAllSubtypes, usedByFilter);
}
});
} else {
matchingUfGroups = ufGroupCollection;
}
//CRM-15427 check for valid subtypes raise a warning if not valid
if (options.allowAllSubtypes && !validTypesId.length) {
validTypes = ufGroupCollection.subcollection({
filter: function(ufGroupModel) {
return ufGroupModel.checkGroupType(options.groupTypeFilter);
}
});
_.each(validTypes.models, function(validTypesattr) {
validTypesId.push(validTypesattr.id);
});
}
if (validTypesId.length && $.inArray($select.val(), validTypesId) == -1) {
var civiComponent;
if (options.groupTypeFilter.indexOf('Membership') !== -1) {
civiComponent = 'Membership';
}
else if (options.groupTypeFilter.indexOf('Participant') !== -1) {
civiComponent = 'Event';
}
else {
civiComponent = 'Contribution';
}
CRM.alert(ts('The selected profile is using a custom field which is not assigned to the "%1" being configured.', {1: civiComponent}), ts('Warning'));
}
var view = new CRM.ProfileSelector.View({
ufGroupId: $select.val(),
ufGroupCollection: matchingUfGroups,
ufEntities: options.entities
});
view.on('change:ufGroupId', function() {
$select.val(view.getUfGroupId()).change();
});
view.render();
$select.after(view.el);
setTimeout(function() {
view.doPreview();
}, 100);
});
};
$('#crm-container').on('crmLoad', function() {
$('.crm-profile-selector:not(.rendered)', this).each(function() {
$(this).crmProfileSelector({
groupTypeFilter: $(this).data('groupType'),
entities: $(this).data('entities'),
//CRM-15427
allowAllSubtypes: $(this).data('default'),
usedByFilter: $(this).data('usedfor')
});
});
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,50 @@
(function($, CRM) {
/**
* Usage:
*
* cj('.my-link').crmRevisionLink({
* 'reportId': 123, // CRM_Report_Utils_Report::getInstanceIDForValue('logging/contact/summary'),
* 'tableName': 'my_table',
* 'contactId': 123
* ));
*
* Note: This file is used by CivHR
*/
$.fn.crmRevisionLink = function(options) {
return this.each(function(){
var $dialog = $('<div><div class="revision-content"></div></div>');
$('body').append($dialog);
$(this).on("click", function() {
$dialog.show();
$dialog.dialog({
title: ts("Revisions"),
modal: true,
width: "680px",
bgiframe: true,
overlay: { opacity: 0.5, background: "black" },
open:function() {
var ajaxurl = CRM.url("civicrm/report/instance/" + options.reportId);
cj.ajax({
data: "reset=1&snippet=4&section=2&altered_contact_id_op=eq&altered_contact_id_value="+options.contactId+"&log_type_table_op=has&log_type_table_value=" + options.tableName,
url: ajaxurl,
success: function (data) {
$dialog.find(".revision-content").html(data);
if (!$dialog.find(".revision-content .report-layout").length) {
$dialog.find(".revision-content").html("Sorry, couldn't find any revisions.");
}
}
});
},
buttons: {
"Done": function() {
$(this).dialog("destroy");
}
}
});
return false;
});
}); // this.each
}; // fn.crmRevisionLink
})(jQuery, CRM);

View file

@ -0,0 +1,580 @@
// https://civicrm.org/licensing
/* global CRM, ts */
/*jshint loopfunc: true */
(function($) {
'use strict';
// Constructor for dashboard object.
$.fn.dashboard = function(options) {
// Public properties of dashboard.
var dashboard = {};
dashboard.element = this.empty();
dashboard.ready = false;
dashboard.columns = [];
dashboard.widgets = {};
/**
* Public methods of dashboard.
*/
// Saves the order of widgets for all columns including the widget.minimized status to options.ajaxCallbacks.saveColumns.
dashboard.saveColumns = function(showStatus) {
// Update the display status of the empty placeholders.
$.each(dashboard.columns, function(c, col) {
if ( typeof col == 'object' ) {
// Are there any visible children of the column (excluding the empty placeholder)?
if (col.element.children(':visible').not(col.emptyPlaceholder).length > 0) {
col.emptyPlaceholder.hide();
}
else {
col.emptyPlaceholder.show();
}
}
});
// Don't save any changes to the server unless the dashboard has finished initiating.
if (!dashboard.ready) {
return;
}
// Build a list of params to post to the server.
var params = {};
// For each column...
$.each(dashboard.columns, function(c, col) {
// IDs of the sortable elements in this column.
var ids = (typeof col == 'object') ? col.element.sortable('toArray') : [];
// For each id...
$.each(ids, function(w, id) {
if (typeof id == 'string') {
// Chop 'widget-' off of the front so that we have the real widget id.
id = id.substring('widget-'.length);
// Add one flat property to the params object that will look like an array element to the PHP server.
// Unfortunately jQuery doesn't do this for us.
if (typeof dashboard.widgets[id] == 'object') params['columns[' + c + '][' + id + ']'] = (dashboard.widgets[id].minimized ? '1' : '0');
}
});
});
// The ajaxCallback settings overwrite any duplicate properties.
$.extend(params, opts.ajaxCallbacks.saveColumns.data);
var post = $.post(opts.ajaxCallbacks.saveColumns.url, params, function() {
invokeCallback(opts.callbacks.saveColumns, dashboard);
});
if (showStatus !== false) {
CRM.status({}, post);
}
};
/**
* Private properties of dashboard.
*/
// Used to determine whether two resort events are resulting from the same UI event.
var currentReSortEvent = null;
// Merge in the caller's options with the defaults.
var opts = $.extend({}, $.fn.dashboard.defaults, options);
var localCache = window.localStorage && localStorage.dashboard ? JSON.parse(localStorage.dashboard) : {};
init(opts.widgetsByColumn);
return dashboard;
/**
* Private methods of dashboard.
*/
// Initialize widget columns.
function init(widgets) {
var markup = '<li class="empty-placeholder">' + opts.emptyPlaceholderInner + '</li>';
// Build the dashboard in the DOM. For each column...
// (Don't iterate on widgets since this will break badly if the dataset has empty columns.)
var emptyDashboard = true;
for (var c = 0; c < opts.columns; c++) {
// Save the column to both the public scope for external accessibility and the local scope for readability.
var col = dashboard.columns[c] = {
initialWidgets: [],
element: $('<ul id="column-' + c + '" class="column column-' + c + '"></ul>').appendTo(dashboard.element)
};
// Add the empty placeholder now, hide it and save it.
col.emptyPlaceholder = $(markup).appendTo(col.element).hide();
// For each widget in this column.
$.each(widgets[c], function(num, item) {
var id = (num+1) + '-' + item.id;
col.initialWidgets[id] = dashboard.widgets[item.id] = widget($.extend({
element: $('<li class="widget"></li>').appendTo(col.element),
initialColumn: col
}, item));
emptyDashboard = false;
});
}
if (emptyDashboard) {
emptyDashboardCondition();
} else {
completeInit();
}
invokeCallback(opts.callbacks.init, dashboard);
}
// function that is called when dashboard is empty
function emptyDashboardCondition( ) {
$(".show-refresh").hide( );
$("#empty-message").show( );
}
// Cache dashlet info in localStorage
function saveLocalCache() {
localCache = {};
$.each(dashboard.widgets, function(id, widget) {
localCache[id] = {
content: widget.content,
lastLoaded: widget.lastLoaded,
minimized: widget.minimized
};
});
if (window.localStorage) {
localStorage.dashboard = JSON.stringify(localCache);
}
}
// Contructors for each widget call this when initialization has finished so that dashboard can complete it's intitialization.
function completeInit() {
// Only do this once.
if (dashboard.ready) {
return;
}
// Make widgets sortable across columns.
dashboard.sortableElement = $('.column').sortable({
connectWith: ['.column'],
// The class of the element by which widgets are draggable.
handle: '.widget-header',
// The class of placeholder elements (the 'ghost' widget showing where the dragged item would land if released now.)
placeholder: 'placeholder',
activate: function(event, ui) {
var h= $(ui.item).height();
$('.placeholder').css('height', h +'px');
},
opacity: 0.2,
// Maks sure that only widgets are sortable, and not empty placeholders.
items: '> .widget',
forcePlaceholderSize: true,
// Callback functions.
update: resorted,
start: hideEmptyPlaceholders
});
// Update empty placeholders.
dashboard.saveColumns();
dashboard.ready = true;
invokeCallback(opts.callbacks.ready, dashboard);
// Auto-refresh widgets when content is stale
window.setInterval(function() {
if (!document.hasFocus || document.hasFocus()) {
$.each(dashboard.widgets, function (i, widget) {
if (!widget.cacheIsFresh()) {
widget.reloadContent();
}
});
}
}, 5000);
}
// Callback for when any list has changed (and the user has finished resorting).
function resorted(e, ui) {
// Only do anything if we haven't already handled resorts based on changes from this UI DOM event.
// (resorted() gets invoked once for each list when an item is moved from one to another.)
if (!currentReSortEvent || e.originalEvent != currentReSortEvent) {
currentReSortEvent = e.originalEvent;
dashboard.saveColumns();
}
}
// Callback for when a user starts resorting a list. Hides all the empty placeholders.
function hideEmptyPlaceholders(e, ui) {
for (var c in dashboard.columns) {
if( (typeof dashboard.columns[c]) == 'object' ) dashboard.columns[c].emptyPlaceholder.hide();
}
}
// @todo use an event library to register, bind to and invoke events.
// @param callback is a function.
// @param theThis is the context given to that function when it executes. It becomes 'this' inside of that function.
function invokeCallback(callback, theThis, parameterOne) {
if (callback) {
callback.call(theThis, parameterOne);
}
}
/**
* widget object
* Private sub-class of dashboard
* Constructor starts
*/
function widget(widget) {
// Merge default options with the options defined for this widget.
widget = $.extend({}, $.fn.dashboard.widget.defaults, localCache[widget.id] || {}, widget);
/**
* Public methods of widget.
*/
// Toggles the minimize() & maximize() methods.
widget.toggleMinimize = function() {
if (widget.minimized) {
widget.maximize();
}
else {
widget.minimize();
}
widget.hideSettings();
};
widget.minimize = function() {
$('.widget-content', widget.element).slideUp(opts.animationSpeed);
$(widget.controls.minimize.element)
.addClass('fa-caret-right')
.removeClass('fa-caret-down')
.attr('title', ts('Expand'));
widget.minimized = true;
saveLocalCache();
};
widget.maximize = function() {
$(widget.controls.minimize.element)
.removeClass( 'fa-caret-right' )
.addClass( 'fa-caret-down' )
.attr('title', ts('Collapse'));
widget.minimized = false;
saveLocalCache();
if (!widget.contentLoaded) {
loadContent();
}
$('.widget-content', widget.element).slideDown(opts.animationSpeed);
};
// Toggles whether the widget is in settings-display mode or not.
widget.toggleSettings = function() {
if (widget.settings.displayed) {
// Widgets always exit settings into maximized state.
widget.maximize();
widget.hideSettings();
invokeCallback(opts.widgetCallbacks.hideSettings, widget);
}
else {
widget.minimize();
widget.showSettings();
invokeCallback(opts.widgetCallbacks.showSettings, widget);
}
};
widget.showSettings = function() {
if (widget.settings.element) {
widget.settings.element.show();
// Settings are loaded via AJAX. Only execute the script if the settings have been loaded.
if (widget.settings.ready) {
getJavascript(widget.settings.script);
}
}
else {
// Settings have not been initialized. Do so now.
initSettings();
}
widget.settings.displayed = true;
};
widget.hideSettings = function() {
if (widget.settings.element) {
widget.settings.element.hide();
}
widget.settings.displayed = false;
};
widget.saveSettings = function() {
// Build list of parameters to POST to server.
var params = {};
// serializeArray() returns an array of objects. Process it.
var fields = widget.settings.element.serializeArray();
$.each(fields, function(i, field) {
// Put the values into flat object properties that PHP will parse into an array server-side.
// (Unfortunately jQuery doesn't do this)
params['settings[' + field.name + ']'] = field.value;
});
// Things get messy here.
// @todo Refactor to use currentState and targetedState properties to determine what needs
// to be done to get to any desired state on any UI or AJAX event since these don't always
// match.
// E.g. When a user starts a new UI event before the Ajax event handler from a previous
// UI event gets invoked.
// Hide the settings first of all.
widget.toggleSettings();
// Save the real settings element so that we can restore the reference later.
var settingsElement = widget.settings.element;
// Empty the settings form.
widget.settings.innerElement.empty();
initThrobber();
// So that showSettings() and hideSettings() can do SOMETHING, without showing the empty settings form.
widget.settings.element = widget.throbber.hide();
widget.settings.ready = false;
// Save the settings to the server.
$.extend(params, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
$.post(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
// Merge the response into widget.settings.
$.extend(widget.settings, response);
// Restore the reference to the real settings element.
widget.settings.element = settingsElement;
// Make sure the settings form is empty and add the updated settings form.
widget.settings.innerElement.empty().append(widget.settings.markup);
widget.settings.ready = true;
// Did the user already jump back into settings-display mode before we could finish reloading the settings form?
if (widget.settings.displayed) {
// Ooops! We had better take care of hiding the throbber and showing the settings form then.
widget.throbber.hide();
widget.showSettings();
invokeCallback(opts.widgetCallbacks.saveSettings, dashboard);
}
}, 'json');
// Don't let form submittal bubble up.
return false;
};
widget.enterFullscreen = function() {
// Make sure the widget actually supports full screen mode.
if (widget.fullscreenUrl) {
CRM.loadPage(widget.fullscreenUrl);
}
};
// Adds controls to a widget. id is for internal use and image file name in images/dashboard/ (a .gif).
widget.addControl = function(id, control) {
var markup = '<a class="crm-i ' + control.icon + '" alt="' + control.description + '" title="' + control.description + '"></a>';
control.element = $(markup).prependTo($('.widget-controls', widget.element)).click(control.callback);
};
// Fetch remote content.
widget.reloadContent = function() {
// If minimized, we'll reload later
if (widget.minimized) {
widget.contentLoaded = false;
widget.lastLoaded = 0;
} else {
CRM.loadPage(widget.url, {target: widget.contentElement});
}
};
// Removes the widget from the dashboard, and saves columns.
widget.remove = function() {
invokeCallback(opts.widgetCallbacks.remove, widget);
widget.element.fadeOut(opts.animationSpeed, function() {
$(this).remove();
delete(dashboard.widgets[widget.id]);
dashboard.saveColumns(false);
});
CRM.alert(
ts('You can re-add it by clicking the "Configure Your Dashboard" button.'),
ts('"%1" Removed', {1: widget.title}),
'success'
);
};
widget.cacheIsFresh = function() {
return (((widget.cacheMinutes * 60000 + widget.lastLoaded) > $.now()) && widget.content);
};
/**
* Public properties of widget.
*/
// Default controls. External script can add more with widget.addControls()
widget.controls = {
settings: {
description: ts('Configure this dashlet'),
callback: widget.toggleSettings,
icon: 'fa-wrench'
},
minimize: {
description: widget.minimized ? ts('Expand') : ts('Collapse'),
callback: widget.toggleMinimize,
icon: widget.minimized ? 'fa-caret-right' : 'fa-caret-down'
},
fullscreen: {
description: ts('View fullscreen'),
callback: widget.enterFullscreen,
icon: 'fa-expand'
},
close: {
description: ts('Remove from dashboard'),
callback: widget.remove,
icon: 'fa-times'
}
};
widget.contentLoaded = false;
init();
return widget;
/**
* Private methods of widget.
*/
function loadContent() {
var loadFromCache = widget.cacheIsFresh();
if (loadFromCache) {
widget.contentElement.html(widget.content).trigger('crmLoad', widget);
}
widget.contentElement.off('crmLoad').on('crmLoad', function(event, data) {
if ($(event.target).is(widget.contentElement)) {
widget.content = data.content;
// Cache for one day
widget.lastLoaded = $.now();
saveLocalCache();
invokeCallback(opts.widgetCallbacks.get, widget);
}
});
if (!loadFromCache) {
widget.reloadContent();
}
widget.contentLoaded = true;
}
// Build widget & load content.
function init() {
// Delete controls that don't apply to this widget.
if (!widget.settings) {
delete widget.controls.settings;
widget.settings = {};
}
if (!widget.fullscreenUrl) {
delete widget.controls.fullscreen;
}
var cssClass = 'widget-' + widget.name.replace('/', '-');
widget.element.attr('id', 'widget-' + widget.id).addClass(cssClass);
// Build and add the widget's DOM element.
$(widget.element).append(widgetHTML());
// Save the content element so that external scripts can reload it easily.
widget.contentElement = $('.widget-content', widget.element);
$.each(widget.controls, widget.addControl);
if (widget.minimized) {
widget.contentElement.hide();
} else {
loadContent();
}
}
// Builds inner HTML for widgets.
function widgetHTML() {
var html = '';
html += '<div class="widget-wrapper">';
html += ' <div class="widget-controls"><h3 class="widget-header">' + widget.title + '</h3></div>';
html += ' <div class="widget-content"></div>';
html += '</div>';
return html;
}
// Initializes a widgets settings pane.
function initSettings() {
// Overwrite widget.settings (boolean).
initThrobber();
widget.settings = {
element: widget.throbber.show(),
ready: false
};
// Get the settings markup and script executables for this widget.
var params = $.extend({}, opts.ajaxCallbacks.widgetSettings.data, { id: widget.id });
$.getJSON(opts.ajaxCallbacks.widgetSettings.url, params, function(response, status) {
$.extend(widget.settings, response);
// Build and add the settings form to the DOM. Bind the form's submit event handler/callback.
widget.settings.element = $(widgetSettingsHTML()).appendTo($('.widget-wrapper', widget.element)).submit(widget.saveSettings);
// Bind the cancel button's event handler too.
widget.settings.cancelButton = $('.widget-settings-cancel', widget.settings.element).click(cancelEditSettings);
// Build and add the inner form elements from the HTML markup provided in the AJAX data.
widget.settings.innerElement = $('.widget-settings-inner', widget.settings.element).append(widget.settings.markup);
widget.settings.ready = true;
if (widget.settings.displayed) {
// If the user hasn't clicked away from the settings pane, then display the form.
widget.throbber.hide();
widget.showSettings();
}
getJavascript(widget.settings.initScript);
});
}
// Builds HTML for widget settings forms.
function widgetSettingsHTML() {
var html = '';
html += '<form class="widget-settings">';
html += ' <div class="widget-settings-inner"></div>';
html += ' <div class="widget-settings-buttons">';
html += ' <input id="' + widget.id + '-settings-save" class="widget-settings-save" value="Save" type="submit" />';
html += ' <input id="' + widget.id + '-settings-cancel" class="widget-settings-cancel" value="Cancel" type="submit" />';
html += ' </div>';
html += '</form>';
return html;
}
// Initializes a generic widget content throbber, for use by settings form and external scripts.
function initThrobber() {
if (!widget.throbber) {
widget.throbber = $(opts.throbberMarkup).appendTo($('.widget-wrapper', widget.element));
}
}
// Event handler/callback for cancel button clicks.
// @todo test this gets caught by all browsers when the cancel button is 'clicked' via the keyboard.
function cancelEditSettings() {
widget.toggleSettings();
return false;
}
// Helper function to execute external script on the server.
// @todo It would be nice to provide some context to the script. How?
function getJavascript(url) {
if (url) {
$.getScript(url);
}
}
}
};
// Public static properties of dashboard. Default settings.
$.fn.dashboard.defaults = {
columns: 2,
emptyPlaceholderInner: '',
throbberMarkup: '',
animationSpeed: 200,
callbacks: {},
widgetCallbacks: {}
};
// Default widget settings.
$.fn.dashboard.widget = {
defaults: {
minimized: false,
content: null,
lastLoaded: 0,
settings: false
// id, url, fullscreenUrl, title, name, cacheMinutes
}
};
})(jQuery);

View file

@ -0,0 +1,121 @@
(function($, _) {
if (!CRM.Designer) CRM.Designer = {};
// TODO Optimize this class
CRM.Designer.PaletteFieldModel = CRM.Backbone.Model.extend({
defaults: {
/**
* @var {string} required; a form-specific binding to an entity instance (eg 'student', 'mother')
*/
entityName: null,
/**
* @var {string}
*/
fieldName: null
},
initialize: function() {
},
getFieldSchema: function() {
return this.getRel('ufGroupModel').getFieldSchema(this.get('entityName'), this.get('fieldName'));
},
getLabel: function() {
// Note: if fieldSchema were a bit tighter, then we need to get a label from PaletteFieldModel at all
return this.getFieldSchema().title || this.get('fieldName');
},
getSectionName: function() {
// Note: if fieldSchema were a bit tighter, then we need to get a section from PaletteFieldModel at all
return this.getFieldSchema().section || 'default';
},
getSection: function() {
return this.getRel('ufGroupModel').getModelClass(this.get('entityName')).prototype.sections[this.getSectionName()];
},
/**
* Add a new UFField model to a UFFieldCollection (if doing so is legal).
* If it fails, display an alert.
*
* @param {int} ufGroupId
* @param {CRM.UF.UFFieldCollection} ufFieldCollection
* @param {Object} addOptions
* @return {CRM.UF.UFFieldModel} or null (if the field is not addable)
*/
addToUFCollection: function(ufFieldCollection, addOptions) {
var name, paletteFieldModel = this;
var ufFieldModel = paletteFieldModel.createUFFieldModel(ufFieldCollection.getRel('ufGroupModel'));
ufFieldModel.set('uf_group_id', ufFieldCollection.uf_group_id);
if (!ufFieldCollection.isAddable(ufFieldModel)) {
CRM.alert(
ts('The field "%1" is already included.', {
1: paletteFieldModel.getLabel()
}),
ts('Duplicate'),
'alert'
);
return null;
}
ufFieldCollection.add(ufFieldModel, addOptions);
// Load metadata and set defaults
// TODO: currently only works for custom fields
name = this.get('fieldName').split('_');
if (name[0] === 'custom') {
CRM.api('custom_field', 'getsingle', {id: name[1]}, {success: function(field) {
ufFieldModel.set(_.pick(field, 'help_pre', 'help_post', 'is_required'));
}});
}
return ufFieldModel;
},
createUFFieldModel: function(ufGroupModel) {
var model = new CRM.UF.UFFieldModel({
is_active: 1,
label: this.getLabel(),
entity_name: this.get('entityName'),
field_type: this.getFieldSchema().civiFieldType,
// For some reason the 'formatting' field gets a random number appended in core so we mimic that here.
// TODO: Why?
field_name: this.get('fieldName') == 'formatting' ? 'formatting_' + (Math.floor(Math.random() * 8999) + 1000) : this.get('fieldName')
});
return model;
}
});
/**
*
* options:
* - ufGroupModel: UFGroupModel
*/
CRM.Designer.PaletteFieldCollection = CRM.Backbone.Collection.extend({
model: CRM.Designer.PaletteFieldModel,
initialize: function(models, options) {
this.initializeCopyToChildrenRelation('ufGroupModel', options.ufGroupModel, models);
},
/**
* Look up a palette-field
*
* @param entityName
* @param fieldName
* @return {CRM.Designer.PaletteFieldModel}
*/
getFieldByName: function(entityName, fieldName) {
if (fieldName.indexOf('formatting') === 0) {
fieldName = 'formatting';
}
return this.find(function(paletteFieldModel) {
return ((!entityName || paletteFieldModel.get('entityName') == entityName) && paletteFieldModel.get('fieldName') == fieldName);
});
},
/**
* Get a list of all fields, grouped into sections by "entityName+sectionName".
*
* @return {Object} keys are sections ("entityName+sectionName"); values are CRM.Designer.PaletteFieldModel
*/
getFieldsByEntitySection: function() {
// TODO cache
var fieldsByEntitySection = this.groupBy(function(paletteFieldModel) {
return paletteFieldModel.get('entityName') + '-' + paletteFieldModel.getSectionName();
});
return fieldsByEntitySection;
}
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,9 @@
(function($, _) {
if (!CRM.ProfileSelector) CRM.ProfileSelector = {};
CRM.ProfileSelector.DummyModel = CRM.Backbone.Model.extend({
defaults: {
profile_id: null
}
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,47 @@
/**
* Dynamically-generated alternative to civi.core.js
*/
(function($, _) {
if (!CRM.Schema) CRM.Schema = {};
/**
* Data models used by the Civi form designer require more attributes than basic Backbone models:
* - sections: array of field-groupings
* - schema: array of fields, keyed by field name, per backbone-forms; extra attributes:
* + section: string, index to the 'sections' array
* + civiFieldType: string
*
* @see https://github.com/powmedia/backbone-forms
*/
CRM.Schema.BaseModel = CRM.Backbone.Model.extend({
initialize: function() {
}
});
CRM.Schema.loadModels = function(civiSchema) {
_.each(civiSchema, function(value, key, list) {
CRM.Schema[key] = CRM.Schema.BaseModel.extend(value);
});
};
CRM.Schema.reloadModels = function(options) {
return $
.ajax({
url: CRM.url("civicrm/profile-editor/schema"),
data: {
'entityTypes': _.keys(CRM.civiSchema).join(',')
},
type: 'POST',
dataType: 'json',
success: function(data) {
if (data) {
CRM.civiSchema = data;
CRM.Schema.loadModels(CRM.civiSchema);
}
}
});
};
CRM.Schema.loadModels(CRM.civiSchema);
})(CRM.$, CRM._);

View file

@ -0,0 +1,51 @@
(function($, _) {
if (!CRM.Schema) CRM.Schema = {};
/**
* Civi data models require more attributes than basic Backbone models:
* - sections: array of field-groupings
* - schema: array of fields, keyed by field name, per backbone-forms
*
* @see https://github.com/powmedia/backbone-forms
*/
CRM.Schema.IndividualModel = CRM.Backbone.Model.extend({
sections: {
'default': {title: 'Individual'},
'custom1': {title: 'Individual: Favorite Things', is_addable: true},
'custom2': {title: 'Individual: Custom Things', is_addable: true}
},
schema: {
first_name: { type: 'Text', title: 'First name', civiFieldType: 'Individual' },
last_name: { type: 'Text', title: 'Last name', civiFieldType: 'Individual' },
legal_name: { type: 'Text', title: 'Legal name', civiFieldType: 'Contact' },
street_address: { validators: ['required', 'email'], title: 'Email', civiFieldType: 'Contact', civiIsLocation: true, civiIsPhone: false },
email: { validators: ['required', 'email'], title: 'Email', civiFieldType: 'Contact', civiIsLocation: true, civiIsPhone: true },
custom_123: { type: 'Checkbox', section: 'custom1', title: 'Likes whiskers on kittens', civiFieldType: 'Individual'},
custom_456: { type: 'Checkbox', section: 'custom1', title: 'Likes dog bites', civiFieldType: 'Individual' },
custom_789: { type: 'Checkbox', section: 'custom1', title: 'Likes bee stings', civiFieldType: 'Individual' },
custom_012: { type: 'Text', section: 'custom2', title: 'Pass phrase', civiFieldType: 'Contact' }
},
initialize: function() {
}
});
CRM.Schema.ActivityModel = CRM.Backbone.Model.extend({
sections: {
'default': {title: 'Activity'},
'custom3': {title: 'Activity: Questions', is_addable: true}
},
schema: {
subject: { type: 'Text', title: 'Subject', civiFieldType: 'Activity' },
location: { type: 'Text', title: 'Location', civiFieldType: 'Activity' },
activity_date_time: { type: 'DateTime', title: 'Date-Time', civiFieldType: 'Activity' },
custom_789: { type: 'Select', section: 'custom3', title: 'How often do you eat cheese?',
options: ['Never', 'Sometimes', 'Often'],
civiFieldType: 'Activity'
}
},
initialize: function() {
}
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,848 @@
(function($, _) {
if (!CRM.UF) CRM.UF = {};
var YESNO = [
{val: 0, label: ts('No')},
{val: 1, label: ts('Yes')}
];
var VISIBILITY = [
{val: 'User and User Admin Only', label: ts('User and User Admin Only'), isInSelectorAllowed: false},
{val: 'Public Pages', label: ts('Expose Publicly'), isInSelectorAllowed: true},
{val: 'Public Pages and Listings', label: ts('Expose Publicly and for Listings'), isInSelectorAllowed: true}
];
var LOCATION_TYPES = _.map(CRM.PseudoConstant.locationType, function(value, key) {
return {val: key, label: value};
});
LOCATION_TYPES.unshift({val: '', label: ts('Primary')});
var DEFAULT_LOCATION_TYPE_ID = '';
var PHONE_TYPES = _.map(CRM.PseudoConstant.phoneType, function(value, key) {
return {val: key, label: value};
});
var WEBSITE_TYPES = _.map(CRM.PseudoConstant.websiteType, function(value, key) {
return {val: key, label: value};
});
var DEFAULT_PHONE_TYPE_ID = PHONE_TYPES[0].val;
var DEFAULT_WEBSITE_TYPE_ID = WEBSITE_TYPES[0].val;
/**
* Add a help link to a form label
*/
function addHelp(title, options) {
return title + ' <a href="#" onclick=\'CRM.help("' + title + '", ' + JSON.stringify(options) + '); return false;\' title="' + ts('%1 Help', {1: title}) + '" class="helpicon"></a>';
}
function watchChanges() {
CRM.designerApp.vent.trigger('ufUnsaved', true);
}
/**
* Parse a "group_type" expression
*
* @param string groupTypeExpr example: "Individual,Activity\0ActivityType:2:28"
* Note: I've seen problems where HTML "&#00;" != JS '\0', so we support ';;' as an equivalent delimiter
* @return Object example: {coreTypes: {"Individual":true,"Activity":true}, subTypes: {"ActivityType":{2: true, 28:true}]}}
*/
CRM.UF.parseTypeList = function(groupTypeExpr) {
var typeList = {coreTypes: {}, subTypes:{}};
// The API may have automatically converted a string with '\0' to an array
var parts = _.isArray(groupTypeExpr) ? groupTypeExpr : groupTypeExpr.replace(';;','\0').split('\0');
var coreTypesExpr = parts[0];
var subTypesExpr = parts[1];
if (!_.isEmpty(coreTypesExpr)) {
_.each(coreTypesExpr.split(','), function(coreType){
typeList.coreTypes[coreType] = true;
});
}
//CRM-15427 Allow Multiple subtype filtering
if (!_.isEmpty(subTypesExpr)) {
if (subTypesExpr.indexOf(';;') !== -1) {
var subTypeparts = subTypesExpr.replace(/;;/g,'\0').split('\0');
_.each(subTypeparts, function(subTypepart) {
var subTypes = subTypepart.split(':');
var subTypeKey = subTypes.shift();
typeList.subTypes[subTypeKey] = {};
_.each(subTypes, function(subTypeId) {
typeList.subTypes[subTypeKey][subTypeId] = true;
});
});
}
else {
var subTypes = subTypesExpr.split(':');
var subTypeKey = subTypes.shift();
typeList.subTypes[subTypeKey] = {};
_.each(subTypes, function(subTypeId) {
typeList.subTypes[subTypeKey][subTypeId] = true;
});
}
}
return typeList;
};
/**
* This function is a hack for generating simulated values of "entity_name"
* in the form-field model.
*
* @param {string} field_type
* @return {string}
*/
CRM.UF.guessEntityName = function(field_type) {
switch (field_type) {
case 'Contact':
case 'Individual':
case 'Organization':
case 'Household':
case 'Formatting':
return 'contact_1';
case 'Activity':
return 'activity_1';
case 'Contribution':
return 'contribution_1';
case 'Membership':
return 'membership_1';
case 'Participant':
return 'participant_1';
case 'Case':
return 'case_1';
default:
if (CRM.contactSubTypes.length && ($.inArray(field_type,CRM.contactSubTypes) > -1)) {
return 'contact_1';
}
else {
throw "Cannot guess entity name for field_type=" + field_type;
}
}
};
/**
* Represents a field in a customizable form.
*/
CRM.UF.UFFieldModel = CRM.Backbone.Model.extend({
/**
* Backbone.Form description of the field to which this refers
*/
defaults: {
help_pre: '',
help_post: '',
/**
* @var bool, non-persistent indication of whether this field is unique or duplicate
* within its UFFieldCollection
*/
is_duplicate: false
},
schema: {
'id': {
type: 'Number'
},
'uf_group_id': {
type: 'Number'
},
'entity_name': {
// pseudo-field
type: 'Text'
},
'field_name': {
type: 'Text'
},
'field_type': {
type: 'Select',
options: ['Contact', 'Individual', 'Organization', 'Contribution', 'Membership', 'Participant', 'Activity']
},
'help_post': {
title: addHelp(ts('Field Post Help'), {id: "help", file:"CRM/UF/Form/Field"}),
type: 'TextArea'
},
'help_pre': {
title: addHelp(ts('Field Pre Help'), {id: "help", file:"CRM/UF/Form/Field"}),
type: 'TextArea'
},
'in_selector': {
title: addHelp(ts('Results Columns?'), {id: "in_selector", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: YESNO
},
'is_active': {
title: addHelp(ts('Active?'), {id: "is_active", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: YESNO
},
'is_multi_summary': {
title: ts("Include in multi-record listing?"),
type: 'Select',
options: YESNO
},
'is_required': {
title: addHelp(ts('Required?'), {id: "is_required", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: YESNO
},
'is_reserved': {
type: 'Select',
options: YESNO
},
'is_searchable': {
title: addHelp(ts("Searchable"), {id: "is_searchable", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: YESNO
},
'is_view': {
title: addHelp(ts('View Only?'), {id: "is_view", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: YESNO
},
'label': {
title: ts('Field Label'),
type: 'Text',
editorAttrs: {maxlength: 255}
},
'location_type_id': {
title: ts('Location Type'),
type: 'Select',
options: LOCATION_TYPES
},
'website_type_id': {
title: ts('Website Type'),
type: 'Select',
options: WEBSITE_TYPES
},
'phone_type_id': {
title: ts('Phone Type'),
type: 'Select',
options: PHONE_TYPES
},
'visibility': {
title: addHelp(ts('Visibility'), {id: "visibility", file:"CRM/UF/Form/Field"}),
type: 'Select',
options: VISIBILITY
},
'weight': {
type: 'Number'
}
},
initialize: function() {
if (this.get('field_name').indexOf('formatting') === 0) {
this.schema.help_pre.title = ts('Markup');
}
this.set('entity_name', CRM.UF.guessEntityName(this.get('field_type')));
this.on("rel:ufGroupModel", this.applyDefaults, this);
this.on('change', watchChanges);
},
applyDefaults: function() {
var fieldSchema = this.getFieldSchema();
if (fieldSchema && fieldSchema.civiIsLocation && !this.get('location_type_id')) {
this.set('location_type_id', DEFAULT_LOCATION_TYPE_ID);
}
if (fieldSchema && fieldSchema.civiIsWebsite && !this.get('website_type_id')) {
this.set('website_type_id', DEFAULT_WEBSITE_TYPE_ID);
}
if (fieldSchema && fieldSchema.civiIsPhone && !this.get('phone_type_id')) {
this.set('phone_type_id', DEFAULT_PHONE_TYPE_ID);
}
},
isInSelectorAllowed: function() {
var visibility = _.first(_.where(VISIBILITY, {val: this.get('visibility')}));
if (visibility) {
return visibility.isInSelectorAllowed;
}
else {
return false;
}
},
getFieldSchema: function() {
return this.getRel('ufGroupModel').getFieldSchema(this.get('entity_name'), this.get('field_name'));
},
/**
* Create a uniqueness signature. Ideally, each UFField in a UFGroup should
* have a unique signature.
*
* @return {String}
*/
getSignature: function() {
return this.get("entity_name") +
'::' + this.get("field_name") +
'::' + (this.get("location_type_id") ? this.get("location_type_id") : this.get("website_type_id") ? this.get("website_type_id") : '') +
'::' + (this.get("phone_type_id") ? this.get("phone_type_id") : '');
},
/**
* This is like destroy(), but it only destroys the item on the client-side;
* it does not trigger REST or Backbone.sync() operations.
*
* @return {Boolean}
*/
destroyLocal: function() {
this.trigger('destroy', this, this.collection, {});
return false;
}
});
/**
* Represents a list of fields in a customizable form
*
* options:
* - uf_group_id: int
*/
CRM.UF.UFFieldCollection = CRM.Backbone.Collection.extend({
model: CRM.UF.UFFieldModel,
uf_group_id: null, // int
initialize: function(models, options) {
options = options || {};
this.uf_group_id = options.uf_group_id;
this.initializeCopyToChildrenRelation('ufGroupModel', options.ufGroupModel, models);
this.on('add', this.watchDuplicates, this);
this.on('remove', this.unwatchDuplicates, this);
this.on('change', watchChanges);
this.on('add', watchChanges);
this.on('remove', watchChanges);
},
getFieldsByName: function(entityName, fieldName) {
return this.filter(function(ufFieldModel) {
return (ufFieldModel.get('entity_name') == entityName && ufFieldModel.get('field_name') == fieldName);
});
},
toSortedJSON: function() {
var fields = this.map(function(ufFieldModel){
return ufFieldModel.toStrictJSON();
});
return _.sortBy(fields, function(ufFieldJSON){
return parseInt(ufFieldJSON.weight);
});
},
isAddable: function(ufFieldModel) {
var entity_name = ufFieldModel.get('entity_name'),
field_name = ufFieldModel.get('field_name'),
fieldSchema = this.getRel('ufGroupModel').getFieldSchema(ufFieldModel.get('entity_name'), ufFieldModel.get('field_name'));
if (field_name.indexOf('formatting') === 0) {
return true;
}
if (! fieldSchema) {
return false;
}
var fields = this.getFieldsByName(entity_name, field_name);
var limit = 1;
if (fieldSchema.civiIsLocation) {
limit *= LOCATION_TYPES.length;
}
if (fieldSchema.civiIsWebsite) {
limit *= WEBSITE_TYPES.length;
}
if (fieldSchema.civiIsPhone) {
limit *= PHONE_TYPES.length;
}
return fields.length < limit;
},
watchDuplicates: function(model, collection, options) {
model.on('change:location_type_id', this.markDuplicates, this);
model.on('change:website_type_id', this.markDuplicates, this);
model.on('change:phone_type_id', this.markDuplicates, this);
this.markDuplicates();
},
unwatchDuplicates: function(model, collection, options) {
model.off('change:location_type_id', this.markDuplicates, this);
model.off('change:website_type_id', this.markDuplicates, this);
model.off('change:phone_type_id', this.markDuplicates, this);
this.markDuplicates();
},
hasDuplicates: function() {
var firstDupe = this.find(function(ufFieldModel){
return ufFieldModel.get('is_duplicate');
});
return firstDupe ? true : false;
},
/**
*
*/
markDuplicates: function() {
var ufFieldModelsByKey = this.groupBy(function(ufFieldModel) {
return ufFieldModel.getSignature();
});
this.each(function(ufFieldModel){
var is_duplicate = ufFieldModelsByKey[ufFieldModel.getSignature()].length > 1;
if (is_duplicate != ufFieldModel.get('is_duplicate')) {
ufFieldModel.set('is_duplicate', is_duplicate);
}
});
}
});
/**
* Represents an entity in a customizable form
*/
CRM.UF.UFEntityModel = CRM.Backbone.Model.extend({
schema: {
'id': {
// title: ts(''),
type: 'Number'
},
'entity_name': {
title: ts('Entity Name'),
help: ts('Symbolic name which referenced in the fields'),
type: 'Text'
},
'entity_type': {
title: ts('Entity Type'),
type: 'Select',
options: ['IndividualModel', 'ActivityModel']
},
'entity_sub_type': {
// Use '*' to match all subtypes; use an int to match a specific type id; use empty-string to match none
title: ts('Sub Type'),
type: 'Text'
}
},
defaults: {
entity_sub_type: '*'
},
initialize: function() {
},
/**
* Get a list of all fields that can be used with this entity.
*
* @return {Object} keys are field names; values are fieldSchemas
*/
getFieldSchemas: function() {
var ufEntityModel = this;
var modelClass= this.getModelClass();
if (this.get('entity_sub_type') == '*') {
return _.clone(modelClass.prototype.schema);
}
var result = {};
_.each(modelClass.prototype.schema, function(fieldSchema, fieldName){
var section = modelClass.prototype.sections[fieldSchema.section];
if (ufEntityModel.isSectionEnabled(section)) {
result[fieldName] = fieldSchema;
}
});
return result;
},
isSectionEnabled: function(section) {
//CRM-15427
return (!section || !section.extends_entity_column_value || _.contains(section.extends_entity_column_value, this.get('entity_sub_type')) || this.get('entity_sub_type') == '*');
},
getSections: function() {
var ufEntityModel = this;
var result = {};
_.each(ufEntityModel.getModelClass().prototype.sections, function(section, sectionKey){
if (ufEntityModel.isSectionEnabled(section)) {
result[sectionKey] = section;
}
});
return result;
},
getModelClass: function() {
return CRM.Schema[this.get('entity_type')];
}
});
/**
* Represents a list of entities in a customizable form
*
* options:
* - ufGroupModel: UFGroupModel
*/
CRM.UF.UFEntityCollection = CRM.Backbone.Collection.extend({
model: CRM.UF.UFEntityModel,
byName: {},
initialize: function(models, options) {
options = options || {};
this.initializeCopyToChildrenRelation('ufGroupModel', options.ufGroupModel, models);
},
/**
*
* @param name
* @return {UFEntityModel} if found; otherwise, null
*/
getByName: function(name) {
// TODO consider indexing
return this.find(function(ufEntityModel){
return ufEntityModel.get('entity_name') == name;
});
}
});
/**
* Represents a customizable form
*/
CRM.UF.UFGroupModel = CRM.Backbone.Model.extend({
defaults: {
title: ts('Unnamed Profile'),
is_active: 1
},
schema: {
'id': {
// title: ts(''),
type: 'Number'
},
'name': {
// title: ts(''),
type: 'Text'
},
'title': {
title: ts('Profile Name'),
help: ts(''),
type: 'Text',
editorAttrs: {maxlength: 64},
validators: ['required']
},
'group_type': {
// For a description of group_type, see CRM_Core_BAO_UFGroup::updateGroupTypes
// title: ts(''),
type: 'Text'
},
'add_captcha': {
title: ts('Include reCAPTCHA?'),
help: ts('FIXME'),
type: 'Select',
options: YESNO
},
'add_to_group_id': {
title: ts('Add new contacts to a Group?'),
help: ts('Select a group if you are using this profile for adding new contacts, AND you want the new contacts to be automatically assigned to a group.'),
type: 'Number'
},
'cancel_URL': {
title: ts('Cancel Redirect URL'),
help: ts('If you are using this profile as a contact signup or edit form, and want to redirect the user to a static URL if they click the Cancel button - enter the complete URL here. If this field is left blank, the built-in Profile form will be redisplayed.'),
type: 'Text'
},
'created_date': {
//title: ts(''),
type: 'Text'// FIXME
},
'created_id': {
//title: ts(''),
type: 'Number'
},
'help_post': {
title: ts('Post-form Help'),
help: ts('Explanatory text displayed at the end of the form.') +
ts('Note that this help text is displayed on profile create/edit screens only.'),
type: 'TextArea'
},
'help_pre': {
title: ts('Pre-form Help'),
help: ts('Explanatory text displayed at the beginning of the form.') +
ts('Note that this help text is displayed on profile create/edit screens only.'),
type: 'TextArea'
},
'is_active': {
title: ts('Is this CiviCRM Profile active?'),
type: 'Select',
options: YESNO
},
'is_cms_user': {
title: ts('Drupal user account registration option?'),// FIXME
help: ts('FIXME'),
type: 'Select',
options: YESNO // FIXME
},
'is_edit_link': {
title: ts('Include profile edit links in search results?'),
help: ts('Check this box if you want to include a link in the listings to Edit profile fields. Only users with permission to edit the contact will see this link.'),
type: 'Select',
options: YESNO
},
'is_map': {
title: ts('Enable mapping for this profile?'),
help: ts('If enabled, a Map link is included on the profile listings rows and detail screens for any contacts whose records include sufficient location data for your mapping provider.'),
type: 'Select',
options: YESNO
},
'is_proximity_search': {
title: ts('Proximity Search'),
help: ts('FIXME'),
type: 'Select',
options: YESNO // FIXME
},
'is_reserved': {
// title: ts(''),
type: 'Select',
options: YESNO
},
'is_uf_link': {
title: ts('Include Drupal user account information links in search results?'), // FIXME
help: ts('FIXME'),
type: 'Select',
options: YESNO
},
'is_update_dupe': {
title: ts('What to do upon duplicate match'),
help: ts('FIXME'),
type: 'Select',
options: YESNO // FIXME
},
'limit_listings_group_id': {
title: ts('Limit listings to a specific Group?'),
help: ts('Select a group if you are using this profile for search and listings, AND you want to limit the listings to members of a specific group.'),
type: 'Number'
},
'notify': {
title: ts('Notify when profile form is submitted?'),
help: ts('If you want member(s) of your organization to receive a notification email whenever this Profile form is used to enter or update contact information, enter one or more email addresses here. Multiple email addresses should be separated by a comma (e.g. jane@example.org, paula@example.org). The first email address listed will be used as the FROM address in the notifications.'),
type: 'TextArea'
},
'post_URL': {
title: ts('Redirect URL'),
help: ts("If you are using this profile as a contact signup or edit form, and want to redirect the user to a static URL after they've submitted the form, you can also use contact tokens in URL - enter the complete URL here. If this field is left blank, the built-in Profile form will be redisplayed with a generic status message - 'Your contact information has been saved.'"),
type: 'Text'
},
'weight': {
title: ts('Order'),
help: ts('Weight controls the order in which profiles are presented when more than one profile is included in User Registration or My Account screens. Enter a positive or negative integer - lower numbers are displayed ahead of higher numbers.'),
type: 'Number'
// FIXME positive int
}
},
initialize: function() {
var ufGroupModel = this;
if (!this.getRel('ufEntityCollection')) {
var ufEntityCollection = new CRM.UF.UFEntityCollection([], {
ufGroupModel: this,
silent: false
});
this.setRel('ufEntityCollection', ufEntityCollection);
}
if (!this.getRel('ufFieldCollection')) {
var ufFieldCollection = new CRM.UF.UFFieldCollection([], {
uf_group_id: this.id,
ufGroupModel: this
});
this.setRel('ufFieldCollection', ufFieldCollection);
}
if (!this.getRel('paletteFieldCollection')) {
var paletteFieldCollection = new CRM.Designer.PaletteFieldCollection([], {
ufGroupModel: this
});
paletteFieldCollection.sync = function(method, model, options) {
if (!options) options = {};
// console.log(method, model, options);
switch (method) {
case 'read':
var success = options.success;
options.success = function(resp, status, xhr) {
if (success) success(resp, status, xhr);
model.trigger('sync', model, resp, options);
};
success(ufGroupModel.buildPaletteFields());
break;
case 'create':
case 'update':
case 'delete':
throw 'Unsupported method: ' + method;
default:
throw 'Unsupported method: ' + method;
}
};
this.setRel('paletteFieldCollection', paletteFieldCollection);
}
this.getRel('ufEntityCollection').on('reset', this.resetEntities, this);
this.resetEntities();
this.on('change', watchChanges);
},
/**
* Generate a copy of this UFGroupModel and its fields, with all ID's removed. The result
* is suitable for a new, identical UFGroup.
*
* @return {CRM.UF.UFGroupModel}
*/
deepCopy: function() {
var copy = new CRM.UF.UFGroupModel(_.omit(this.toStrictJSON(), ['id','created_id','created_date','is_reserved','group_type']));
copy.getRel('ufEntityCollection').reset(
this.getRel('ufEntityCollection').toJSON()
// FIXME: for configurable entities, omit ['id', 'uf_group_id']
);
copy.getRel('ufFieldCollection').reset(
this.getRel('ufFieldCollection').map(function(ufFieldModel) {
return _.omit(ufFieldModel.toStrictJSON(), ['id', 'uf_group_id']);
})
);
var copyLabel = ' ' + ts('(Copy)');
copy.set('title', copy.get('title').slice(0, 64 - copyLabel.length) + copyLabel);
return copy;
},
getModelClass: function(entity_name) {
var ufEntity = this.getRel('ufEntityCollection').getByName(entity_name);
if (!ufEntity) throw 'Failed to locate entity: ' + entity_name;
return ufEntity.getModelClass();
},
getFieldSchema: function(entity_name, field_name) {
if (field_name.indexOf('formatting') === 0) {
field_name = 'formatting';
}
var modelClass = this.getModelClass(entity_name);
var fieldSchema = modelClass.prototype.schema[field_name];
if (!fieldSchema) {
CRM.console('warn', 'Failed to locate field: ' + entity_name + "." + field_name);
return null;
}
return fieldSchema;
},
/**
* Check that the group_type contains *only* the types listed in validTypes
*
* @param string validTypesExpr
* @param bool allowAllSubtypes
* @return {Boolean}
*/
//CRM-15427
checkGroupType: function(validTypesExpr, allowAllSubtypes, usedByFilter) {
var allMatched = true;
allowAllSubtypes = allowAllSubtypes || false;
usedByFilter = usedByFilter || null;
if (_.isEmpty(this.get('group_type'))) {
return true;
}
if (usedByFilter && _.isEmpty(this.get('module'))) {
return false;
}
var actualTypes = CRM.UF.parseTypeList(this.get('group_type'));
var validTypes = CRM.UF.parseTypeList(validTypesExpr);
// Every actual.coreType is a valid.coreType
_.each(actualTypes.coreTypes, function(ignore, actualCoreType) {
if (! validTypes.coreTypes[actualCoreType]) {
allMatched = false;
}
});
// CRM-16915 - filter with usedBy module if specified.
if (usedByFilter && this.get('module') != usedByFilter) {
allMatched = false;
}
//CRM-15427 allow all subtypes
if (!$.isEmptyObject(validTypes.subTypes) && !allowAllSubtypes) {
// Every actual.subType is a valid.subType
_.each(actualTypes.subTypes, function(actualSubTypeIds, actualSubTypeKey) {
if (!validTypes.subTypes[actualSubTypeKey]) {
allMatched = false;
return;
}
// actualSubTypeIds is a list of all subtypes which can be used by group,
// so it's sufficient to match any one of them
var subTypeMatched = false;
_.each(actualSubTypeIds, function(ignore, actualSubTypeId) {
if (validTypes.subTypes[actualSubTypeKey][actualSubTypeId]) {
subTypeMatched = true;
}
});
allMatched = allMatched && subTypeMatched;
});
}
return allMatched;
},
calculateContactEntityType: function() {
var ufGroupModel = this;
// set proper entity model based on selected profile
var contactTypes = ['Individual', 'Household', 'Organization'];
var profileType = ufGroupModel.get('group_type') || '';
// check if selected profile have subtype defined eg: ["Individual,Contact,Case", "caseType:7"]
if (_.isArray(profileType) && profileType[0]) {
profileType = profileType[0];
}
profileType = profileType.split(',');
var ufEntityModel;
_.each(profileType, function (ptype) {
if ($.inArray(ptype, contactTypes) > -1) {
ufEntityModel = ptype + 'Model';
return true;
}
});
return ufEntityModel;
},
setUFGroupModel: function(entityType, allEntityModels) {
var ufGroupModel = this;
var newUfEntityModels = [];
_.each(allEntityModels, function (values) {
if (entityType && values.entity_name == 'contact_1') {
values.entity_type = entityType;
}
newUfEntityModels.push(new CRM.UF.UFEntityModel(values));
});
ufGroupModel.getRel('ufEntityCollection').reset(newUfEntityModels);
},
resetEntities: function() {
var ufGroupModel = this;
var deleteFieldList = [];
ufGroupModel.getRel('ufFieldCollection').each(function(ufFieldModel){
if (!ufFieldModel.getFieldSchema()) {
CRM.alert(ts('This profile no longer includes field "%1"! All references to the field have been removed.', {
1: ufFieldModel.get('label')
}), '', 'alert', {expires: false});
deleteFieldList.push(ufFieldModel);
}
});
_.each(deleteFieldList, function(ufFieldModel) {
ufFieldModel.destroyLocal();
});
this.getRel('paletteFieldCollection').reset(this.buildPaletteFields());
// reset to redraw the cancel after entity type is updated.
ufGroupModel.getRel('ufFieldCollection').reset(ufGroupModel.getRel('ufFieldCollection').toJSON());
},
/**
*
* @return {Array} of PaletteFieldModel
*/
buildPaletteFields: function() {
// rebuild list of fields; reuse old instances of PaletteFieldModel and create new ones
// as appropriate
// Note: The system as a whole is ill-defined in cases where we have an existing
// UFField that references a model field that disappears.
var ufGroupModel = this;
var oldPaletteFieldModelsBySig = {};
this.getRel('paletteFieldCollection').each(function(paletteFieldModel){
oldPaletteFieldModelsBySig[paletteFieldModel.get("entityName") + '::' + paletteFieldModel.get("fieldName")] = paletteFieldModel;
});
var newPaletteFieldModels = [];
this.getRel('ufEntityCollection').each(function(ufEntityModel){
var modelClass = ufEntityModel.getModelClass();
_.each(ufEntityModel.getFieldSchemas(), function(value, key, list) {
var model = oldPaletteFieldModelsBySig[ufEntityModel.get('entity_name') + '::' + key];
if (!model) {
model = new CRM.Designer.PaletteFieldModel({
modelClass: modelClass,
entityName: ufEntityModel.get('entity_name'),
fieldName: key
});
}
newPaletteFieldModels.push(model);
});
});
return newPaletteFieldModels;
}
});
/**
* Represents a list of customizable form
*/
CRM.UF.UFGroupCollection = CRM.Backbone.Collection.extend({
model: CRM.UF.UFGroupModel
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,3 @@
if (!window.CRM) window.CRM = {};
window.cj = CRM.$ = jQuery.noConflict(true);
CRM._ = _.noConflict();

View file

@ -0,0 +1,878 @@
(function($, _) {
if (!CRM.Designer) CRM.Designer = {};
/**
* When rendering a template with Marionette.ItemView, the list of variables is determined by
* serializeData(). The normal behavior is to map each property of this.model to a template
* variable.
*
* This function extends that practice by exporting variables "_view", "_model", "_collection",
* and "_options". This makes it easier for the template to, e.g., access computed properties of
* a model (by calling "_model.getComputedProperty"), or to access constructor options (by
* calling "_options.myoption").
*
* @return {*}
*/
var extendedSerializeData = function() {
var result = Marionette.ItemView.prototype.serializeData.apply(this);
result._view = this;
result._model = this.model;
result._collection = this.collection;
result._options = this.options;
return result;
};
/**
* Display a dialog window with an editable form for a UFGroupModel
*
* The implementation here is very "jQuery-style" and not "Backbone-style";
* it's been extracted
*
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.DesignerDialog = Backbone.Marionette.Layout.extend({
serializeData: extendedSerializeData,
template: '#designer_dialog_template',
className: 'crm-designer-dialog',
regions: {
designerRegion: '.crm-designer'
},
/** @var bool whether this dialog is currently open */
isDialogOpen: false,
/** @var bool whether any changes have been made */
isUfUnsaved: false,
/** @var obj handle for the CRM.alert containing undo link */
undoAlert: null,
/** @var bool whether this dialog is being re-opened by the undo link */
undoState: false,
initialize: function(options) {
CRM.designerApp.vent.on('ufUnsaved', this.onUfChanged, this);
CRM.designerApp.vent.on('ufSaved', this.onUfSaved, this);
},
onClose: function() {
if (this.undoAlert && this.undoAlert.close) this.undoAlert.close();
CRM.designerApp.vent.off('ufUnsaved', this.onUfChanged, this);
},
onUfChanged: function(isUfUnsaved) {
this.isUfUnsaved = isUfUnsaved;
},
onUfSaved: function() {
CRM.designerApp.vent.off('ufUnsaved', this.onUfChanged, this);
this.isUfUnsaved = false;
},
onRender: function() {
var designerDialog = this;
designerDialog.$el.dialog({
autoOpen: true, // note: affects accordion height
title: ts('Edit Profile'),
modal: true,
width: '75%',
height: parseInt($(window).height() * 0.8, 10),
minWidth: 500,
minHeight: 600, // to allow dropping in big whitespace, coordinate with min-height of .crm-designer-fields
open: function() {
// Prevent conflicts with other onbeforeunload handlers
designerDialog.oldOnBeforeUnload = window.onbeforeunload;
// Warn of unsaved changes when navigating away from the page
window.onbeforeunload = function() {
if (designerDialog.isDialogOpen && designerDialog.isUfUnsaved) {
return ts("Your profile has not been saved.");
}
if (designerDialog.oldOnBeforeUnload) {
return designerDialog.oldOnBeforeUnload.apply(arguments);
}
};
if (designerDialog.undoAlert && designerDialog.undoAlert.close) designerDialog.undoAlert.close();
designerDialog.isDialogOpen = true;
// Initialize new dialog if we are not re-opening unsaved changes
if (designerDialog.undoState === false) {
if (designerDialog.designerRegion && designerDialog.designerRegion.close) designerDialog.designerRegion.close();
designerDialog.$el.block();
designerDialog.options.findCreateUfGroupModel({
onLoad: function(ufGroupModel) {
designerDialog.model = ufGroupModel;
var designerLayout = new CRM.Designer.DesignerLayout({
model: ufGroupModel,
el: '<div class="full-height"></div>'
});
designerDialog.$el.unblock();
designerDialog.designerRegion.show(designerLayout);
CRM.designerApp.vent.trigger('resize');
designerDialog.isUfUnsaved = false;
}
});
}
designerDialog.undoState = false;
// CRM-12188
CRM.designerApp.DetachedProfiles = [];
},
close: function() {
window.onbeforeunload = designerDialog.oldOnBeforeUnload;
designerDialog.isDialogOpen = false;
if (designerDialog.undoAlert && designerDialog.undoAlert.close) designerDialog.undoAlert.close();
if (designerDialog.isUfUnsaved) {
designerDialog.undoAlert = CRM.alert('<p>' + ts('%1 has not been saved.', {1: designerDialog.model.get('title')}) + '</p><a href="#" class="crm-undo">' + ts('Restore') + '</a>', ts('Unsaved Changes'), 'alert', {expires: 60000});
$('.ui-notify-message a.crm-undo').button({icons: {primary: 'fa-undo'}}).click(function(e) {
e.preventDefault();
designerDialog.undoState = true;
designerDialog.$el.dialog('open');
});
}
// CRM-12188
CRM.designerApp.restorePreviewArea();
},
resize: function() {
CRM.designerApp.vent.trigger('resize');
}
});
}
});
/**
* Display a complete form-editing UI, including canvas, palette, and
* buttons.
*
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.DesignerLayout = Backbone.Marionette.Layout.extend({
serializeData: extendedSerializeData,
template: '#designer_template',
regions: {
buttons: '.crm-designer-buttonset-region',
palette: '.crm-designer-palette-region',
form: '.crm-designer-form-region',
fields: '.crm-designer-fields-region'
},
initialize: function() {
CRM.designerApp.vent.on('resize', this.onResize, this);
},
onClose: function() {
CRM.designerApp.vent.off('resize', this.onResize, this);
},
onRender: function() {
this.buttons.show(new CRM.Designer.ToolbarView({
model: this.model
}));
this.palette.show(new CRM.Designer.PaletteView({
model: this.model
}));
this.form.show(new CRM.Designer.UFGroupView({
model: this.model
}));
this.fields.show(new CRM.Designer.UFFieldCanvasView({
model: this.model
}));
},
onResize: function() {
if (! this.hasResizedBefore) {
this.hasResizedBefore = true;
this.$('.crm-designer-toolbar').resizable({
handles: 'w',
maxWidth: 400,
minWidth: 150,
resize: function(event, ui) {
$('.crm-designer-canvas').css('margin-right', (ui.size.width + 10) + 'px');
$(this).css({left: '', height: ''});
}
}).css({left: '', height: ''});
}
}
});
/**
* Display toolbar with working button
*
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.ToolbarView = Backbone.Marionette.ItemView.extend({
serializeData: extendedSerializeData,
template: '#designer_buttons_template',
previewMode: false,
events: {
'click .crm-designer-save': 'doSave',
'click .crm-designer-preview': 'doPreview'
},
onRender: function() {
this.$('.crm-designer-save').button({icons: {primary: 'fa-check'}}).attr({
disabled: 'disabled',
style: 'opacity:.5; cursor:default;'
});
this.$('.crm-designer-preview').button({icons: {primary: 'fa-television'}});
},
initialize: function(options) {
CRM.designerApp.vent.on('ufUnsaved', this.onUfChanged, this);
},
onUfChanged: function(isUfUnsaved) {
if (isUfUnsaved) {
this.$('.crm-designer-save').removeAttr('style').prop('disabled', false);
}
},
doSave: function(e) {
e.preventDefault();
var ufGroupModel = this.model;
if (ufGroupModel.getRel('ufFieldCollection').hasDuplicates()) {
CRM.alert(ts('Please correct errors before saving.'), '', 'alert');
return;
}
var $dialog = this.$el.closest('.crm-designer-dialog'); // FIXME use events
$dialog.block();
var profile = ufGroupModel.toStrictJSON();
profile["api.UFField.replace"] = {values: ufGroupModel.getRel('ufFieldCollection').toSortedJSON(), 'option.autoweight': 0};
CRM.api('UFGroup', 'create', profile, {
success: function(data) {
$dialog.unblock();
var error = false;
if (data.is_error) {
CRM.alert(data.error_message);
error = true;
}
_.each(data.values, function(ufGroupResponse) {
if (ufGroupResponse['api.UFField.replace'].is_error) {
CRM.alert(ufGroupResponse['api.UFField.replace'].error_message);
error = true;
}
});
if (!error) {
if (!ufGroupModel.get('id')) {
ufGroupModel.set('id', data.id);
}
CRM.designerApp.vent.trigger('ufUnsaved', false);
CRM.designerApp.vent.trigger('ufSaved');
$dialog.dialog('close');
}
}
});
},
doPreview: function(e) {
e.preventDefault();
this.previewMode = !this.previewMode;
if (!this.previewMode) {
$('.crm-designer-preview-canvas').html('');
$('.crm-designer-canvas > *, .crm-designer-palette-region').show();
$('.crm-designer-preview').button('option', {icons: {primary: 'fa-television'}}).find('span').text(ts('Preview'));
return;
}
if (this.model.getRel('ufFieldCollection').hasDuplicates()) {
CRM.alert(ts('Please correct errors before previewing.'), '', 'alert');
return;
}
var $dialog = this.$el.closest('.crm-designer-dialog'); // FIXME use events
$dialog.block();
// CRM-12188
CRM.designerApp.clearPreviewArea();
$.post(CRM.url("civicrm/ajax/inline"), {
'qfKey': CRM.profilePreviewKey,
'class_name': 'CRM_UF_Form_Inline_Preview',
'snippet': 1,
'ufData': JSON.stringify({
ufGroup: this.model.toStrictJSON(),
ufFieldCollection: this.model.getRel('ufFieldCollection').toSortedJSON()
})
}).done(function(data) {
$dialog.unblock();
$('.crm-designer-canvas > *, .crm-designer-palette-region').hide();
$('.crm-designer-preview-canvas').html(data).show().trigger('crmLoad').find(':input').prop('readOnly', true);
$('.crm-designer-preview').button('option', {icons: {primary: 'fa-pencil'}}).find('span').text(ts('Edit'));
});
}
});
/**
* Display a selection of available fields
*
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.PaletteView = Backbone.Marionette.ItemView.extend({
serializeData: extendedSerializeData,
template: '#palette_template',
el: '<div class="full-height"></div>',
openTreeNodes: [],
events: {
'keyup .crm-designer-palette-search input': 'doSearch',
'change .crm-contact-types': 'doSetPaletteEntity',
'click .crm-designer-palette-clear-search': 'clearSearch',
'click .crm-designer-palette-toggle': 'toggleAll',
'click .crm-designer-palette-add button': 'doNewCustomFieldDialog',
'click #crm-designer-add-custom-set': 'doNewCustomSetDialog',
'dblclick .crm-designer-palette-field': 'doAddToCanvas'
},
initialize: function() {
this.model.getRel('ufFieldCollection')
.on('add', this.toggleActive, this)
.on('remove', this.toggleActive, this);
this.model.getRel('paletteFieldCollection')
.on('reset', this.render, this);
CRM.designerApp.vent.on('resize', this.onResize, this);
},
onClose: function() {
this.model.getRel('ufFieldCollection')
.off('add', this.toggleActive, this)
.off('remove', this.toggleActive, this);
this.model.getRel('paletteFieldCollection')
.off('reset', this.render, this);
CRM.designerApp.vent.off('resize', this.onResize, this);
},
onRender: function() {
var paletteView = this;
// Prepare data for jstree
var treeData = [];
var paletteFieldsByEntitySection = this.model.getRel('paletteFieldCollection').getFieldsByEntitySection();
paletteView.model.getRel('ufEntityCollection').each(function(ufEntityModel){
_.each(ufEntityModel.getSections(), function(section, sectionKey){
var defaultValue = paletteView.selectedContactType;
if (!defaultValue) {
defaultValue = paletteView.model.calculateContactEntityType();
}
// set selected option as default, since we are rebuilding palette
paletteView.$('.crm-contact-types').val(defaultValue).prop('selected','selected');
var entitySection = ufEntityModel.get('entity_name') + '-' + sectionKey;
var items = [];
if (paletteFieldsByEntitySection[entitySection]) {
_.each(paletteFieldsByEntitySection[entitySection], function(paletteFieldModel, k) {
items.push({data: paletteFieldModel.getLabel(), attr: {'class': 'crm-designer-palette-field', 'data-plm-cid': paletteFieldModel.cid}});
});
}
if (section.is_addable) {
items.push({data: ts('+ Add New Field'), attr: {'class': 'crm-designer-palette-add'}});
}
if (items.length > 0) {
treeData.push({
data: section.title,
children: items,
state: _.contains(paletteView.openTreeNodes, sectionKey) ? 'open' : 'closed',
attr: {
'class': 'crm-designer-palette-section',
'data-section': sectionKey,
'data-entity': ufEntityModel.get('entity_name')
}
});
}
});
});
this.$('.crm-designer-palette-tree').jstree({
'json_data': {data: treeData},
'search': {
'case_insensitive' : true,
'show_only_matches': true
},
themes: {
"theme": 'classic',
"dots": false,
"icons": false,
"url": CRM.config.resourceBase + 'packages/jquery/plugins/jstree/themes/classic/style.css'
},
'plugins': ['themes', 'json_data', 'ui', 'search']
}).bind('loaded.jstree', function () {
$('.crm-designer-palette-field', this).draggable({
appendTo: '.crm-designer',
zIndex: $(this.$el).css("zIndex") + 5000,
helper: 'clone',
connectToSortable: '.crm-designer-fields' // FIXME: tight canvas/palette coupling
});
paletteView.model.getRel('ufFieldCollection').each(function(ufFieldModel) {
paletteView.toggleActive(ufFieldModel, paletteView.model.getRel('ufFieldCollection'));
});
paletteView.$('.crm-designer-palette-add a').replaceWith('<button>' + $('.crm-designer-palette-add a').first().text() + '</<button>');
paletteView.$('.crm-designer-palette-tree > ul').append('<li><button id="crm-designer-add-custom-set">+ ' + ts('Add Set of Custom Fields') + '</button></li>');
paletteView.$('.crm-designer-palette-tree button').button();
}).bind("select_node.jstree", function (e, data) {
$(this).jstree("toggle_node", data.rslt.obj);
$(this).jstree("deselect_node", data.rslt.obj);
});
// FIXME: tight canvas/palette coupling
this.$(".crm-designer-fields").droppable({
activeClass: "ui-state-default",
hoverClass: "ui-state-hover",
accept: ":not(.ui-sortable-helper)"
});
this.onResize();
},
onResize: function() {
var pos = this.$('.crm-designer-palette-tree').position();
var div = this.$('.crm-designer-palette-tree').closest('.crm-container').height();
this.$('.crm-designer-palette-tree').css({height: div - pos.top});
},
doSearch: function(e) {
var str = $(e.target).val();
this.$('.crm-designer-palette-clear-search').css('visibility', str ? 'visible' : 'hidden');
this.$('.crm-designer-palette-tree').jstree("search", str);
},
doSetPaletteEntity: function(event) {
this.selectedContactType = $('.crm-contact-types :selected').val();
// loop through entity collection and remove non-valid entity section's
var newUfEntityModels = [];
this.model.getRel('ufEntityCollection').each(function(oldUfEntityModel){
var values = oldUfEntityModel.toJSON();
if (values.entity_name == 'contact_1') {
values.entity_type = $('.crm-contact-types :selected').val();
}
newUfEntityModels.push(new CRM.UF.UFEntityModel(values));
});
this.model.getRel('ufEntityCollection').reset(newUfEntityModels);
},
doAddToCanvas: function(event) {
var paletteFieldModel = this.model.getRel('paletteFieldCollection').get($(event.currentTarget).attr('data-plm-cid'));
paletteFieldModel.addToUFCollection(this.model.getRel('ufFieldCollection'));
event.stopPropagation();
},
doNewCustomFieldDialog: function(e) {
e.preventDefault();
var paletteView = this;
var entityKey = $(e.currentTarget).closest('.crm-designer-palette-section').attr('data-entity');
var sectionKey = $(e.currentTarget).closest('.crm-designer-palette-section').attr('data-section');
var ufEntityModel = paletteView.model.getRel('ufEntityCollection').getByName(entityKey);
var sections = ufEntityModel.getSections();
var url = CRM.url('civicrm/admin/custom/group/field/add', {
reset: 1,
action: 'add',
gid: sections[sectionKey].custom_group_id
});
CRM.loadForm(url).on('crmFormSuccess', function(e, data) {
paletteView.doRefresh('custom_' + data.id);
});
},
doNewCustomSetDialog: function(e) {
e.preventDefault();
var paletteView = this;
var url = CRM.url('civicrm/admin/custom/group', 'action=add&reset=1');
// Create custom field set and automatically go to next step (create fields) after save button is clicked.
CRM.loadForm(url, {refreshAction: ['next']})
.on('crmFormSuccess', function(e, data) {
// When form switches to create custom field context, modify button behavior to only continue for "save and new"
if (data.customField) ($(this).data('civiCrmSnippet').options.crmForm.refreshAction = ['next_new']);
paletteView.doRefresh(data.customField ? 'custom_' + data.id : null);
});
},
doRefresh: function(fieldToAdd) {
var ufGroupModel = this.model;
this.getOpenTreeNodes();
CRM.Schema.reloadModels()
.done(function(data){
ufGroupModel.resetEntities();
if (fieldToAdd) {
var field = ufGroupModel.getRel('paletteFieldCollection').getFieldByName(null, fieldToAdd);
field.addToUFCollection(ufGroupModel.getRel('ufFieldCollection'));
}
})
.fail(function() {
CRM.alert(ts('Failed to retrieve schema'), ts('Error'), 'error');
});
},
clearSearch: function(e) {
e.preventDefault();
$('.crm-designer-palette-search input').val('').keyup();
},
toggleActive: function(ufFieldModel, ufFieldCollection, options) {
var paletteFieldCollection = this.model.getRel('paletteFieldCollection');
var paletteFieldModel = paletteFieldCollection.getFieldByName(ufFieldModel.get('entity_name'), ufFieldModel.get('field_name'));
var isAddable = ufFieldCollection.isAddable(ufFieldModel);
if (paletteFieldModel) {
this.$('[data-plm-cid='+paletteFieldModel.cid+']').toggleClass('disabled', !isAddable);
}
},
toggleAll: function(e) {
if (_.isEmpty($('.crm-designer-palette-search input').val())) {
$('.crm-designer-palette-tree').jstree($(e.target).attr('rel'));
}
e.preventDefault();
},
getOpenTreeNodes: function() {
var paletteView = this;
this.openTreeNodes = [];
this.$('.crm-designer-palette-section.jstree-open').each(function() {
paletteView.openTreeNodes.push($(this).data('section'));
});
}
});
/**
* Display all UFFieldModel objects in a UFGroupModel.
*
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.UFFieldCanvasView = Backbone.Marionette.View.extend({
initialize: function() {
this.model.getRel('ufFieldCollection')
.on('add', this.updatePlaceholder, this)
.on('remove', this.updatePlaceholder, this)
.on('add', this.addUFFieldView, this)
.on('reset', this.render, this);
},
onClose: function() {
this.model.getRel('ufFieldCollection')
.off('add', this.updatePlaceholder, this)
.off('remove', this.updatePlaceholder, this)
.off('add', this.addUFFieldView, this)
.off('reset', this.render, this);
},
render: function() {
var ufFieldCanvasView = this;
this.$el.html(_.template($('#field_canvas_view_template').html()));
// BOTTOM: Setup field-level editing
var $fields = this.$('.crm-designer-fields');
this.updatePlaceholder();
var ufFieldModels = this.model.getRel('ufFieldCollection').sortBy(function(ufFieldModel) {
return parseInt(ufFieldModel.get('weight'));
});
_.each(ufFieldModels, function(ufFieldModel) {
ufFieldCanvasView.addUFFieldView(ufFieldModel, ufFieldCanvasView.model.getRel('ufFieldCollection'), {skipWeights: true});
});
this.$(".crm-designer-fields").sortable({
placeholder: 'crm-designer-row-placeholder',
forcePlaceholderSize: true,
cancel: 'input,textarea,button,select,option,a,.crm-designer-open',
receive: function(event, ui) {
var paletteFieldModel = ufFieldCanvasView.model.getRel('paletteFieldCollection').get(ui.item.attr('data-plm-cid'));
var ufFieldModel = paletteFieldModel.addToUFCollection(
ufFieldCanvasView.model.getRel('ufFieldCollection'),
{skipWeights: true}
);
if (_.isEmpty(ufFieldModel)) {
ufFieldCanvasView.$('.crm-designer-fields .ui-draggable').remove();
} else {
// Move from end to the 'dropped' position
var ufFieldViewEl = ufFieldCanvasView.$('div[data-field-cid='+ufFieldModel.cid+']').parent();
ufFieldCanvasView.$('.crm-designer-fields .ui-draggable').replaceWith(ufFieldViewEl);
}
// note: the sortable() update callback will call updateWeight
},
update: function() {
ufFieldCanvasView.updateWeights();
}
});
},
/** Determine visual order of fields and set the model values for "weight" */
updateWeights: function() {
var ufFieldCanvasView = this;
var weight = 1;
var rows = this.$('.crm-designer-row').each(function(key, row) {
if ($(row).hasClass('placeholder')) {
return;
}
var ufFieldCid = $(row).attr('data-field-cid');
var ufFieldModel = ufFieldCanvasView.model.getRel('ufFieldCollection').get(ufFieldCid);
ufFieldModel.set('weight', weight);
weight++;
});
},
addUFFieldView: function(ufFieldModel, ufFieldCollection, options) {
var paletteFieldModel = this.model.getRel('paletteFieldCollection').getFieldByName(ufFieldModel.get('entity_name'), ufFieldModel.get('field_name'));
var ufFieldView = new CRM.Designer.UFFieldView({
el: $("<div></div>"),
model: ufFieldModel,
paletteFieldModel: paletteFieldModel
});
ufFieldView.render();
this.$('.crm-designer-fields').append(ufFieldView.$el);
if (! (options && options.skipWeights)) {
this.updateWeights();
}
},
updatePlaceholder: function() {
if (this.model.getRel('ufFieldCollection').isEmpty()) {
this.$('.placeholder').css({display: 'block', border: '0 none', cursor: 'default'});
} else {
this.$('.placeholder').hide();
}
}
});
/**
* options:
* - model: CRM.UF.UFFieldModel
* - paletteFieldModel: CRM.Designer.PaletteFieldModel
*/
CRM.Designer.UFFieldView = Backbone.Marionette.Layout.extend({
serializeData: extendedSerializeData,
template: '#field_row_template',
expanded: false,
regions: {
summary: '.crm-designer-field-summary',
detail: '.crm-designer-field-detail'
},
events: {
"click .crm-designer-action-settings": 'doToggleForm',
"click button.crm-designer-edit-custom": 'doEditCustomField',
"click .crm-designer-action-remove": 'doRemove'
},
modelEvents: {
"destroy": 'remove',
"change:is_duplicate": 'onChangeIsDuplicate'
},
onRender: function() {
this.summary.show(new CRM.Designer.UFFieldSummaryView({
model: this.model,
fieldSchema: this.model.getFieldSchema(),
paletteFieldModel: this.options.paletteFieldModel
}));
this.detail.show(new CRM.Designer.UFFieldDetailView({
model: this.model,
fieldSchema: this.model.getFieldSchema()
}));
this.onChangeIsDuplicate(this.model, this.model.get('is_duplicate'));
if (!this.expanded) {
this.detail.$el.hide();
}
var that = this;
CRM.designerApp.vent.on('formOpened', function(event) {
if (that.expanded && event != that.cid) {
that.doToggleForm(false);
}
});
},
doToggleForm: function(event) {
this.expanded = !this.expanded;
if (this.expanded && event !== false) {
CRM.designerApp.vent.trigger('formOpened', this.cid);
}
this.$el.toggleClass('crm-designer-open', this.expanded);
var $detail = this.detail.$el;
if (!this.expanded) {
$detail.toggle('blind', 250);
this.$('button.crm-designer-edit-custom').remove();
}
else {
var $canvas = $('.crm-designer-canvas');
var top = $canvas.offset().top;
$detail.slideDown({
duration: 250,
step: function(num, effect) {
// Scroll canvas to keep field details visible
if (effect.prop == 'height') {
if (effect.now + $detail.offset().top - top > $canvas.height() - 9) {
$canvas.scrollTop($canvas.scrollTop() + effect.now + $detail.offset().top - top - $canvas.height() + 9);
}
}
}
});
if (this.model.get('field_name').split('_')[0] == 'custom') {
this.$('.crm-designer-field-summary > div').append('<button class="crm-designer-edit-custom">' + ts('Edit Custom Field') + '</button>');
this.$('button.crm-designer-edit-custom').button({icons: {primary: 'fa-pencil'}}).attr('title', ts('Edit global settings for this custom field.'));
}
}
},
doEditCustomField: function(e) {
e.preventDefault();
var url = CRM.url('civicrm/admin/custom/group/field/update', {
action: 'update',
reset: 1,
id: this.model.get('field_name').split('_')[1]
});
var form1 = CRM.loadForm(url)
.on('crmFormLoad', function() {
$(this).prepend('<div class="messages status"><i class="crm-i fa-info-circle"></i> ' + ts('Note: This will modify the field system-wide, not just in this profile form.') + '</div>');
});
},
onChangeIsDuplicate: function(model, value, options) {
this.$el.toggleClass('crm-designer-duplicate', value);
},
doRemove: function(event) {
var that = this;
this.$el.hide(250, function() {
that.model.destroyLocal();
});
}
});
/**
* options:
* - model: CRM.UF.UFFieldModel
* - fieldSchema: (Backbone.Form schema element)
* - paletteFieldModel: CRM.Designer.PaletteFieldModel
*/
CRM.Designer.UFFieldSummaryView = Backbone.Marionette.ItemView.extend({
serializeData: extendedSerializeData,
template: '#field_summary_template',
modelEvents: {
'change': 'render'
},
/**
* Compose a printable string which describes the binding of this UFField to the data model
* @return {String}
*/
getBindingLabel: function() {
var result = this.options.paletteFieldModel.getSection().title + ": " + this.options.paletteFieldModel.getLabel();
if (this.options.fieldSchema.civiIsPhone) {
result = result + '-' + CRM.PseudoConstant.phoneType[this.model.get('phone_type_id')];
}
if (this.options.fieldSchema.civiIsWebsite) {
result = result + '-' + CRM.PseudoConstant.websiteType[this.model.get('website_type_id')];
}
if (this.options.fieldSchema.civiIsLocation) {
var locType = this.model.get('location_type_id') ? CRM.PseudoConstant.locationType[this.model.get('location_type_id')] : ts('Primary');
result = result + ' (' + locType + ')';
}
return result;
},
/**
* Return a string marking if the field is required
* @return {String}
*/
getRequiredMarker: function() {
if (this.model.get('is_required') == 1) {
return ' <span class="crm-marker">*</span> ';
}
return '';
},
onRender: function() {
this.$el.toggleClass('disabled', this.model.get('is_active') != 1);
if (this.model.get("is_reserved") == 1) {
this.$('.crm-designer-buttons').hide();
}
}
});
/**
* options:
* - model: CRM.UF.UFFieldModel
* - fieldSchema: (Backbone.Form schema element)
*/
CRM.Designer.UFFieldDetailView = Backbone.View.extend({
initialize: function() {
// FIXME: hide/display 'in_selector' if 'visibility' is one of the public options
var fields = ['location_type_id', 'website_type_id', 'phone_type_id', 'label', 'is_multi_summary', 'is_required', 'is_view', 'visibility', 'in_selector', 'is_searchable', 'help_pre', 'help_post', 'is_active'];
if (! this.options.fieldSchema.civiIsLocation) {
fields = _.without(fields, 'location_type_id');
}
if (! this.options.fieldSchema.civiIsWebsite) {
fields = _.without(fields, 'website_type_id');
}
if (! this.options.fieldSchema.civiIsPhone) {
fields = _.without(fields, 'phone_type_id');
}
if (!this.options.fieldSchema.civiIsMultiple) {
fields = _.without(fields, 'is_multi_summary');
}
if (this.options.fieldSchema.type == 'Markup') {
fields = _.without(fields, 'is_required', 'is_view', 'visibility', 'in_selector', 'is_searchable', 'help_post');
}
this.form = new Backbone.Form({
model: this.model,
fields: fields
});
this.form.on('change', this.onFormChange, this);
this.model.on('change', this.onModelChange, this);
},
render: function() {
this.$el.html(this.form.render().el);
this.onFormChange();
},
onModelChange: function() {
$.each(this.form.fields, function(i, field) {
this.form.setValue(field.key, this.model.get(field.key));
});
},
onFormChange: function() {
this.form.commit();
this.$('.field-is_multi_summary').toggle(this.options.fieldSchema.civiIsMultiple ? true : false);
this.$('.field-in_selector').toggle(this.model.isInSelectorAllowed());
if (!this.model.isInSelectorAllowed() && this.model.get('in_selector') != "0") {
this.model.set('in_selector', "0");
if (this.form.fields.in_selector) {
this.form.setValue('in_selector', "0");
}
// TODO: It might be nicer if we didn't completely discard in_selector -- e.g.
// if the value could be restored when the user isInSelectorAllowed becomes true
// again. However, I haven't found a simple way to do this.
}
}
});
/**
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.UFGroupView = Backbone.Marionette.Layout.extend({
serializeData: extendedSerializeData,
template: '#form_row_template',
expanded: false,
regions: {
summary: '.crm-designer-form-summary',
detail: '.crm-designer-form-detail'
},
events: {
"click .crm-designer-action-settings": 'doToggleForm'
},
onRender: function() {
this.summary.show(new CRM.Designer.UFGroupSummaryView({
model: this.model
}));
this.detail.show(new CRM.Designer.UFGroupDetailView({
model: this.model
}));
if (!this.expanded) {
this.detail.$el.hide();
}
var that = this;
CRM.designerApp.vent.on('formOpened', function(event) {
if (that.expanded && event !== 0) {
that.doToggleForm(false);
}
});
},
doToggleForm: function(event) {
this.expanded = !this.expanded;
if (this.expanded && event !== false) {
CRM.designerApp.vent.trigger('formOpened', 0);
}
this.$el.toggleClass('crm-designer-open', this.expanded);
this.detail.$el.toggle('blind', 250);
}
});
/**
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.UFGroupSummaryView = Backbone.Marionette.ItemView.extend({
serializeData: extendedSerializeData,
template: '#form_summary_template',
modelEvents: {
'change': 'render'
},
onRender: function() {
this.$el.toggleClass('disabled', this.model.get('is_active') != 1);
if (this.model.get("is_reserved") == 1) {
this.$('.crm-designer-buttons').hide();
}
}
});
/**
* options:
* - model: CRM.UF.UFGroupModel
*/
CRM.Designer.UFGroupDetailView = Backbone.View.extend({
initialize: function() {
this.form = new Backbone.Form({
model: this.model,
fields: ['title', 'help_pre', 'help_post', 'is_active']
});
this.form.on('change', this.form.commit, this.form);
},
render: function() {
this.$el.html(this.form.render().el);
}
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,188 @@
(function($, _) {
if (!CRM.ProfileSelector) CRM.ProfileSelector = {};
CRM.ProfileSelector.Option = Backbone.Marionette.ItemView.extend({
template: '#profile_selector_option_template',
tagName: 'option',
modelEvents: {
'change:title': 'render'
},
onRender: function() {
this.$el.attr('value', this.model.get('id'));
}
});
CRM.ProfileSelector.Select = Backbone.Marionette.CollectionView.extend({
tagName: 'select',
itemView: CRM.ProfileSelector.Option
});
/**
* Render a pane with 'Select/Preview/Edit/Copy/Create' functionality for profiles.
*
* Note: This view works with a ufGroupCollection, and it creates popups for a
* ufGroupModel. These are related but not facilely. The ufGroupModels in the
* ufGroupCollection are never passed to the popup, and the models from the
* popup are never added to the collection. This is because the popup works
* with temporary, local copies -- but the collection reflects the actual list
* on the server.
*
* options:
* - ufGroupId: int, the default selection
* - ufGroupCollection: the profiles which can be selected
* - ufEntities: hard-coded entity list used with any new/existing forms
* (this may be removed when the form-runtime is updated to support hand-picking
* entities for each form)
*/
CRM.ProfileSelector.View = Backbone.Marionette.Layout.extend({
template: '#profile_selector_template',
regions: {
selectRegion: '.crm-profile-selector-select'
},
events: {
'change .crm-profile-selector-select select': 'onChangeUfGroupId',
'click .crm-profile-selector-edit': 'doEdit',
'click .crm-profile-selector-copy': 'doCopy',
'click .crm-profile-selector-create': 'doCreate',
'click .crm-profile-selector-preview': 'doShowPreview',
// prevent interaction with preview form
'click .crm-profile-selector-preview-pane': false,
'crmLoad .crm-profile-selector-preview-pane': 'disableForm'
},
/** @var Marionette.View which specifically builds on jQuery-UI's dialog */
activeDialog: null,
onRender: function() {
var view = new CRM.ProfileSelector.Select({
collection: this.options.ufGroupCollection
});
this.selectRegion.show(view);
this.setUfGroupId(this.options.ufGroupId, {silent: true});
this.toggleButtons();
this.$('.crm-profile-selector-select select').css('width', '25em').crmSelect2();
this.doShowPreview();
},
onChangeUfGroupId: function(event) {
this.options.ufGroupId = $(event.target).val();
this.trigger('change:ufGroupId', this);
this.toggleButtons();
this.doPreview();
},
toggleButtons: function() {
this.$('.crm-profile-selector-edit,.crm-profile-selector-copy').prop('disabled', !this.hasUfGroupId());
},
hasUfGroupId: function() {
return (this.getUfGroupId() && this.getUfGroupId() !== '') ? true : false;
},
setUfGroupId: function(value, options) {
this.options.ufGroupId = value;
this.$('.crm-profile-selector-select select').val(value);
this.$('.crm-profile-selector-select select').select2('val', value, (!options || !options.silent));
},
getUfGroupId: function() {
return this.options.ufGroupId;
},
doPreview: function() {
var $pane = this.$('.crm-profile-selector-preview-pane');
if (!this.hasUfGroupId()) {
$pane.html($('#profile_selector_empty_preview_template').html());
} else {
CRM.loadPage(CRM.url("civicrm/ajax/inline", {class_name: 'CRM_UF_Form_Inline_PreviewById', id: this.getUfGroupId()}), {target: $pane});
}
},
doShowPreview: function() {
var $preview = this.$('.crm-profile-selector-preview');
var $pane = this.$('.crm-profile-selector-preview-pane');
if ($preview.hasClass('crm-profile-selector-preview-show')) {
$preview.removeClass('crm-profile-selector-preview-show');
$preview.find('.crm-i').removeClass('fa-television').addClass('fa-times');
$pane.show();
} else {
$preview.addClass('crm-profile-selector-preview-show');
$preview.find('.crm-i').removeClass('fa-times').addClass('fa-television');
$pane.hide();
}
},
disableForm: function() {
this.$(':input', '.crm-profile-selector-preview-pane').not('.select2-input').prop('readOnly', true);
},
doEdit: function(e) {
e.preventDefault();
var profileSelectorView = this;
var designerDialog = new CRM.Designer.DesignerDialog({
findCreateUfGroupModel: function(options) {
var ufId = profileSelectorView.getUfGroupId();
// Retrieve UF group and fields from the api
CRM.api('UFGroup', 'getsingle', {id: ufId, "api.UFField.get": 1}, {
success: function(formData) {
// Note: With chaining, API returns some extraneous keys that aren't part of UFGroupModel
var ufGroupModel = new CRM.UF.UFGroupModel(_.pick(formData, _.keys(CRM.UF.UFGroupModel.prototype.schema)));
ufGroupModel.setUFGroupModel(ufGroupModel.calculateContactEntityType(), profileSelectorView.options.ufEntities);
ufGroupModel.getRel('ufFieldCollection').reset(_.values(formData["api.UFField.get"].values));
options.onLoad(ufGroupModel);
}
});
}
});
CRM.designerApp.vent.on('ufSaved', this.onSave, this);
this.setDialog(designerDialog);
},
doCopy: function(e) {
e.preventDefault();
// This is largely the same as doEdit, but we ultimately pass in a deepCopy of the ufGroupModel.
var profileSelectorView = this;
var designerDialog = new CRM.Designer.DesignerDialog({
findCreateUfGroupModel: function(options) {
var ufId = profileSelectorView.getUfGroupId();
// Retrieve UF group and fields from the api
CRM.api('UFGroup', 'getsingle', {id: ufId, "api.UFField.get": 1}, {
success: function(formData) {
// Note: With chaining, API returns some extraneous keys that aren't part of UFGroupModel
var ufGroupModel = new CRM.UF.UFGroupModel(_.pick(formData, _.keys(CRM.UF.UFGroupModel.prototype.schema)));
ufGroupModel.setUFGroupModel(ufGroupModel.calculateContactEntityType(), profileSelectorView.options.ufEntities);
ufGroupModel.getRel('ufFieldCollection').reset(_.values(formData["api.UFField.get"].values));
options.onLoad(ufGroupModel.deepCopy());
}
});
}
});
CRM.designerApp.vent.on('ufSaved', this.onSave, this);
this.setDialog(designerDialog);
},
doCreate: function(e) {
e.preventDefault();
var profileSelectorView = this;
var designerDialog = new CRM.Designer.DesignerDialog({
findCreateUfGroupModel: function(options) {
// Initialize new UF group
var ufGroupModel = new CRM.UF.UFGroupModel();
ufGroupModel.getRel('ufEntityCollection').reset(profileSelectorView.options.ufEntities);
options.onLoad(ufGroupModel);
}
});
CRM.designerApp.vent.on('ufSaved', this.onSave, this);
this.setDialog(designerDialog);
},
onSave: function() {
CRM.designerApp.vent.off('ufSaved', this.onSave, this);
var ufGroupId = this.activeDialog.model.get('id');
var modelFromCollection = this.options.ufGroupCollection.get(ufGroupId);
if (modelFromCollection) {
// copy in changes to UFGroup
modelFromCollection.set(this.activeDialog.model.toStrictJSON());
} else {
// add in new UFGroup
modelFromCollection = new CRM.UF.UFGroupModel(this.activeDialog.model.toStrictJSON());
this.options.ufGroupCollection.add(modelFromCollection);
}
this.setUfGroupId(ufGroupId);
this.doPreview();
},
setDialog: function(view) {
if (this.activeDialog) {
this.activeDialog.close();
}
this.activeDialog = view;
view.render();
}
});
})(CRM.$, CRM._);

View file

@ -0,0 +1,114 @@
// https://civicrm.org/licensing
(function($, _) {
'use strict';
/* jshint validthis: true */
var configRowTpl = _.template($('#config-row-tpl').html()),
options;
// Weird conflict with drupal styles
$('body').removeClass('toolbar');
function format(item) {
var icon = '<span class="ui-icon ui-icon-gear"></span>';
if (item.icon) {
icon = '<img src="' + CRM.config.resourceBase + item.icon + '" />';
}
return icon + '&nbsp;' + item.text;
}
function initOptions(data) {
options = _.filter(data, function(n) {
return $.inArray(n.id, CRM.vars.ckConfig.blacklist) < 0;
});
addOption();
$.each(CRM.vars.ckConfig.settings, function(key, val) {
if ($.inArray(key, CRM.vars.ckConfig.blacklist) < 0) {
var $opt = $('.crm-config-option-row:last input.crm-config-option-name');
$opt.val(key).change();
$opt.siblings('span').find(':input').val(val);
}
});
}
function changeOptionName() {
var $el = $(this),
name = $el.val();
$el.next('span').remove();
if (name) {
if (($('input.crm-config-option-name').filter(function() {return !this.value;})).length < 1) {
addOption();
}
var type = $el.select2('data').type;
if (type === 'Boolean') {
$el.after('<span>&nbsp; = &nbsp;<select class="crm-form-select" name="config_' + name + '"><option value="false">false</option><option value="true">true</option></select></span>');
}
else {
$el.after('<span>&nbsp; = &nbsp;<input class="crm-form-text ' + (type==='Number' ? 'eight" type="number"' : 'huge" type="text"') + ' name="config_' + name + '"/></span>');
}
} else {
$el.closest('div').remove();
}
}
function addOption() {
$('#crm-custom-config-options').append($(configRowTpl({})));
$('div:last input.crm-config-option-name', '#crm-custom-config-options').crmSelect2({
data: {results: options, text: 'id'},
formatSelection: function(field) {
return '<strong>' + field.id + '</strong> (' + field.type + ')';
},
formatResult: function(field) {
return '<strong>' + field.id + '</strong> (' + field.type + ')' +
'<div class="api-field-desc">' + field.description + '</div>';
}
});
}
$('#extraPlugins').crmSelect2({
multiple: true,
closeOnSelect: false,
data: CRM.vars.ckConfig.plugins,
escapeMarkup: _.identity,
formatResult: format,
formatSelection: format
});
var toolbarModifier = new ToolbarConfigurator.ToolbarModifier( 'editor-basic' );
toolbarModifier.init(_.noop);
CKEDITOR.document.getById( 'toolbarModifierWrapper' ).append( toolbarModifier.mainContainer );
$(function() {
var selectorOpen = false,
changedWhileOpen = false;
$('#toolbarModifierForm')
.on('submit', function(e) {
$('.toolbar button:last', '#toolbarModifierWrapper')[0].click();
$('.configContainer textarea', '#toolbarModifierWrapper').attr('name', 'config');
})
.on('change', '.config-param', function(e) {
changedWhileOpen = true;
if (!selectorOpen) {
$('#toolbarModifierForm').submit().block();
}
})
.on('change', 'input.crm-config-option-name', changeOptionName)
// Debounce the change event so it only fires after the multiselect is closed
.on('select2-open', 'input.config-param', function(e) {
selectorOpen = true;
changedWhileOpen = false;
})
.on('select2-close', 'input.config-param', function(e) {
selectorOpen = false;
if (changedWhileOpen) {
$(this).change();
}
});
$.getJSON(CRM.config.resourceBase + 'js/wysiwyg/ck-options.json', null, initOptions);
});
})(CRM.$, CRM._);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,146 @@
// https://civicrm.org/licensing
(function($, _) {
function getInstance(item) {
var name = $(item).attr("name"),
id = $(item).attr("id");
if (name && window.CKEDITOR && CKEDITOR.instances[name]) {
return CKEDITOR.instances[name];
}
if (id && window.CKEDITOR && CKEDITOR.instances[id]) {
return CKEDITOR.instances[id];
}
}
CRM.wysiwyg.supportsFileUploads = true;
CRM.wysiwyg._create = function(item) {
var deferred = $.Deferred();
function onReady() {
var debounce,
editor = this;
editor.on('focus', function() {
$(item).trigger('focus');
});
editor.on('blur', function() {
editor.updateElement();
$(item).trigger("blur");
$(item).trigger("change");
});
editor.on('insertText', function() {
$(item).trigger("keypress");
});
_.each(['key', 'pasteState'], function(evName) {
editor.on(evName, function(evt) {
if (debounce) clearTimeout(debounce);
debounce = setTimeout(function() {
editor.updateElement();
$(item).trigger("change");
}, 50);
});
});
editor.on('pasteState', function() {
$(item).trigger("paste");
});
// Hide CiviCRM menubar when editor is fullscreen
editor.on('maximize', function (e) {
$('#civicrm-menu').toggle(e.data === 2);
});
deferred.resolve();
}
function initialize() {
var
browseUrl = CRM.config.resourceBase + "packages/kcfinder/browse.php?cms=civicrm",
uploadUrl = CRM.config.resourceBase + "packages/kcfinder/upload.php?cms=civicrm",
preset = $(item).data('preset') || 'default',
// This variable is always an array but a legacy extension could be setting it as a string.
customConfig = (typeof CRM.config.CKEditorCustomConfig === 'string') ? CRM.config.CKEditorCustomConfig :
(CRM.config.CKEditorCustomConfig[preset] || CRM.config.CKEditorCustomConfig.default);
$(item).addClass('crm-wysiwyg-enabled');
CKEDITOR.replace($(item)[0], {
filebrowserBrowseUrl: browseUrl + '&type=files',
filebrowserImageBrowseUrl: browseUrl + '&type=images',
filebrowserFlashBrowseUrl: browseUrl + '&type=flash',
filebrowserUploadUrl: uploadUrl + '&type=files',
filebrowserImageUploadUrl: uploadUrl + '&type=images',
filebrowserFlashUploadUrl: uploadUrl + '&type=flash',
customConfig: customConfig,
on: {
instanceReady: onReady
}
});
}
if ($(item).hasClass('crm-wysiwyg-enabled')) {
deferred.resolve();
}
else if ($(item).length) {
// Lazy-load ckeditor.js
if (window.CKEDITOR) {
initialize();
} else {
CRM.loadScript(CRM.config.resourceBase + 'bower_components/ckeditor/ckeditor.js').done(initialize);
}
} else {
deferred.reject();
}
return deferred;
};
CRM.wysiwyg.destroy = function(item) {
$(item).removeClass('crm-wysiwyg-enabled');
var editor = getInstance(item);
if (editor) {
editor.destroy();
}
};
CRM.wysiwyg.updateElement = function(item) {
var editor = getInstance(item);
if (editor) {
editor.updateElement();
}
};
CRM.wysiwyg.getVal = function(item) {
var editor = getInstance(item);
if (editor) {
return editor.getData();
} else {
return $(item).val();
}
};
CRM.wysiwyg.setVal = function(item, val) {
var editor = getInstance(item);
if (editor) {
return editor.setData(val);
} else {
return $(item).val(val);
}
};
CRM.wysiwyg.insert = function(item, text) {
var editor = getInstance(item);
if (editor) {
editor.insertText(text);
} else {
CRM.wysiwyg._insertIntoTextarea(item, text);
}
};
CRM.wysiwyg.focus = function(item) {
var editor = getInstance(item);
if (editor) {
editor.focus();
} else {
$(item).focus();
}
};
})(CRM.$, CRM._);

View file

@ -0,0 +1,69 @@
// https://civicrm.org/licensing
(function($, _) {
// This defines an interface which by default only handles plain textareas
// A wysiwyg implementation can extend this by overriding as many of these functions as needed
CRM.wysiwyg = {
supportsFileUploads: !!CRM.config.wysisygScriptLocation,
create: function(item) {
var ret = $.Deferred();
// Lazy-load the wysiwyg js
if (CRM.config.wysisygScriptLocation) {
CRM.loadScript(CRM.config.wysisygScriptLocation).done(function() {
CRM.wysiwyg._create(item).done(function() {
ret.resolve();
});
});
} else {
ret.resolve();
}
return ret;
},
destroy: _.noop,
updateElement: _.noop,
getVal: function(item) {
return $(item).val();
},
setVal: function(item, val) {
return $(item).val(val);
},
insert: function(item, text) {
CRM.wysiwyg._insertIntoTextarea(item, text);
},
focus: function(item) {
$(item).focus();
},
// Fallback function to use when a wysiwyg has not been initialized
_insertIntoTextarea: function(item, text) {
var itemObj = $(item);
var origVal = itemObj.val();
var origStart = itemObj[0].selectionStart;
var origEnd = itemObj[0].selectionEnd;
var newVal = origVal.substring(0, origStart) + text + origVal.substring(origEnd);
itemObj.val(newVal);
var newPos = (origStart + text.length);
itemObj[0].selectionStart = newPos;
itemObj[0].selectionEnd = newPos;
itemObj.triggerHandler('change');
CRM.wysiwyg.focus(item);
},
// Create a "collapsed" textarea that expands into a wysiwyg when clicked
createCollapsed: function(item) {
$(item)
.hide()
.on('blur', function () {
CRM.wysiwyg.destroy(item);
$(item).hide().next('.replace-plain').show().html($(item).val());
})
.after('<div class="replace-plain" tabindex="0"></div>');
$(item).next('.replace-plain')
.attr('title', ts('Click to edit'))
.html($(item).val())
.on('click keypress', function (e) {
// Stop browser from opening clicked links
e.preventDefault();
$(item).show().next('.replace-plain').hide();
CRM.wysiwyg.create(item);
});
}
};
})(CRM.$, CRM._);