mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 18:19:22 +03:00
Add alias support (domains)
This commit is contained in:
parent
a1d829c697
commit
28a125426a
26 changed files with 1014 additions and 92 deletions
|
|
@ -39,10 +39,11 @@ class AdminApp extends AbstractApp
|
||||||
DomainStore.domains.loading(false);
|
DomainStore.domains.loading(false);
|
||||||
if (StorageResultType.Success === result && data && data.Result)
|
if (StorageResultType.Success === result && data && data.Result)
|
||||||
{
|
{
|
||||||
DomainStore.domains(_.map(data.Result, (enabled, name) => {
|
DomainStore.domains(_.map(data.Result, ([enabled, alias], name) => {
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
disabled: ko.observable(!enabled),
|
disabled: ko.observable(!enabled),
|
||||||
|
alias: alias,
|
||||||
deleteAccess: ko.observable(false)
|
deleteAccess: ko.observable(false)
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
var
|
var
|
||||||
_ = require('_'),
|
_ = require('_'),
|
||||||
|
|
||||||
|
Utils = require('Common/Utils'),
|
||||||
|
|
||||||
AbstractAjaxRemote = require('Remote/AbstractAjax')
|
AbstractAjaxRemote = require('Remote/AbstractAjax')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -54,10 +56,14 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {?Function} fCallback
|
* @param {?Function} fCallback
|
||||||
|
* @param {boolean=} bIncludeAliases = true
|
||||||
*/
|
*/
|
||||||
RemoteAdminStorage.prototype.domainList = function (fCallback)
|
RemoteAdminStorage.prototype.domainList = function (fCallback, bIncludeAliases)
|
||||||
{
|
{
|
||||||
this.defaultRequest(fCallback, 'AdminDomainList');
|
bIncludeAliases = Utils.isUnd(bIncludeAliases) ? true : bIncludeAliases;
|
||||||
|
this.defaultRequest(fCallback, 'AdminDomainList', {
|
||||||
|
'IncludeAliases': bIncludeAliases ? '1' : '0'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -208,6 +214,14 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
RemoteAdminStorage.prototype.createDomainAlias = function (fCallback, sName, sAlias)
|
||||||
|
{
|
||||||
|
this.defaultRequest(fCallback, 'AdminDomainAliasSave', {
|
||||||
|
'Name': sName,
|
||||||
|
'Alias': sAlias
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
RemoteAdminStorage.prototype.createOrUpdateDomain = function (fCallback,
|
RemoteAdminStorage.prototype.createOrUpdateDomain = function (fCallback,
|
||||||
bCreate, sName,
|
bCreate, sName,
|
||||||
sIncHost, iIncPort, sIncSecure, bIncShortLogin,
|
sIncHost, iIncPort, sIncSecure, bIncShortLogin,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@
|
||||||
require('Knoin/Knoin').showScreenPopup(require('View/Popup/Domain'));
|
require('Knoin/Knoin').showScreenPopup(require('View/Popup/Domain'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DomainsAdminSettings.prototype.createDomainAlias = function ()
|
||||||
|
{
|
||||||
|
require('Knoin/Knoin').showScreenPopup(require('View/Popup/DomainAlias'));
|
||||||
|
};
|
||||||
|
|
||||||
DomainsAdminSettings.prototype.deleteDomain = function (oDomain)
|
DomainsAdminSettings.prototype.deleteDomain = function (oDomain)
|
||||||
{
|
{
|
||||||
this.domains.remove(oDomain);
|
this.domains.remove(oDomain);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,10 @@
|
||||||
{
|
{
|
||||||
this.domains = ko.observableArray([]);
|
this.domains = ko.observableArray([]);
|
||||||
this.domains.loading = ko.observable(false).extend({'throttle': 100});
|
this.domains.loading = ko.observable(false).extend({'throttle': 100});
|
||||||
|
|
||||||
|
this.domainsWithoutAliases = this.domains.filter(function (oItem) {
|
||||||
|
return oItem && !oItem.alias;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new DomainAdminStore();
|
module.exports = new DomainAdminStore();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
|
.b-domain-alias-content {
|
||||||
|
&.modal {
|
||||||
|
width: 330px;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.b-domain-content {
|
.b-domain-content {
|
||||||
|
|
||||||
&.modal {
|
&.modal {
|
||||||
|
|
@ -40,7 +53,6 @@
|
||||||
|
|
||||||
.error-desc {
|
.error-desc {
|
||||||
color: red;
|
color: red;
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.testing-done {
|
.testing-done {
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,17 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled .domain-name {
|
.domain-alias {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
color: #bbb;
|
color: #bbb;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
.domain-name, .domain-alias {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-delete {
|
.button-delete {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@
|
||||||
this.smtpAuth = ko.observable(true);
|
this.smtpAuth = ko.observable(true);
|
||||||
this.smtpPhpMail = ko.observable(false);
|
this.smtpPhpMail = ko.observable(false);
|
||||||
this.whiteList = ko.observable('');
|
this.whiteList = ko.observable('');
|
||||||
|
this.aliasName = ko.observable('');
|
||||||
|
|
||||||
this.enableSmartPorts = ko.observable(false);
|
this.enableSmartPorts = ko.observable(false);
|
||||||
|
|
||||||
|
|
@ -97,10 +98,30 @@
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.headerText = ko.computed(function () {
|
this.headerText = ko.computed(function () {
|
||||||
var sName = this.name();
|
|
||||||
return this.edit() ? Translator.i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', {'NAME': sName}) :
|
var
|
||||||
('' === sName ? Translator.i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN') :
|
sName = this.name(),
|
||||||
Translator.i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN_WITH_NAME', {'NAME': sName}));
|
sAliasName = this.aliasName(),
|
||||||
|
sResult = ''
|
||||||
|
;
|
||||||
|
|
||||||
|
if (this.edit())
|
||||||
|
{
|
||||||
|
sResult = Translator.i18n('POPUPS_DOMAIN/TITLE_EDIT_DOMAIN', {'NAME': sName});
|
||||||
|
if (sAliasName)
|
||||||
|
{
|
||||||
|
sResult += ' ← ' + sAliasName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sResult = ('' === sName ? Translator.i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN') :
|
||||||
|
Translator.i18n('POPUPS_DOMAIN/TITLE_ADD_DOMAIN_WITH_NAME', {'NAME': sName}))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sResult;
|
||||||
|
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.domainDesc = ko.computed(function () {
|
this.domainDesc = ko.computed(function () {
|
||||||
|
|
@ -418,6 +439,7 @@
|
||||||
this.smtpAuth(!!oDomain.OutAuth);
|
this.smtpAuth(!!oDomain.OutAuth);
|
||||||
this.smtpPhpMail(!!oDomain.OutUsePhpMail);
|
this.smtpPhpMail(!!oDomain.OutUsePhpMail);
|
||||||
this.whiteList(Utils.trim(oDomain.WhiteList));
|
this.whiteList(Utils.trim(oDomain.WhiteList));
|
||||||
|
this.aliasName(Utils.trim(oDomain.AliasName));
|
||||||
|
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
}
|
}
|
||||||
|
|
@ -464,6 +486,7 @@
|
||||||
this.smtpPhpMail(false);
|
this.smtpPhpMail(false);
|
||||||
|
|
||||||
this.whiteList('');
|
this.whiteList('');
|
||||||
|
this.aliasName('');
|
||||||
this.enableSmartPorts(true);
|
this.enableSmartPorts(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
117
dev/View/Popup/DomainAlias.js
Normal file
117
dev/View/Popup/DomainAlias.js
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var
|
||||||
|
_ = require('_'),
|
||||||
|
ko = require('ko'),
|
||||||
|
|
||||||
|
Enums = require('Common/Enums'),
|
||||||
|
Globals = require('Common/Globals'),
|
||||||
|
Utils = require('Common/Utils'),
|
||||||
|
|
||||||
|
Translator = require('Common/Translator'),
|
||||||
|
|
||||||
|
DomainStore = require('Stores/Admin/Domain'),
|
||||||
|
|
||||||
|
Remote = require('Remote/Admin/Ajax'),
|
||||||
|
|
||||||
|
kn = require('Knoin/Knoin'),
|
||||||
|
AbstractView = require('Knoin/AbstractView')
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @extends AbstractView
|
||||||
|
*/
|
||||||
|
function DomainAliasPopupView()
|
||||||
|
{
|
||||||
|
AbstractView.call(this, 'Popups', 'PopupsDomainAlias');
|
||||||
|
|
||||||
|
this.saving = ko.observable(false);
|
||||||
|
this.savingError = ko.observable('');
|
||||||
|
|
||||||
|
this.name = ko.observable('');
|
||||||
|
this.name.focused = ko.observable(false);
|
||||||
|
|
||||||
|
this.alias = ko.observable('');
|
||||||
|
|
||||||
|
this.domains = DomainStore.domainsWithoutAliases;
|
||||||
|
|
||||||
|
this.domainsOptions = ko.computed(function () {
|
||||||
|
return _.map(this.domains(), function(item) {
|
||||||
|
return {
|
||||||
|
optValue: item.name,
|
||||||
|
optText: item.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.canBeSaved = ko.computed(function () {
|
||||||
|
return !this.saving() && '' !== this.name() && '' !== this.alias();
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.createCommand = Utils.createCommand(this, function () {
|
||||||
|
this.saving(true);
|
||||||
|
Remote.createDomainAlias(
|
||||||
|
_.bind(this.onDomainAliasCreateOrSaveResponse, this),
|
||||||
|
this.name(),
|
||||||
|
this.alias()
|
||||||
|
);
|
||||||
|
}, this.canBeSaved);
|
||||||
|
|
||||||
|
kn.constructorEnd(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
kn.extendAsViewModel(['View/Popup/DomainAlias', 'PopupsDomainAliasViewModel'], DomainAliasPopupView);
|
||||||
|
_.extend(DomainAliasPopupView.prototype, AbstractView.prototype);
|
||||||
|
|
||||||
|
DomainAliasPopupView.prototype.onDomainAliasCreateOrSaveResponse = function (sResult, oData)
|
||||||
|
{
|
||||||
|
this.saving(false);
|
||||||
|
if (Enums.StorageResultType.Success === sResult && oData)
|
||||||
|
{
|
||||||
|
if (oData.Result)
|
||||||
|
{
|
||||||
|
require('App/Admin').default.reloadDomainList();
|
||||||
|
this.closeCommand();
|
||||||
|
}
|
||||||
|
else if (Enums.Notification.DomainAlreadyExists === oData.ErrorCode)
|
||||||
|
{
|
||||||
|
this.savingError(Translator.i18n('ERRORS/DOMAIN_ALREADY_EXISTS'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.savingError(Translator.i18n('ERRORS/UNKNOWN_ERROR'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainAliasPopupView.prototype.onShow = function ()
|
||||||
|
{
|
||||||
|
this.clearForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainAliasPopupView.prototype.onShowWithDelay = function ()
|
||||||
|
{
|
||||||
|
if ('' === this.name() && !Globals.bMobile)
|
||||||
|
{
|
||||||
|
this.name.focused(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainAliasPopupView.prototype.clearForm = function ()
|
||||||
|
{
|
||||||
|
this.saving(false);
|
||||||
|
this.savingError('');
|
||||||
|
|
||||||
|
this.name('');
|
||||||
|
this.name.focused(false);
|
||||||
|
|
||||||
|
this.alias('');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = DomainAliasPopupView;
|
||||||
|
|
||||||
|
}());
|
||||||
|
|
@ -189,7 +189,7 @@ cfg.paths.js = {
|
||||||
'vendors/routes/hasher.min.js',
|
'vendors/routes/hasher.min.js',
|
||||||
'vendors/routes/crossroads.min.js',
|
'vendors/routes/crossroads.min.js',
|
||||||
'vendors/knockout/knockout-3.4.0.js',
|
'vendors/knockout/knockout-3.4.0.js',
|
||||||
'vendors/knockout-projections/knockout-projections-1.0.0.min.js',
|
'vendors/knockout-projections/knockout-projections-1.1.0.min.js',
|
||||||
'vendors/knockout-sortable/knockout-sortable.min.js',
|
'vendors/knockout-sortable/knockout-sortable.min.js',
|
||||||
'vendors/ssm/ssm.min.js',
|
'vendors/ssm/ssm.min.js',
|
||||||
'vendors/jua/jua.min.js',
|
'vendors/jua/jua.min.js',
|
||||||
|
|
|
||||||
|
|
@ -4082,25 +4082,14 @@ NewThemeLink IncludeCss LoadingDescriptionEsc TemplatesLink LangLink IncludeBack
|
||||||
$iOffset = (int) $this->GetActionParam('Offset', 0);
|
$iOffset = (int) $this->GetActionParam('Offset', 0);
|
||||||
$iLimit = (int) $this->GetActionParam('Limit', 20);
|
$iLimit = (int) $this->GetActionParam('Limit', 20);
|
||||||
$sSearch = (string) $this->GetActionParam('Search', '');
|
$sSearch = (string) $this->GetActionParam('Search', '');
|
||||||
|
$bIncludeAliases = '1' === (string) $this->GetActionParam('IncludeAliases', '1');
|
||||||
|
|
||||||
$iOffset = 0;
|
$iOffset = 0;
|
||||||
$sSearch = '';
|
$sSearch = '';
|
||||||
$iLimit = $this->Config()->Get('labs', 'domain_list_limit', 99);
|
$iLimit = $this->Config()->Get('labs', 'domain_list_limit', 99);
|
||||||
|
|
||||||
$sSearch = \trim($sSearch);
|
|
||||||
|
|
||||||
if ($iOffset < 0)
|
|
||||||
{
|
|
||||||
$iOffset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($iLimit < 20)
|
|
||||||
{
|
|
||||||
$iLimit = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->DefaultResponse(__FUNCTION__,
|
return $this->DefaultResponse(__FUNCTION__,
|
||||||
$this->DomainProvider()->GetList($iOffset, $iLimit, $sSearch));
|
$this->DomainProvider()->GetList($iOffset, $iLimit, $sSearch, $bIncludeAliases));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -4140,6 +4129,19 @@ NewThemeLink IncludeCss LoadingDescriptionEsc TemplatesLink LangLink IncludeBack
|
||||||
$oDomain instanceof \RainLoop\Model\Domain ? $this->DomainProvider()->Save($oDomain) : false);
|
$oDomain instanceof \RainLoop\Model\Domain ? $this->DomainProvider()->Save($oDomain) : false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function DoAdminDomainAliasSave()
|
||||||
|
{
|
||||||
|
$this->IsAdminLoggined();
|
||||||
|
|
||||||
|
return $this->DefaultResponse(__FUNCTION__, $this->DomainProvider()->SaveAlias(
|
||||||
|
(string) $this->GetActionParam('Name', ''),
|
||||||
|
(string) $this->GetActionParam('Alias', '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,11 @@ class Domain
|
||||||
*/
|
*/
|
||||||
private $sWhiteList;
|
private $sWhiteList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $sAliasName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sName
|
* @param string $sName
|
||||||
* @param string $sIncHost
|
* @param string $sIncHost
|
||||||
|
|
@ -138,6 +143,7 @@ class Domain
|
||||||
$this->bSieveAllowRaw = false;
|
$this->bSieveAllowRaw = false;
|
||||||
|
|
||||||
$this->sWhiteList = \trim($sWhiteList);
|
$this->sWhiteList = \trim($sWhiteList);
|
||||||
|
$this->sAliasName = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -510,6 +516,25 @@ class Domain
|
||||||
return $this->sWhiteList;
|
return $this->sWhiteList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function AliasName()
|
||||||
|
{
|
||||||
|
return $this->sAliasName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sAliasName
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public function SetAliasName($sAliasName)
|
||||||
|
{
|
||||||
|
$this->sAliasName = $sAliasName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sEmail
|
* @param string $sEmail
|
||||||
* @param string $sLogin = ''
|
* @param string $sLogin = ''
|
||||||
|
|
@ -558,7 +583,8 @@ class Domain
|
||||||
'OutShortLogin' => $this->OutShortLogin(),
|
'OutShortLogin' => $this->OutShortLogin(),
|
||||||
'OutAuth' => $this->OutAuth(),
|
'OutAuth' => $this->OutAuth(),
|
||||||
'OutUsePhpMail' => $this->OutUsePhpMail(),
|
'OutUsePhpMail' => $this->OutUsePhpMail(),
|
||||||
'WhiteList' => $this->WhiteList()
|
'WhiteList' => $this->WhiteList(),
|
||||||
|
'AliasName' => $this->AliasName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,13 @@ class Domain extends \RainLoop\Providers\AbstractProvider
|
||||||
* @param string $sName
|
* @param string $sName
|
||||||
* @param bool $bFindWithWildCard = false
|
* @param bool $bFindWithWildCard = false
|
||||||
* @param bool $bCheckDisabled = true
|
* @param bool $bCheckDisabled = true
|
||||||
|
* @param bool $bCheckAliases = true
|
||||||
*
|
*
|
||||||
* @return \RainLoop\Model\Domain|null
|
* @return \RainLoop\Model\Domain|null
|
||||||
*/
|
*/
|
||||||
public function Load($sName, $bFindWithWildCard = false, $bCheckDisabled = true)
|
public function Load($sName, $bFindWithWildCard = false, $bCheckDisabled = true, $bCheckAliases = true)
|
||||||
{
|
{
|
||||||
$oDomain = $this->oDriver->Load($sName, $bFindWithWildCard, $bCheckDisabled);
|
$oDomain = $this->oDriver->Load($sName, $bFindWithWildCard, $bCheckDisabled, $bCheckAliases);
|
||||||
if ($oDomain instanceof \RainLoop\Model\Domain)
|
if ($oDomain instanceof \RainLoop\Model\Domain)
|
||||||
{
|
{
|
||||||
$this->oPlugins->RunHook('filter.domain', array(&$oDomain));
|
$this->oPlugins->RunHook('filter.domain', array(&$oDomain));
|
||||||
|
|
@ -68,6 +69,22 @@ class Domain extends \RainLoop\Providers\AbstractProvider
|
||||||
return $this->bAdmin ? $this->oDriver->Save($oDomain) : false;
|
return $this->bAdmin ? $this->oDriver->Save($oDomain) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sName
|
||||||
|
* @param string $sAlias
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function SaveAlias($sName, $sAlias)
|
||||||
|
{
|
||||||
|
if ($this->Load($sName, false, false))
|
||||||
|
{
|
||||||
|
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->bAdmin ? $this->oDriver->SaveAlias($sName, $sAlias) : false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sName
|
* @param string $sName
|
||||||
*
|
*
|
||||||
|
|
@ -93,12 +110,25 @@ class Domain extends \RainLoop\Providers\AbstractProvider
|
||||||
* @param int $iOffset = 0
|
* @param int $iOffset = 0
|
||||||
* @param int $iLimit = 20
|
* @param int $iLimit = 20
|
||||||
* @param string $sSearch = ''
|
* @param string $sSearch = ''
|
||||||
|
* @param bool $bIncludeAliases = true
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function GetList($iOffset = 0, $iLimit = 20, $sSearch = '')
|
public function GetList($iOffset = 0, $iLimit = 20, $sSearch = '', $bIncludeAliases = true)
|
||||||
{
|
{
|
||||||
return $this->bAdmin ? $this->oDriver->GetList($iOffset, $iLimit, $sSearch) : array();
|
$sSearch = \trim($sSearch);
|
||||||
|
|
||||||
|
if ($iOffset < 0)
|
||||||
|
{
|
||||||
|
$iOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($iLimit < 20)
|
||||||
|
{
|
||||||
|
$iLimit = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->bAdmin ? $this->oDriver->GetList($iOffset, $iLimit, $sSearch, $bIncludeAliases) : array();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -184,6 +214,79 @@ class Domain extends \RainLoop\Providers\AbstractProvider
|
||||||
return $oDomain;
|
return $oDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \RainLoop\Actions $oActions
|
||||||
|
* @param string $sNameForTest = ''
|
||||||
|
*
|
||||||
|
* @return \RainLoop\Model\Domain | null
|
||||||
|
*/
|
||||||
|
public function CreateNewAliasFromAction(\RainLoop\Actions $oActions, $sNameForTest = '')
|
||||||
|
{
|
||||||
|
$oDomain = null;
|
||||||
|
|
||||||
|
if ($this->bAdmin)
|
||||||
|
{
|
||||||
|
$bCreate = '1' === (string) $oActions->GetActionParam('Create', '0');
|
||||||
|
$sName = (string) $oActions->GetActionParam('Name', '');
|
||||||
|
$sIncHost = (string) $oActions->GetActionParam('IncHost', '');
|
||||||
|
$iIncPort = (int) $oActions->GetActionParam('IncPort', 143);
|
||||||
|
$iIncSecure = (int) $oActions->GetActionParam('IncSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||||
|
$bIncShortLogin = '1' === (string) $oActions->GetActionParam('IncShortLogin', '0');
|
||||||
|
$bUseSieve = '1' === (string) $oActions->GetActionParam('UseSieve', '0');
|
||||||
|
$bSieveAllowRaw = '1' === (string) $oActions->GetActionParam('SieveAllowRaw', '0');
|
||||||
|
$sSieveHost = (string) $oActions->GetActionParam('SieveHost', '');
|
||||||
|
$iSievePort = (int) $oActions->GetActionParam('SievePort', 4190);
|
||||||
|
$iSieveSecure = (int) $oActions->GetActionParam('SieveSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||||
|
$sOutHost = (string) $oActions->GetActionParam('OutHost', '');
|
||||||
|
$iOutPort = (int) $oActions->GetActionParam('OutPort', 25);
|
||||||
|
$iOutSecure = (int) $oActions->GetActionParam('OutSecure', \MailSo\Net\Enumerations\ConnectionSecurityType::NONE);
|
||||||
|
$bOutShortLogin = '1' === (string) $oActions->GetActionParam('OutShortLogin', '0');
|
||||||
|
$bOutAuth = '1' === (string) $oActions->GetActionParam('OutAuth', '1');
|
||||||
|
$bOutUsePhpMail = '1' === (string) $oActions->GetActionParam('OutUsePhpMail', '0');
|
||||||
|
$sWhiteList = (string) $oActions->GetActionParam('WhiteList', '');
|
||||||
|
|
||||||
|
if (0 < \strlen($sName) && 0 < strlen($sNameForTest) && false === \strpos($sName, '*'))
|
||||||
|
{
|
||||||
|
$sNameForTest = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 < strlen($sName) || 0 < strlen($sNameForTest))
|
||||||
|
{
|
||||||
|
$oDomain = 0 < strlen($sNameForTest) ? null : $this->Load($sName);
|
||||||
|
if ($oDomain instanceof \RainLoop\Model\Domain)
|
||||||
|
{
|
||||||
|
if ($bCreate)
|
||||||
|
{
|
||||||
|
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DomainAlreadyExists);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$oDomain->UpdateInstance(
|
||||||
|
$sIncHost, $iIncPort, $iIncSecure, $bIncShortLogin,
|
||||||
|
$bUseSieve, $sSieveHost, $iSievePort, $iSieveSecure,
|
||||||
|
$sOutHost, $iOutPort, $iOutSecure, $bOutShortLogin, $bOutAuth, $bOutUsePhpMail,
|
||||||
|
$sWhiteList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$oDomain = \RainLoop\Model\Domain::NewInstance(0 < strlen($sNameForTest) ? $sNameForTest : $sName,
|
||||||
|
$sIncHost, $iIncPort, $iIncSecure, $bIncShortLogin,
|
||||||
|
$bUseSieve, $sSieveHost, $iSievePort, $iSieveSecure,
|
||||||
|
$sOutHost, $iOutPort, $iOutSecure, $bOutShortLogin, $bOutAuth, $bOutUsePhpMail,
|
||||||
|
$sWhiteList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($oDomain)
|
||||||
|
{
|
||||||
|
$oDomain->SetSieveAllowRaw($bSieveAllowRaw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $oDomain;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -55,44 +55,6 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
return $bBack ? \str_replace('_wildcard_', '*', $sName) : \str_replace('*', '_wildcard_', $sName);
|
return $bBack ? \str_replace('_wildcard_', '*', $sName) : \str_replace('*', '_wildcard_', $sName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $sName
|
|
||||||
* @param bool $bDisable
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function Disable($sName, $bDisable)
|
|
||||||
{
|
|
||||||
$sName = \MailSo\Base\Utils::IdnToAscii($sName, true);
|
|
||||||
|
|
||||||
$sFile = '';
|
|
||||||
if (\file_exists($this->sDomainPath.'/disabled'))
|
|
||||||
{
|
|
||||||
$sFile = @\file_get_contents($this->sDomainPath.'/disabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
$aResult = array();
|
|
||||||
$aNames = \explode(',', $sFile);
|
|
||||||
if ($bDisable)
|
|
||||||
{
|
|
||||||
\array_push($aNames, $sName);
|
|
||||||
$aResult = $aNames;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach ($aNames as $sItem)
|
|
||||||
{
|
|
||||||
if ($sName !== $sItem)
|
|
||||||
{
|
|
||||||
$aResult[] = $sItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$aResult = \array_unique($aResult);
|
|
||||||
return false !== \file_put_contents($this->sDomainPath.'/disabled', \trim(\implode(',', $aResult), ', '));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|
@ -146,10 +108,11 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
* @param string $sName
|
* @param string $sName
|
||||||
* @param bool $bFindWithWildCard = false
|
* @param bool $bFindWithWildCard = false
|
||||||
* @param bool $bCheckDisabled = true
|
* @param bool $bCheckDisabled = true
|
||||||
|
* @param bool $bCheckAliases = true
|
||||||
*
|
*
|
||||||
* @return \RainLoop\Model\Domain|null
|
* @return \RainLoop\Model\Domain|null
|
||||||
*/
|
*/
|
||||||
public function Load($sName, $bFindWithWildCard = false, $bCheckDisabled = true)
|
public function Load($sName, $bFindWithWildCard = false, $bCheckDisabled = true, $bCheckAliases = true)
|
||||||
{
|
{
|
||||||
$mResult = null;
|
$mResult = null;
|
||||||
|
|
||||||
|
|
@ -167,6 +130,17 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
(!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.\MailSo\Base\Utils::IdnToAscii($sName, true).',')))
|
(!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.\MailSo\Base\Utils::IdnToAscii($sName, true).',')))
|
||||||
{
|
{
|
||||||
$aDomain = \RainLoop\Utils::CustomParseIniFile($this->sDomainPath.'/'.$sRealFileName.'.ini');
|
$aDomain = \RainLoop\Utils::CustomParseIniFile($this->sDomainPath.'/'.$sRealFileName.'.ini');
|
||||||
|
// if ($bCheckAliases && !empty($aDomain['alias']))
|
||||||
|
// {
|
||||||
|
// $oDomain = $this->Load($aDomain['alias'], false, false, false);
|
||||||
|
// if ($oDomain && $oDomain instanceof \RainLoop\Model\Domain)
|
||||||
|
// {
|
||||||
|
// $oDomain->SetAliasName($sName);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return $oDomain;
|
||||||
|
// }
|
||||||
|
|
||||||
// fix misspellings (#119)
|
// fix misspellings (#119)
|
||||||
if (\is_array($aDomain))
|
if (\is_array($aDomain))
|
||||||
{
|
{
|
||||||
|
|
@ -194,6 +168,21 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
|
|
||||||
$mResult = \RainLoop\Model\Domain::NewInstanceFromDomainConfigArray($sName, $aDomain);
|
$mResult = \RainLoop\Model\Domain::NewInstanceFromDomainConfigArray($sName, $aDomain);
|
||||||
}
|
}
|
||||||
|
else if ($bCheckAliases && \file_exists($this->sDomainPath.'/'.$sRealFileName.'.alias') &&
|
||||||
|
(!$bCheckDisabled || 0 === \strlen($sDisabled) || false === \strpos(','.$sDisabled.',', ','.\MailSo\Base\Utils::IdnToAscii($sName, true).',')))
|
||||||
|
{
|
||||||
|
$sAlias = \trim(\file_get_contents($this->sDomainPath.'/'.$sRealFileName.'.alias'));
|
||||||
|
if (!empty($sAlias))
|
||||||
|
{
|
||||||
|
$oDomain = $this->Load($sAlias, false, false, false);
|
||||||
|
if ($oDomain && $oDomain instanceof \RainLoop\Model\Domain)
|
||||||
|
{
|
||||||
|
$oDomain->SetAliasName($sName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $oDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if ($bFindWithWildCard)
|
else if ($bFindWithWildCard)
|
||||||
{
|
{
|
||||||
$sNames = $this->getWildcardDomainsLine();
|
$sNames = $this->getWildcardDomainsLine();
|
||||||
|
|
@ -231,6 +220,63 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
return \is_int($mResult) && 0 < $mResult;
|
return \is_int($mResult) && 0 < $mResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sName
|
||||||
|
* @param string $sAlias
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function SaveAlias($sName, $sAlias)
|
||||||
|
{
|
||||||
|
$sRealFileName = $this->codeFileName($sName);
|
||||||
|
|
||||||
|
if ($this->oCacher)
|
||||||
|
{
|
||||||
|
$this->oCacher->Delete($this->wildcardDomainsCacheKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
$mResult = \file_put_contents($this->sDomainPath.'/'.$sRealFileName.'.alias', $sAlias);
|
||||||
|
return \is_int($mResult) && 0 < $mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sName
|
||||||
|
* @param bool $bDisable
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function Disable($sName, $bDisable)
|
||||||
|
{
|
||||||
|
$sName = \MailSo\Base\Utils::IdnToAscii($sName, true);
|
||||||
|
|
||||||
|
$sFile = '';
|
||||||
|
if (\file_exists($this->sDomainPath.'/disabled'))
|
||||||
|
{
|
||||||
|
$sFile = @\file_get_contents($this->sDomainPath.'/disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
$aResult = array();
|
||||||
|
$aNames = \explode(',', $sFile);
|
||||||
|
if ($bDisable)
|
||||||
|
{
|
||||||
|
\array_push($aNames, $sName);
|
||||||
|
$aResult = $aNames;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach ($aNames as $sItem)
|
||||||
|
{
|
||||||
|
if ($sName !== $sItem)
|
||||||
|
{
|
||||||
|
$aResult[] = $sItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$aResult = \array_unique($aResult);
|
||||||
|
return false !== \file_put_contents($this->sDomainPath.'/disabled', \trim(\implode(',', $aResult), ', '));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sName
|
* @param string $sName
|
||||||
*
|
*
|
||||||
|
|
@ -241,10 +287,17 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
$bResult = true;
|
$bResult = true;
|
||||||
$sRealFileName = $this->codeFileName($sName);
|
$sRealFileName = $this->codeFileName($sName);
|
||||||
|
|
||||||
if (0 < \strlen($sName) && \file_exists($this->sDomainPath.'/'.$sRealFileName.'.ini'))
|
if (0 < \strlen($sName))
|
||||||
|
{
|
||||||
|
if (\file_exists($this->sDomainPath.'/'.$sRealFileName.'.ini'))
|
||||||
{
|
{
|
||||||
$bResult = \unlink($this->sDomainPath.'/'.$sRealFileName.'.ini');
|
$bResult = \unlink($this->sDomainPath.'/'.$sRealFileName.'.ini');
|
||||||
}
|
}
|
||||||
|
else if (\file_exists($this->sDomainPath.'/'.$sRealFileName.'.alias'))
|
||||||
|
{
|
||||||
|
$bResult = \unlink($this->sDomainPath.'/'.$sRealFileName.'.alias');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($bResult)
|
if ($bResult)
|
||||||
{
|
{
|
||||||
|
|
@ -260,36 +313,51 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $iOffset
|
* @param int $iOffset = 0
|
||||||
* @param int $iLimit = 20
|
* @param int $iLimit = 20
|
||||||
|
* @param int $sSearch = ''
|
||||||
|
* @param bool $bIncludeAliases = true
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function GetList($iOffset, $iLimit = 20)
|
public function GetList($iOffset = 0, $iLimit = 20, $sSearch = '', $bIncludeAliases = true)
|
||||||
{
|
{
|
||||||
$aResult = array();
|
$aResult = array();
|
||||||
$aWildCards = array();
|
$aWildCards = array();
|
||||||
$aList = \glob($this->sDomainPath.'/*.ini');
|
$aAliases = array();
|
||||||
|
|
||||||
|
$aList = \glob($this->sDomainPath.'/*.{ini,alias}', GLOB_BRACE);
|
||||||
|
|
||||||
foreach ($aList as $sFile)
|
foreach ($aList as $sFile)
|
||||||
{
|
{
|
||||||
$sName = \substr(\basename($sFile), 0, -4);
|
$sName = \basename($sFile);
|
||||||
|
$bAlias = '.alias' === \substr($sName, -6);
|
||||||
|
|
||||||
|
$sName = \preg_replace('/\.(ini|alias)$/', '', $sName);
|
||||||
$sName = $this->codeFileName($sName, true);
|
$sName = $this->codeFileName($sName, true);
|
||||||
|
|
||||||
if (false === \strpos($sName, '*'))
|
if ($bAlias)
|
||||||
{
|
{
|
||||||
$aResult[] = $sName;
|
if ($bIncludeAliases)
|
||||||
|
{
|
||||||
|
$aAliases[] = $sName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (false !== \strpos($sName, '*'))
|
||||||
|
{
|
||||||
|
$aWildCards[] = $sName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$aWildCards[] = $sName;
|
$aResult[] = $sName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
\sort($aResult, SORT_STRING);
|
\sort($aResult, SORT_STRING);
|
||||||
|
\sort($aAliases, SORT_STRING);
|
||||||
\rsort($aWildCards, SORT_STRING);
|
\rsort($aWildCards, SORT_STRING);
|
||||||
|
|
||||||
$aResult = \array_merge($aResult, $aWildCards);
|
$aResult = \array_merge($aResult, $aAliases, $aWildCards);
|
||||||
|
|
||||||
$iOffset = (0 > $iOffset) ? 0 : $iOffset;
|
$iOffset = (0 > $iOffset) ? 0 : $iOffset;
|
||||||
$iLimit = (0 > $iLimit) ? 0 : ((999 < $iLimit) ? 999 : $iLimit);
|
$iLimit = (0 > $iLimit) ? 0 : ((999 < $iLimit) ? 999 : $iLimit);
|
||||||
|
|
@ -314,7 +382,10 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
$aReturn = array();
|
$aReturn = array();
|
||||||
foreach ($aResult as $sName)
|
foreach ($aResult as $sName)
|
||||||
{
|
{
|
||||||
$aReturn[$sName] = !\in_array($sName, $aDisabledNames);
|
$aReturn[$sName] = array(
|
||||||
|
!\in_array($sName, $aDisabledNames),
|
||||||
|
\in_array($sName, $aAliases)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $aReturn;
|
return $aReturn;
|
||||||
|
|
@ -322,11 +393,12 @@ class DefaultDomain implements \RainLoop\Providers\Domain\DomainAdminInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $sSearch = ''
|
* @param string $sSearch = ''
|
||||||
|
* @param bool $bIncludeAliases = true
|
||||||
*
|
*
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function Count()
|
public function Count($sSearch = '', $bIncludeAliases = true)
|
||||||
{
|
{
|
||||||
return \count($this->GetList(0, 999));
|
return \count($this->GetList(0, 999, $sSearch, $bIncludeAliases));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,15 +37,20 @@ interface DomainAdminInterface extends DomainInterface
|
||||||
public function Delete($sName);
|
public function Delete($sName);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $iOffset
|
* @param int $iOffset = 0
|
||||||
* @param int $iLimit = 20
|
* @param int $iLimit = 20
|
||||||
|
* @param int $sSearch = ''
|
||||||
|
* @param bool $bIncludeAliases = true
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function GetList($iOffset, $iLimit = 20);
|
public function GetList($iOffset = 0, $iLimit = 20, $sSearch = '', $bIncludeAliases = true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param string $sSearch = ''
|
||||||
|
* @param bool $bIncludeAliases = true
|
||||||
|
*
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
public function Count();
|
public function Count($sSearch = '', $bIncludeAliases = true);
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +89,7 @@ en:
|
||||||
TAB_DOMAINS:
|
TAB_DOMAINS:
|
||||||
LEGEND_DOMAINS: "Domains"
|
LEGEND_DOMAINS: "Domains"
|
||||||
BUTTON_ADD_DOMAIN: "Add Domain"
|
BUTTON_ADD_DOMAIN: "Add Domain"
|
||||||
|
BUTTON_ADD_ALIAS: "Add Alias"
|
||||||
DELETE_ARE_YOU_SURE: "Are you sure?"
|
DELETE_ARE_YOU_SURE: "Are you sure?"
|
||||||
HTML_DOMAINS_HELPER: |
|
HTML_DOMAINS_HELPER: |
|
||||||
List of domains webmail is allowed to access.
|
List of domains webmail is allowed to access.
|
||||||
|
|
@ -186,6 +187,12 @@ en:
|
||||||
Note that subscription key can be activated for a single domain only.
|
Note that subscription key can be activated for a single domain only.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Once started, the process of activation cannot be aborted or cancelled.
|
Once started, the process of activation cannot be aborted or cancelled.
|
||||||
|
POPUPS_DOMAIN_ALIAS:
|
||||||
|
TITLE_ADD_DOMAIN_ALIAS: "Add Alias"
|
||||||
|
LABEL_ALIAS: "Alias"
|
||||||
|
LABEL_DOMAIN: "Domain"
|
||||||
|
BUTTON_CLOSE: "Close"
|
||||||
|
BUTTON_ADD: "Add"
|
||||||
POPUPS_DOMAIN:
|
POPUPS_DOMAIN:
|
||||||
TITLE_ADD_DOMAIN: "Add Domain"
|
TITLE_ADD_DOMAIN: "Add Domain"
|
||||||
TITLE_ADD_DOMAIN_WITH_NAME: "Add Domain \"%NAME%\""
|
TITLE_ADD_DOMAIN_WITH_NAME: "Add Domain \"%NAME%\""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<tr class="e-item" data-bind="css: {'disabled': disabled }">
|
<tr class="e-item" data-bind="css: {'disabled': disabled }">
|
||||||
<td class="e-action">
|
<td class="e-action">
|
||||||
<span class="domain-name" data-bind="text: name"></span>
|
<span class="domain-name" data-bind="text: name"></span>
|
||||||
|
<span class="domain-alias" data-bind="if: alias">(alias)</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a class="btn btn-small btn-small-small btn-danger pull-right button-delete button-delete-transitions" data-bind="css: {'delete-access': deleteAccess()}, click: function(oDomain) { $root.deleteDomain(oDomain); }">
|
<a class="btn btn-small btn-small-small btn-danger pull-right button-delete button-delete-transitions" data-bind="css: {'delete-access': deleteAccess()}, click: function(oDomain) { $root.deleteDomain(oDomain); }">
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,12 @@
|
||||||
|
|
||||||
<span data-i18n="TAB_DOMAINS/BUTTON_ADD_DOMAIN"></span>
|
<span data-i18n="TAB_DOMAINS/BUTTON_ADD_DOMAIN"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a class="btn" data-bind="click: createDomainAlias">
|
||||||
|
<i class="icon-plus"></i>
|
||||||
|
|
||||||
|
<span data-i18n="TAB_DOMAINS/BUTTON_ADD_ALIAS"></span>
|
||||||
|
</a>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<blockquote>
|
<blockquote>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<div class="popups">
|
||||||
|
<div class="modal b-domain-alias-content" data-bind="modal: modalVisibility">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-bind="command: cancelCommand">×</button>
|
||||||
|
<h3 data-i18n="POPUPS_DOMAIN_ALIAS/TITLE_ADD_DOMAIN_ALIAS"></h3>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="control-group" data-bind="css: {error: '' !== savingError()}">
|
||||||
|
<label class="control-label" data-i18n="POPUPS_DOMAIN_ALIAS/LABEL_ALIAS"></label>
|
||||||
|
<div class="controls">
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Input',
|
||||||
|
params: {
|
||||||
|
value: name,
|
||||||
|
size: 4
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
<div data-bind="visible: '' !== savingError()">
|
||||||
|
<span class="error-desc" data-bind="text: savingError"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="control-label" data-i18n="POPUPS_DOMAIN_ALIAS/LABEL_DOMAIN"></label>
|
||||||
|
<div class="controls">
|
||||||
|
<div data-bind="component: {
|
||||||
|
name: 'Select',
|
||||||
|
params: {
|
||||||
|
options: domainsOptions,
|
||||||
|
value: alias,
|
||||||
|
optionsText: 'optText',
|
||||||
|
optionsValue: 'optValue',
|
||||||
|
size: 4
|
||||||
|
}
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a class="btn buttonClose" data-bind="command: cancelCommand">
|
||||||
|
<i class="icon-remove"></i>
|
||||||
|
|
||||||
|
<span data-i18n="POPUPS_DOMAIN_ALIAS/BUTTON_CLOSE"></span>
|
||||||
|
</a>
|
||||||
|
<a class="btn buttonClear" data-bind="command: createCommand">
|
||||||
|
<i class="icon-plus" data-bind="visible: !saving()"></i>
|
||||||
|
<i class="icon-spinner animated" data-bind="visible: saving"></i>
|
||||||
|
|
||||||
|
<span data-i18n="POPUPS_DOMAIN_ALIAS/BUTTON_ADD"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CHECK_ALL"></td><td>Ctrl + A, Cmd + A</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CHECK_ALL"></td><td>Ctrl + A, ⌘ + A</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_ARCHIVE"></td><td>Z</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_ARCHIVE"></td><td>Z</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_DELETE"></td><td>Delete, Shift + Delete, #</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_DELETE"></td><td>Delete, Shift + Delete, #</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_THREAD"></td><td>T</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_THREAD"></td><td>T</td></tr>
|
||||||
|
|
@ -62,7 +62,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_FULLSCREEN_TOGGLE"></td><td>Enter</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_FULLSCREEN_TOGGLE"></td><td>Enter</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_BLOCKQUOTES_TOGGLE"></td><td>B</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_BLOCKQUOTES_TOGGLE"></td><td>B</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_PRINT"></td><td>Ctrl + P, Cmd + P</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_PRINT"></td><td>Ctrl + P, ⌘ + P</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_EXIT_FULLSCREEN"></td><td>Esc</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_EXIT_FULLSCREEN"></td><td>Esc</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CLOSE_MESSAGE"></td><td>Esc</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CLOSE_MESSAGE"></td><td>Esc</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SWITCH_TO_LIST"></td><td>Tab, Shift + Tab, Esc</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SWITCH_TO_LIST"></td><td>Tab, Shift + Tab, Esc</td></tr>
|
||||||
|
|
@ -75,9 +75,9 @@
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_COMPOSE_POPUP"></td><td>W, C</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_COMPOSE_POPUP"></td><td>W, C</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_IDENTITIES_DROPDOWN"></td><td>` (tilde), Ctrl + `, Cmd + `</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_OPEN_IDENTITIES_DROPDOWN"></td><td>` (tilde), Ctrl + `, ⌘ + `</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SAVE_MESSAGE"></td><td>Ctrl + S, Cmd + S</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SAVE_MESSAGE"></td><td>Ctrl + S, ⌘ + S</td></tr>
|
||||||
<!--<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SEND_MESSAGE"></td><td>Ctrl + Enter, Cmd + Enter</td></tr>-->
|
<!--<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_SEND_MESSAGE"></td><td>Ctrl + Enter, ⌘ + Enter</td></tr>-->
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_MINIMIZE_COMPOSE_POPUP"></td><td>Esc</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_MINIMIZE_COMPOSE_POPUP"></td><td>Esc</td></tr>
|
||||||
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CLOSE_COMPOSE"></td><td>Shift + Esc</td></tr>
|
<tr><td class="i18n" data-i18n="SHORTCUTS_HELP/LABEL_CLOSE_COMPOSE"></td><td>Shift + Esc</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<input class="i18n" type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
<input class="i18n" type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||||
data-bind="value: value, attr: {'placeholder': placeholder}, enable: enable, css: className" />
|
data-bind="textInput: value, attr: {'placeholder': placeholder}, enable: enable, css: className" />
|
||||||
<!-- ko if: labeled -->
|
<!-- ko if: labeled -->
|
||||||
|
|
||||||
<span class="i18n" data-bind="attr: {'data-i18n': label}"></span>
|
<span class="i18n" data-bind="attr: {'data-i18n': label}"></span>
|
||||||
|
|
|
||||||
15
vendors/knockout-projections/CONTRIBUTING.md
vendored
Normal file
15
vendors/knockout-projections/CONTRIBUTING.md
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Thanks for your interest in contributing to Knockout Projections!
|
||||||
|
|
||||||
|
# Issues
|
||||||
|
|
||||||
|
If you think you've found a bug or limitation in this plugin, please report it using GitHub's [issues](https://github.com/SteveSanderson/knockout-projections/issues/) page.
|
||||||
|
|
||||||
|
# Pull requests not currently accepted
|
||||||
|
|
||||||
|
Very sorry, but I can't currently accept pull requests into this specific plugin due to licensing terms. This limitation *only* applies to Knockout-Projections - it *does not* apply to [Knockout.js itself](http://knockoutjs.com/), which does accept pull requests :)
|
||||||
|
|
||||||
|
If you have a feature suggestion, then either:
|
||||||
|
|
||||||
|
* [Post it as an issue](https://github.com/SteveSanderson/knockout-projections/issues/). Please don't provide a suggested implementation, but instead just describe the requirements in words so that I can implement it if agreed.
|
||||||
|
|
||||||
|
* Or, fork this repo and implement your feature in a separate fork. The beauty of distributed source control is that you're not dependent on a central repo to share and collaborate on enhancements!
|
||||||
28
vendors/knockout-projections/bower.json
vendored
Normal file
28
vendors/knockout-projections/bower.json
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "knockout-projections",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"homepage": "https://github.com/SteveSanderson/knockout-projections",
|
||||||
|
"authors": [
|
||||||
|
"Microsoft Corporation"
|
||||||
|
],
|
||||||
|
"description": "Observable maps and filters for Knockout observable arrays",
|
||||||
|
"main": "dist/knockout-projections.min.js",
|
||||||
|
"moduleType": [
|
||||||
|
"amd",
|
||||||
|
"globals",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"Knockout"
|
||||||
|
],
|
||||||
|
"license": "Apache 2.0",
|
||||||
|
"ignore": [
|
||||||
|
"**/.*",
|
||||||
|
"node_modules",
|
||||||
|
"bower_components",
|
||||||
|
"spec"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"knockout": "~3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
391
vendors/knockout-projections/knockout-projections-1.1.0.js
vendored
Normal file
391
vendors/knockout-projections/knockout-projections-1.1.0.js
vendored
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
/*! Knockout projections plugin - version 1.1.0
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
Copyright (c) Microsoft Corporation
|
||||||
|
All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||||
|
See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(global, undefined) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var exclusionMarker = {};
|
||||||
|
|
||||||
|
function StateItem(ko, inputItem, initialStateArrayIndex, initialOutputArrayIndex, mappingOptions, arrayOfState, outputObservableArray) {
|
||||||
|
// Capture state for later use
|
||||||
|
this.inputItem = inputItem;
|
||||||
|
this.stateArrayIndex = initialStateArrayIndex;
|
||||||
|
this.mappingOptions = mappingOptions;
|
||||||
|
this.arrayOfState = arrayOfState;
|
||||||
|
this.outputObservableArray = outputObservableArray;
|
||||||
|
this.outputArray = this.outputObservableArray.peek();
|
||||||
|
this.isIncluded = null; // Means 'not yet determined'
|
||||||
|
this.suppressNotification = false; // TODO: Instead of this technique, consider raising a sparse diff with a "mutated" entry when a single item changes, and not having any other change logic inside StateItem
|
||||||
|
|
||||||
|
// Set up observables
|
||||||
|
this.outputArrayIndex = ko.observable(initialOutputArrayIndex); // When excluded, it's the position the item would go if it became included
|
||||||
|
this.disposeFuncFromMostRecentMapping = null;
|
||||||
|
this.mappedValueComputed = ko.computed(this.mappingEvaluator, this);
|
||||||
|
this.mappedValueComputed.subscribe(this.onMappingResultChanged, this);
|
||||||
|
this.previousMappedValue = this.mappedValueComputed.peek();
|
||||||
|
}
|
||||||
|
|
||||||
|
StateItem.prototype.dispose = function() {
|
||||||
|
this.mappedValueComputed.dispose();
|
||||||
|
this.disposeResultFromMostRecentEvaluation();
|
||||||
|
};
|
||||||
|
|
||||||
|
StateItem.prototype.disposeResultFromMostRecentEvaluation = function() {
|
||||||
|
if (this.disposeFuncFromMostRecentMapping) {
|
||||||
|
this.disposeFuncFromMostRecentMapping();
|
||||||
|
this.disposeFuncFromMostRecentMapping = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mappingOptions.disposeItem) {
|
||||||
|
var mappedItem = this.mappedValueComputed();
|
||||||
|
this.mappingOptions.disposeItem(mappedItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
StateItem.prototype.mappingEvaluator = function() {
|
||||||
|
if (this.isIncluded !== null) { // i.e., not first run
|
||||||
|
// This is a replace-in-place, so call any dispose callbacks
|
||||||
|
// we have for the earlier value
|
||||||
|
this.disposeResultFromMostRecentEvaluation();
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedValue;
|
||||||
|
if (this.mappingOptions.mapping) {
|
||||||
|
mappedValue = this.mappingOptions.mapping(this.inputItem, this.outputArrayIndex);
|
||||||
|
} else if (this.mappingOptions.mappingWithDisposeCallback) {
|
||||||
|
var mappedValueWithDisposeCallback = this.mappingOptions.mappingWithDisposeCallback(this.inputItem, this.outputArrayIndex);
|
||||||
|
if (!('mappedValue' in mappedValueWithDisposeCallback)) {
|
||||||
|
throw new Error('Return value from mappingWithDisposeCallback should have a \'mappedItem\' property.');
|
||||||
|
}
|
||||||
|
mappedValue = mappedValueWithDisposeCallback.mappedValue;
|
||||||
|
this.disposeFuncFromMostRecentMapping = mappedValueWithDisposeCallback.dispose;
|
||||||
|
} else {
|
||||||
|
throw new Error('No mapping callback given.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var newInclusionState = mappedValue !== exclusionMarker;
|
||||||
|
|
||||||
|
// Inclusion state changes can *only* happen as a result of changing an individual item.
|
||||||
|
// Structural changes to the array can't cause this (because they don't cause any remapping;
|
||||||
|
// they only map newly added items which have no earlier inclusion state to change).
|
||||||
|
if (this.isIncluded !== newInclusionState) {
|
||||||
|
if (this.isIncluded !== null) { // i.e., not first run
|
||||||
|
this.moveSubsequentItemsBecauseInclusionStateChanged(newInclusionState);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isIncluded = newInclusionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
StateItem.prototype.onMappingResultChanged = function(newValue) {
|
||||||
|
if (newValue !== this.previousMappedValue) {
|
||||||
|
if (this.isIncluded) {
|
||||||
|
this.outputArray.splice(this.outputArrayIndex.peek(), 1, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.suppressNotification) {
|
||||||
|
this.outputObservableArray.valueHasMutated();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousMappedValue = newValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
StateItem.prototype.moveSubsequentItemsBecauseInclusionStateChanged = function(newInclusionState) {
|
||||||
|
var outputArrayIndex = this.outputArrayIndex.peek(),
|
||||||
|
iterationIndex,
|
||||||
|
stateItem;
|
||||||
|
|
||||||
|
if (newInclusionState) {
|
||||||
|
// Shift all subsequent items along by one space, and increment their indexes.
|
||||||
|
// Note that changing their indexes might cause remapping, but won't affect their
|
||||||
|
// inclusion status (by definition, inclusion status must not be affected by index,
|
||||||
|
// otherwise you get undefined results) so there's no risk of a chain reaction.
|
||||||
|
this.outputArray.splice(outputArrayIndex, 0, null);
|
||||||
|
for (iterationIndex = this.stateArrayIndex + 1; iterationIndex < this.arrayOfState.length; iterationIndex++) {
|
||||||
|
stateItem = this.arrayOfState[iterationIndex];
|
||||||
|
stateItem.setOutputArrayIndexSilently(stateItem.outputArrayIndex.peek() + 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Shift all subsequent items back by one space, and decrement their indexes
|
||||||
|
this.outputArray.splice(outputArrayIndex, 1);
|
||||||
|
for (iterationIndex = this.stateArrayIndex + 1; iterationIndex < this.arrayOfState.length; iterationIndex++) {
|
||||||
|
stateItem = this.arrayOfState[iterationIndex];
|
||||||
|
stateItem.setOutputArrayIndexSilently(stateItem.outputArrayIndex.peek() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
StateItem.prototype.setOutputArrayIndexSilently = function(newIndex) {
|
||||||
|
// We only want to raise one output array notification per input array change,
|
||||||
|
// so during processing, we suppress notifications
|
||||||
|
this.suppressNotification = true;
|
||||||
|
this.outputArrayIndex(newIndex);
|
||||||
|
this.suppressNotification = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDiffEntryPostOperationIndex(diffEntry, editOffset) {
|
||||||
|
// The diff algorithm's "index" value refers to the output array for additions,
|
||||||
|
// but the "input" array for deletions. Get the output array position.
|
||||||
|
if (!diffEntry) { return null; }
|
||||||
|
switch (diffEntry.status) {
|
||||||
|
case 'added':
|
||||||
|
return diffEntry.index;
|
||||||
|
case 'deleted':
|
||||||
|
return diffEntry.index + editOffset;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown diff status: ' + diffEntry.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertOutputItem(ko, diffEntry, movedStateItems, stateArrayIndex, outputArrayIndex, mappingOptions, arrayOfState, outputObservableArray, outputArray) {
|
||||||
|
// Retain the existing mapped value if this is a move, otherwise perform mapping
|
||||||
|
var isMoved = typeof diffEntry.moved === 'number',
|
||||||
|
stateItem = isMoved ?
|
||||||
|
movedStateItems[diffEntry.moved] :
|
||||||
|
new StateItem(ko, diffEntry.value, stateArrayIndex, outputArrayIndex, mappingOptions, arrayOfState, outputObservableArray);
|
||||||
|
arrayOfState.splice(stateArrayIndex, 0, stateItem);
|
||||||
|
if (stateItem.isIncluded) {
|
||||||
|
outputArray.splice(outputArrayIndex, 0, stateItem.mappedValueComputed.peek());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update indexes
|
||||||
|
if (isMoved) {
|
||||||
|
// We don't change the index until *after* updating this item's position in outputObservableArray,
|
||||||
|
// because changing the index may trigger re-mapping, which in turn would cause the new
|
||||||
|
// value to be written to the 'index' position in the output array
|
||||||
|
stateItem.stateArrayIndex = stateArrayIndex;
|
||||||
|
stateItem.setOutputArrayIndexSilently(outputArrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOutputItem(diffEntry, arrayOfState, stateArrayIndex, outputArrayIndex, outputArray) {
|
||||||
|
var stateItem = arrayOfState.splice(stateArrayIndex, 1)[0];
|
||||||
|
if (stateItem.isIncluded) {
|
||||||
|
outputArray.splice(outputArrayIndex, 1);
|
||||||
|
}
|
||||||
|
if (typeof diffEntry.moved !== 'number') {
|
||||||
|
// Be careful to dispose only if this item really was deleted and not moved
|
||||||
|
stateItem.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRetainedOutputItem(stateItem, stateArrayIndex, outputArrayIndex) {
|
||||||
|
// Just have to update its indexes
|
||||||
|
stateItem.stateArrayIndex = stateArrayIndex;
|
||||||
|
stateItem.setOutputArrayIndexSilently(outputArrayIndex);
|
||||||
|
|
||||||
|
// Return the new value for outputArrayIndex
|
||||||
|
return outputArrayIndex + (stateItem.isIncluded ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLookupOfMovedStateItems(diff, arrayOfState) {
|
||||||
|
// Before we mutate arrayOfComputedMappedValues at all, grab a reference to each moved item
|
||||||
|
var movedStateItems = {};
|
||||||
|
for (var diffIndex = 0; diffIndex < diff.length; diffIndex++) {
|
||||||
|
var diffEntry = diff[diffIndex];
|
||||||
|
if (diffEntry.status === 'added' && (typeof diffEntry.moved === 'number')) {
|
||||||
|
movedStateItems[diffEntry.moved] = arrayOfState[diffEntry.moved];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return movedStateItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFirstModifiedOutputIndex(firstDiffEntry, arrayOfState, outputArray) {
|
||||||
|
// Work out where the first edit will affect the output array
|
||||||
|
// Then we can update outputArrayIndex incrementally while walking the diff list
|
||||||
|
if (!outputArray.length || !arrayOfState[firstDiffEntry.index]) {
|
||||||
|
// The first edit is beyond the end of the output or state array, so we must
|
||||||
|
// just be appending items.
|
||||||
|
return outputArray.length;
|
||||||
|
} else {
|
||||||
|
// The first edit corresponds to an existing state array item, so grab
|
||||||
|
// the first output array index from it.
|
||||||
|
return arrayOfState[firstDiffEntry.index].outputArrayIndex.peek();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondToArrayStructuralChanges(ko, inputObservableArray, arrayOfState, outputArray, outputObservableArray, mappingOptions) {
|
||||||
|
return inputObservableArray.subscribe(function(diff) {
|
||||||
|
if (!diff.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var movedStateItems = makeLookupOfMovedStateItems(diff, arrayOfState),
|
||||||
|
diffIndex = 0,
|
||||||
|
diffEntry = diff[0],
|
||||||
|
editOffset = 0, // A running total of (num(items added) - num(items deleted)) not accounting for filtering
|
||||||
|
outputArrayIndex = diffEntry && getFirstModifiedOutputIndex(diffEntry, arrayOfState, outputArray);
|
||||||
|
|
||||||
|
// Now iterate over the state array, at each stage checking whether the current item
|
||||||
|
// is the next one to have been edited. We can skip all the state array items whose
|
||||||
|
// indexes are less than the first edit index (i.e., diff[0].index).
|
||||||
|
for (var stateArrayIndex = diffEntry.index; diffEntry || (stateArrayIndex < arrayOfState.length); stateArrayIndex++) {
|
||||||
|
// Does the current diffEntry correspond to this position in the state array?
|
||||||
|
if (getDiffEntryPostOperationIndex(diffEntry, editOffset) === stateArrayIndex) {
|
||||||
|
// Yes - insert or delete the corresponding state and output items
|
||||||
|
switch (diffEntry.status) {
|
||||||
|
case 'added':
|
||||||
|
// Add to output, and update indexes
|
||||||
|
var stateItem = insertOutputItem(ko, diffEntry, movedStateItems, stateArrayIndex, outputArrayIndex, mappingOptions, arrayOfState, outputObservableArray, outputArray);
|
||||||
|
if (stateItem.isIncluded) {
|
||||||
|
outputArrayIndex++;
|
||||||
|
}
|
||||||
|
editOffset++;
|
||||||
|
break;
|
||||||
|
case 'deleted':
|
||||||
|
// Just erase from the output, and update indexes
|
||||||
|
deleteOutputItem(diffEntry, arrayOfState, stateArrayIndex, outputArrayIndex, outputArray);
|
||||||
|
editOffset--;
|
||||||
|
stateArrayIndex--; // To compensate for the "for" loop incrementing it
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown diff status: ' + diffEntry.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're done with this diff entry. Move on to the next one.
|
||||||
|
diffIndex++;
|
||||||
|
diffEntry = diff[diffIndex];
|
||||||
|
} else if (stateArrayIndex < arrayOfState.length) {
|
||||||
|
// No - the current item was retained. Just update its index.
|
||||||
|
outputArrayIndex = updateRetainedOutputItem(arrayOfState[stateArrayIndex], stateArrayIndex, outputArrayIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputObservableArray.valueHasMutated();
|
||||||
|
}, null, 'arrayChange');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping
|
||||||
|
function observableArrayMap(ko, mappingOptions) {
|
||||||
|
var inputObservableArray = this,
|
||||||
|
arrayOfState = [],
|
||||||
|
outputArray = [],
|
||||||
|
outputObservableArray = ko.observableArray(outputArray),
|
||||||
|
originalInputArrayContents = inputObservableArray.peek();
|
||||||
|
|
||||||
|
// Shorthand syntax - just pass a function instead of an options object
|
||||||
|
if (typeof mappingOptions === 'function') {
|
||||||
|
mappingOptions = { mapping: mappingOptions };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the options
|
||||||
|
if (mappingOptions.mappingWithDisposeCallback) {
|
||||||
|
if (mappingOptions.mapping || mappingOptions.disposeItem) {
|
||||||
|
throw new Error('\'mappingWithDisposeCallback\' cannot be used in conjunction with \'mapping\' or \'disposeItem\'.');
|
||||||
|
}
|
||||||
|
} else if (!mappingOptions.mapping) {
|
||||||
|
throw new Error('Specify either \'mapping\' or \'mappingWithDisposeCallback\'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state: map each of the inputs
|
||||||
|
for (var i = 0; i < originalInputArrayContents.length; i++) {
|
||||||
|
var inputItem = originalInputArrayContents[i],
|
||||||
|
stateItem = new StateItem(ko, inputItem, i, outputArray.length, mappingOptions, arrayOfState, outputObservableArray),
|
||||||
|
mappedValue = stateItem.mappedValueComputed.peek();
|
||||||
|
arrayOfState.push(stateItem);
|
||||||
|
|
||||||
|
if (stateItem.isIncluded) {
|
||||||
|
outputArray.push(mappedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the input array changes structurally (items added or removed), update the outputs
|
||||||
|
var inputArraySubscription = respondToArrayStructuralChanges(ko, inputObservableArray, arrayOfState, outputArray, outputObservableArray, mappingOptions);
|
||||||
|
|
||||||
|
// Return value is a readonly computed which can track its own changes to permit chaining.
|
||||||
|
// When disposed, it cleans up everything it created.
|
||||||
|
var returnValue = ko.computed(outputObservableArray).extend({ trackArrayChanges: true }),
|
||||||
|
originalDispose = returnValue.dispose;
|
||||||
|
returnValue.dispose = function() {
|
||||||
|
inputArraySubscription.dispose();
|
||||||
|
ko.utils.arrayForEach(arrayOfState, function(stateItem) {
|
||||||
|
stateItem.dispose();
|
||||||
|
});
|
||||||
|
originalDispose.call(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make projections chainable
|
||||||
|
addProjectionFunctions(ko, returnValue);
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
function observableArrayFilter(ko, predicate) {
|
||||||
|
return observableArrayMap.call(this, ko, function(item) {
|
||||||
|
return predicate(item) ? item : exclusionMarker;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attaching projection functions
|
||||||
|
// ------------------------------
|
||||||
|
//
|
||||||
|
// Builds a collection of projection functions that can quickly be attached to any object.
|
||||||
|
// The functions are predefined to retain 'this' and prefix the arguments list with the
|
||||||
|
// relevant 'ko' instance.
|
||||||
|
|
||||||
|
var projectionFunctionsCacheName = '_ko.projections.cache';
|
||||||
|
|
||||||
|
function attachProjectionFunctionsCache(ko) {
|
||||||
|
// Wraps callback so that, when invoked, its arguments list is prefixed by 'ko' and 'this'
|
||||||
|
function makeCaller(ko, callback) {
|
||||||
|
return function() {
|
||||||
|
return callback.apply(this, [ko].concat(Array.prototype.slice.call(arguments, 0)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ko[projectionFunctionsCacheName] = {
|
||||||
|
map: makeCaller(ko, observableArrayMap),
|
||||||
|
filter: makeCaller(ko, observableArrayFilter)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProjectionFunctions(ko, target) {
|
||||||
|
ko.utils.extend(target, ko[projectionFunctionsCacheName]);
|
||||||
|
return target; // Enable chaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module initialisation
|
||||||
|
// ---------------------
|
||||||
|
//
|
||||||
|
// When this script is first evaluated, it works out what kind of module loading scenario
|
||||||
|
// it is in (Node.js or a browser `<script>` tag), and then attaches itself to whichever
|
||||||
|
// instance of Knockout.js it can find.
|
||||||
|
|
||||||
|
function attachToKo(ko) {
|
||||||
|
ko.projections = {
|
||||||
|
_exclusionMarker: exclusionMarker
|
||||||
|
};
|
||||||
|
attachProjectionFunctionsCache(ko);
|
||||||
|
addProjectionFunctions(ko, ko.observableArray.fn); // Make all observable arrays projectable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines which module loading scenario we're in, grabs dependencies, and attaches to KO
|
||||||
|
function prepareExports() {
|
||||||
|
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||||
|
// Node.js case - load KO synchronously
|
||||||
|
var ko = require('knockout');
|
||||||
|
attachToKo(ko);
|
||||||
|
module.exports = ko;
|
||||||
|
} else if (typeof define === 'function' && define.amd) {
|
||||||
|
define(['knockout'], attachToKo);
|
||||||
|
} else if ('ko' in global) {
|
||||||
|
// Non-module case - attach to the global instance
|
||||||
|
attachToKo(global.ko);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareExports();
|
||||||
|
|
||||||
|
})(this);
|
||||||
10
vendors/knockout-projections/knockout-projections-1.1.0.min.js
vendored
Normal file
10
vendors/knockout-projections/knockout-projections-1.1.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
vendors/knockout-projections/package.json
vendored
7
vendors/knockout-projections/package.json
vendored
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "knockout-projections",
|
"name": "knockout-projections",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Knockout.js observable arrays get smarter",
|
"description": "Knockout.js observable arrays get smarter",
|
||||||
"main": "knockout-projections.js",
|
"main": "dist/knockout-projections.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
"test": "test"
|
"test": "test"
|
||||||
},
|
},
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"grunt-contrib-uglify": "~0.2.0",
|
"grunt-contrib-uglify": "~0.2.0",
|
||||||
"grunt-contrib-concat": "~0.3.0",
|
"grunt-contrib-concat": "~0.3.0",
|
||||||
"grunt-contrib-watch": "~0.4.3",
|
"grunt-contrib-watch": "~0.4.3",
|
||||||
"grunt-jasmine-node": "~0.1.0"
|
"grunt-jasmine-node": "~0.1.0",
|
||||||
|
"grunt-string-replace": "~0.2.7"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
|
|
||||||
17
vendors/knockout-projections/package.nuspec
vendored
Normal file
17
vendors/knockout-projections/package.nuspec
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
|
||||||
|
<metadata>
|
||||||
|
<id>knockout-projections</id>
|
||||||
|
<version>1.1.0</version>
|
||||||
|
<authors>Microsoft Corporation</authors>
|
||||||
|
<requireLicenseAcceptance>true</requireLicenseAcceptance>
|
||||||
|
<description>Knockout.js observable arrays get smarter</description>
|
||||||
|
<licenseUrl>http://www.apache.org/licenses/LICENSE-2.0.html</licenseUrl>
|
||||||
|
<dependencies>
|
||||||
|
<dependency id="knockoutjs" version="3.0.0" />
|
||||||
|
</dependencies>
|
||||||
|
</metadata>
|
||||||
|
<files>
|
||||||
|
<file src="dist\*.js" target="content\scripts" />
|
||||||
|
</files>
|
||||||
|
</package>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue