mirror of
https://github.com/overleaf/overleaf.git
synced 2024-09-16 02:52:31 -04:00
Merge pull request #15248 from overleaf/mj-eslint-angular-components
[web] Add eslint rules for angularjs components GitOrigin-RevId: 1343d584368faeb912f04c5879228bcbd07a042a
This commit is contained in:
parent
4b6b9c3bef
commit
1a92e1b664
33 changed files with 1846 additions and 1719 deletions
|
@ -226,7 +226,32 @@
|
|||
// Require .jsx or .tsx file extension when using JSX
|
||||
"react/jsx-filename-extension": ["error", {
|
||||
"extensions": [".jsx", ".tsx"]
|
||||
}]
|
||||
}],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
// Begin: Make sure angular can withstand minification
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > :function[params.length > 0]",
|
||||
"message": "Wrap the function in an array with the parameter names, to withstand minifcation. E.g. App.controller('MyController', ['param1', function(param1) {}]"
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > ArrowFunctionExpression",
|
||||
"message": "Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}"
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrowFunctionExpression",
|
||||
"message": "Use standard function syntax instead of arrow function syntax in angular components. E.g. function(param1) {}"
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression > :not(:function, Identifier):last-child",
|
||||
"message": "Last element of the array must be a function. E.g ['param1', function(param1) {}]"
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='App'][callee.property.name=/run|directive|config|controller/] > ArrayExpression[elements.length=0]",
|
||||
"message": "Array must not be empty. Add parameters and a function. E.g ['param1', function(param1) {}]"
|
||||
}
|
||||
// End: Make sure angular can withstand minification
|
||||
]
|
||||
}
|
||||
},
|
||||
// React + TypeScript-specific rules
|
||||
|
|
|
@ -4,177 +4,180 @@ App.directive('asyncForm', [
|
|||
'$http',
|
||||
'validateCaptcha',
|
||||
'validateCaptchaV3',
|
||||
($http, validateCaptcha, validateCaptchaV3) => ({
|
||||
controller: [
|
||||
'$scope',
|
||||
'$location',
|
||||
function ($scope, $location) {
|
||||
this.getEmail = () => $scope.email
|
||||
this.getEmailFromQuery = () =>
|
||||
$location.search().email || $location.search().new_email
|
||||
return this
|
||||
},
|
||||
],
|
||||
link(scope, element, attrs, ctrl) {
|
||||
let response
|
||||
const formName = attrs.asyncForm
|
||||
function ($http, validateCaptcha, validateCaptchaV3) {
|
||||
return {
|
||||
controller: [
|
||||
'$scope',
|
||||
'$location',
|
||||
function ($scope, $location) {
|
||||
this.getEmail = () => $scope.email
|
||||
this.getEmailFromQuery = () =>
|
||||
$location.search().email || $location.search().new_email
|
||||
return this
|
||||
},
|
||||
],
|
||||
link(scope, element, attrs, ctrl) {
|
||||
let response
|
||||
const formName = attrs.asyncForm
|
||||
|
||||
scope[attrs.name].response = response = {}
|
||||
scope[attrs.name].inflight = false
|
||||
scope.email =
|
||||
scope.email ||
|
||||
scope.usersEmail ||
|
||||
ctrl.getEmailFromQuery() ||
|
||||
attrs.newEmail
|
||||
scope[attrs.name].response = response = {}
|
||||
scope[attrs.name].inflight = false
|
||||
scope.email =
|
||||
scope.email ||
|
||||
scope.usersEmail ||
|
||||
ctrl.getEmailFromQuery() ||
|
||||
attrs.newEmail
|
||||
|
||||
const validateCaptchaIfEnabled = function (callback) {
|
||||
scope.$applyAsync(() => {
|
||||
scope[attrs.name].inflight = true
|
||||
})
|
||||
const validateCaptchaIfEnabled = function (callback) {
|
||||
scope.$applyAsync(() => {
|
||||
scope[attrs.name].inflight = true
|
||||
})
|
||||
|
||||
if (attrs.captchaActionName) {
|
||||
validateCaptchaV3(attrs.captchaActionName)
|
||||
}
|
||||
if (attrs.captcha != null) {
|
||||
validateCaptcha(callback)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const _submitRequest = function (grecaptchaResponse) {
|
||||
const formData = {}
|
||||
for (const data of Array.from(element.serializeArray())) {
|
||||
formData[data.name] = data.value
|
||||
if (attrs.captchaActionName) {
|
||||
validateCaptchaV3(attrs.captchaActionName)
|
||||
}
|
||||
if (attrs.captcha != null) {
|
||||
validateCaptcha(callback)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
if (grecaptchaResponse) {
|
||||
formData['g-recaptcha-response'] = grecaptchaResponse
|
||||
}
|
||||
const _submitRequest = function (grecaptchaResponse) {
|
||||
const formData = {}
|
||||
for (const data of Array.from(element.serializeArray())) {
|
||||
formData[data.name] = data.value
|
||||
}
|
||||
|
||||
// clear the response object which may be referenced downstream
|
||||
Object.keys(response).forEach(field => delete response[field])
|
||||
if (grecaptchaResponse) {
|
||||
formData['g-recaptcha-response'] = grecaptchaResponse
|
||||
}
|
||||
|
||||
// for asyncForm prevent automatic redirect to /login if
|
||||
// authentication fails, we will handle it ourselves
|
||||
const httpRequestFn = _httpRequestFn(element.attr('method'))
|
||||
return httpRequestFn(element.attr('action'), formData, {
|
||||
disableAutoLoginRedirect: true,
|
||||
})
|
||||
.then(function (httpResponse) {
|
||||
const { data, headers } = httpResponse
|
||||
scope[attrs.name].inflight = false
|
||||
response.success = true
|
||||
response.error = false
|
||||
// clear the response object which may be referenced downstream
|
||||
Object.keys(response).forEach(field => delete response[field])
|
||||
|
||||
const onSuccessHandler = scope[attrs.onSuccess]
|
||||
if (onSuccessHandler) {
|
||||
onSuccessHandler(httpResponse)
|
||||
return
|
||||
}
|
||||
// for asyncForm prevent automatic redirect to /login if
|
||||
// authentication fails, we will handle it ourselves
|
||||
const httpRequestFn = _httpRequestFn(element.attr('method'))
|
||||
return httpRequestFn(element.attr('action'), formData, {
|
||||
disableAutoLoginRedirect: true,
|
||||
})
|
||||
.then(function (httpResponse) {
|
||||
const { data, headers } = httpResponse
|
||||
scope[attrs.name].inflight = false
|
||||
response.success = true
|
||||
response.error = false
|
||||
|
||||
if (data.redir) {
|
||||
ga('send', 'event', formName, 'success')
|
||||
return (window.location = data.redir)
|
||||
} else if (data.message) {
|
||||
response.message = data.message
|
||||
const onSuccessHandler = scope[attrs.onSuccess]
|
||||
if (onSuccessHandler) {
|
||||
onSuccessHandler(httpResponse)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.message.type === 'error') {
|
||||
response.success = false
|
||||
response.error = true
|
||||
return ga('send', 'event', formName, 'failure', data.message)
|
||||
if (data.redir) {
|
||||
ga('send', 'event', formName, 'success')
|
||||
return (window.location = data.redir)
|
||||
} else if (data.message) {
|
||||
response.message = data.message
|
||||
|
||||
if (data.message.type === 'error') {
|
||||
response.success = false
|
||||
response.error = true
|
||||
return ga('send', 'event', formName, 'failure', data.message)
|
||||
} else {
|
||||
return ga('send', 'event', formName, 'success')
|
||||
}
|
||||
} else if (scope.$eval(attrs.asyncFormDownloadResponse)) {
|
||||
const blob = new Blob([data], {
|
||||
type: headers('Content-Type'),
|
||||
})
|
||||
location.href = URL.createObjectURL(blob) // Trigger file save
|
||||
}
|
||||
})
|
||||
.catch(function (httpResponse) {
|
||||
const { data, status } = httpResponse
|
||||
scope[attrs.name].inflight = false
|
||||
response.success = false
|
||||
response.error = true
|
||||
response.status = status
|
||||
response.data = data
|
||||
|
||||
const onErrorHandler = scope[attrs.onError]
|
||||
if (onErrorHandler) {
|
||||
onErrorHandler(httpResponse)
|
||||
return
|
||||
}
|
||||
|
||||
let responseMessage
|
||||
if (data.message && data.message.text) {
|
||||
responseMessage = data.message.text
|
||||
} else {
|
||||
return ga('send', 'event', formName, 'success')
|
||||
responseMessage = data.message
|
||||
}
|
||||
} else if (scope.$eval(attrs.asyncFormDownloadResponse)) {
|
||||
const blob = new Blob([data], {
|
||||
type: headers('Content-Type'),
|
||||
})
|
||||
location.href = URL.createObjectURL(blob) // Trigger file save
|
||||
}
|
||||
})
|
||||
.catch(function (httpResponse) {
|
||||
const { data, status } = httpResponse
|
||||
scope[attrs.name].inflight = false
|
||||
response.success = false
|
||||
response.error = true
|
||||
response.status = status
|
||||
response.data = data
|
||||
|
||||
const onErrorHandler = scope[attrs.onError]
|
||||
if (onErrorHandler) {
|
||||
onErrorHandler(httpResponse)
|
||||
return
|
||||
}
|
||||
|
||||
let responseMessage
|
||||
if (data.message && data.message.text) {
|
||||
responseMessage = data.message.text
|
||||
} else {
|
||||
responseMessage = data.message
|
||||
}
|
||||
|
||||
if (status === 400) {
|
||||
// Bad Request
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Invalid Request. Please correct the data and try again.',
|
||||
type: 'error',
|
||||
if (status === 400) {
|
||||
// Bad Request
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Invalid Request. Please correct the data and try again.',
|
||||
type: 'error',
|
||||
}
|
||||
} else if (status === 403) {
|
||||
// Forbidden
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.',
|
||||
type: 'error',
|
||||
}
|
||||
} else if (status === 429) {
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Too many attempts. Please wait for a while and try again.',
|
||||
type: 'error',
|
||||
}
|
||||
} else {
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Something went wrong talking to the server :(. Please try again.',
|
||||
type: 'error',
|
||||
}
|
||||
}
|
||||
} else if (status === 403) {
|
||||
// Forbidden
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.',
|
||||
type: 'error',
|
||||
}
|
||||
} else if (status === 429) {
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Too many attempts. Please wait for a while and try again.',
|
||||
type: 'error',
|
||||
}
|
||||
} else {
|
||||
response.message = {
|
||||
text:
|
||||
responseMessage ||
|
||||
'Something went wrong talking to the server :(. Please try again.',
|
||||
type: 'error',
|
||||
}
|
||||
}
|
||||
ga('send', 'event', formName, 'failure', data.message)
|
||||
})
|
||||
}
|
||||
|
||||
const submit = () =>
|
||||
validateCaptchaIfEnabled(response => _submitRequest(response))
|
||||
|
||||
const _httpRequestFn = (method = 'post') => {
|
||||
const $HTTP_FNS = {
|
||||
post: $http.post,
|
||||
get: $http.get,
|
||||
ga('send', 'event', formName, 'failure', data.message)
|
||||
})
|
||||
}
|
||||
return $HTTP_FNS[method.toLowerCase()]
|
||||
}
|
||||
|
||||
element.on('submit', function (e) {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
})
|
||||
const submit = () =>
|
||||
validateCaptchaIfEnabled(response => _submitRequest(response))
|
||||
|
||||
if (attrs.autoSubmit) {
|
||||
submit()
|
||||
}
|
||||
},
|
||||
}),
|
||||
const _httpRequestFn = (method = 'post') => {
|
||||
const $HTTP_FNS = {
|
||||
post: $http.post,
|
||||
get: $http.get,
|
||||
}
|
||||
return $HTTP_FNS[method.toLowerCase()]
|
||||
}
|
||||
|
||||
element.on('submit', function (e) {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
})
|
||||
|
||||
if (attrs.autoSubmit) {
|
||||
submit()
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.directive('formMessages', () => ({
|
||||
restrict: 'E',
|
||||
template: `\
|
||||
App.directive('formMessages', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: `\
|
||||
<div class="alert" ng-class="{
|
||||
'alert-danger': form.response.message.type == 'error',
|
||||
'alert-success': form.response.message.type != 'error'
|
||||
|
@ -182,8 +185,9 @@ App.directive('formMessages', () => ({
|
|||
</div>
|
||||
<div ng-transclude></div>\
|
||||
`,
|
||||
transclude: true,
|
||||
scope: {
|
||||
form: '=for',
|
||||
},
|
||||
}))
|
||||
transclude: true,
|
||||
scope: {
|
||||
form: '=for',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,63 +2,70 @@ import _ from 'lodash'
|
|||
import App from '../base'
|
||||
App.directive('bookmarkableTabset', [
|
||||
'$location',
|
||||
$location => ({
|
||||
restrict: 'A',
|
||||
require: 'tabset',
|
||||
link(scope, el, attrs, tabset) {
|
||||
const _makeActive = function (hash) {
|
||||
if (hash && hash !== '') {
|
||||
const matchingTab = _.find(
|
||||
tabset.tabs,
|
||||
tab => tab.bookmarkableTabId === hash
|
||||
)
|
||||
if (matchingTab) {
|
||||
matchingTab.select()
|
||||
return el.children()[0].scrollIntoView({ behavior: 'smooth' })
|
||||
function ($location) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'tabset',
|
||||
link(scope, el, attrs, tabset) {
|
||||
const _makeActive = function (hash) {
|
||||
if (hash && hash !== '') {
|
||||
const matchingTab = _.find(
|
||||
tabset.tabs,
|
||||
tab => tab.bookmarkableTabId === hash
|
||||
)
|
||||
if (matchingTab) {
|
||||
matchingTab.select()
|
||||
return el.children()[0].scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.$applyAsync(function () {
|
||||
// for page load
|
||||
const hash = $location.hash()
|
||||
_makeActive(hash)
|
||||
|
||||
// for links within page to a tab
|
||||
// this needs to be within applyAsync because there could be a link
|
||||
// within a tab to another tab
|
||||
const linksToTabs = document.querySelectorAll('.link-to-tab')
|
||||
const _clickLinkToTab = event => {
|
||||
const hash = event.currentTarget.getAttribute('href').split('#').pop()
|
||||
scope.$applyAsync(function () {
|
||||
// for page load
|
||||
const hash = $location.hash()
|
||||
_makeActive(hash)
|
||||
}
|
||||
|
||||
if (linksToTabs) {
|
||||
Array.from(linksToTabs).map(link =>
|
||||
link.addEventListener('click', _clickLinkToTab)
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
// for links within page to a tab
|
||||
// this needs to be within applyAsync because there could be a link
|
||||
// within a tab to another tab
|
||||
const linksToTabs = document.querySelectorAll('.link-to-tab')
|
||||
const _clickLinkToTab = event => {
|
||||
const hash = event.currentTarget
|
||||
.getAttribute('href')
|
||||
.split('#')
|
||||
.pop()
|
||||
_makeActive(hash)
|
||||
}
|
||||
|
||||
if (linksToTabs) {
|
||||
Array.from(linksToTabs).map(link =>
|
||||
link.addEventListener('click', _clickLinkToTab)
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.directive('bookmarkableTab', [
|
||||
'$location',
|
||||
$location => ({
|
||||
restrict: 'A',
|
||||
require: 'tab',
|
||||
link(scope, el, attrs, tab) {
|
||||
const tabScope = el.isolateScope()
|
||||
const tabId = attrs.bookmarkableTab
|
||||
if (tabScope && tabId && tabId !== '') {
|
||||
tabScope.bookmarkableTabId = tabId
|
||||
tabScope.$watch('active', function (isActive, wasActive) {
|
||||
if (isActive && !wasActive && $location.hash() !== tabId) {
|
||||
return $location.hash(tabId)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}),
|
||||
function ($location) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'tab',
|
||||
link(scope, el, attrs, tab) {
|
||||
const tabScope = el.isolateScope()
|
||||
const tabId = attrs.bookmarkableTab
|
||||
if (tabScope && tabId && tabId !== '') {
|
||||
tabScope.bookmarkableTabId = tabId
|
||||
tabScope.$watch('active', function (isActive, wasActive) {
|
||||
if (isActive && !wasActive && $location.hash() !== tabId) {
|
||||
return $location.hash(tabId)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -6,85 +6,88 @@ import _ from 'lodash'
|
|||
*/
|
||||
import App from '../base'
|
||||
import '../vendor/libs/passfield'
|
||||
App.directive('complexPassword', () => ({
|
||||
require: ['^asyncForm', 'ngModel'],
|
||||
App.directive('complexPassword', function () {
|
||||
return {
|
||||
require: ['^asyncForm', 'ngModel'],
|
||||
|
||||
link(scope, element, attrs, ctrl) {
|
||||
PassField.Config.blackList = []
|
||||
const defaultPasswordOpts = {
|
||||
pattern: '',
|
||||
length: {
|
||||
min: 6,
|
||||
max: 72,
|
||||
},
|
||||
allowEmpty: false,
|
||||
allowAnyChars: false,
|
||||
isMasked: true,
|
||||
showToggle: false,
|
||||
showGenerate: false,
|
||||
showTip: false,
|
||||
showWarn: false,
|
||||
checkMode: PassField.CheckModes.STRICT,
|
||||
chars: {
|
||||
digits: '1234567890',
|
||||
letters: 'abcdefghijklmnopqrstuvwxyz',
|
||||
letters_up: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
symbols: '@#$%^&*()-_=+[]{};:<>/?!£€.,',
|
||||
},
|
||||
}
|
||||
link(scope, element, attrs, ctrl) {
|
||||
PassField.Config.blackList = []
|
||||
const defaultPasswordOpts = {
|
||||
pattern: '',
|
||||
length: {
|
||||
min: 6,
|
||||
max: 72,
|
||||
},
|
||||
allowEmpty: false,
|
||||
allowAnyChars: false,
|
||||
isMasked: true,
|
||||
showToggle: false,
|
||||
showGenerate: false,
|
||||
showTip: false,
|
||||
showWarn: false,
|
||||
checkMode: PassField.CheckModes.STRICT,
|
||||
chars: {
|
||||
digits: '1234567890',
|
||||
letters: 'abcdefghijklmnopqrstuvwxyz',
|
||||
letters_up: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||
symbols: '@#$%^&*()-_=+[]{};:<>/?!£€.,',
|
||||
},
|
||||
}
|
||||
|
||||
const opts = _.defaults(
|
||||
window.passwordStrengthOptions || {},
|
||||
defaultPasswordOpts
|
||||
)
|
||||
const opts = _.defaults(
|
||||
window.passwordStrengthOptions || {},
|
||||
defaultPasswordOpts
|
||||
)
|
||||
|
||||
if (opts.length.min === 1) {
|
||||
// this allows basically anything to be a valid password
|
||||
opts.acceptRate = 0
|
||||
}
|
||||
if (opts.length.min === 1) {
|
||||
// this allows basically anything to be a valid password
|
||||
opts.acceptRate = 0
|
||||
}
|
||||
|
||||
if (opts.length.max > 72) {
|
||||
// there is a hard limit of 71 characters in the password at the backend
|
||||
opts.length.max = 72
|
||||
}
|
||||
if (opts.length.max > 72) {
|
||||
// there is a hard limit of 71 characters in the password at the backend
|
||||
opts.length.max = 72
|
||||
}
|
||||
|
||||
if (opts.length.max > 0) {
|
||||
// PassField's notion of 'max' is non-inclusive
|
||||
opts.length.max += 1
|
||||
}
|
||||
if (opts.length.max > 0) {
|
||||
// PassField's notion of 'max' is non-inclusive
|
||||
opts.length.max += 1
|
||||
}
|
||||
|
||||
const passField = new PassField.Field('passwordField', opts)
|
||||
const [asyncFormCtrl, ngModelCtrl] = Array.from(ctrl)
|
||||
const passField = new PassField.Field('passwordField', opts)
|
||||
const [asyncFormCtrl, ngModelCtrl] = Array.from(ctrl)
|
||||
|
||||
ngModelCtrl.$parsers.unshift(function (modelValue) {
|
||||
let isValid = passField.validatePass()
|
||||
const email = asyncFormCtrl.getEmail() || window.usersEmail
|
||||
ngModelCtrl.$parsers.unshift(function (modelValue) {
|
||||
let isValid = passField.validatePass()
|
||||
const email = asyncFormCtrl.getEmail() || window.usersEmail
|
||||
|
||||
if (!isValid) {
|
||||
scope.complexPasswordErrorMessage = passField.getPassValidationMessage()
|
||||
} else if (typeof email === 'string' && email !== '') {
|
||||
const startOfEmail = email.split('@')[0]
|
||||
if (
|
||||
modelValue.indexOf(email) !== -1 ||
|
||||
modelValue.indexOf(startOfEmail) !== -1
|
||||
) {
|
||||
isValid = false
|
||||
if (!isValid) {
|
||||
scope.complexPasswordErrorMessage =
|
||||
'Password can not contain email address'
|
||||
passField.getPassValidationMessage()
|
||||
} else if (typeof email === 'string' && email !== '') {
|
||||
const startOfEmail = email.split('@')[0]
|
||||
if (
|
||||
modelValue.indexOf(email) !== -1 ||
|
||||
modelValue.indexOf(startOfEmail) !== -1
|
||||
) {
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage =
|
||||
'Password can not contain email address'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opts.length.max != null && modelValue.length >= opts.length.max) {
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = `Maximum password length ${
|
||||
opts.length.max - 1
|
||||
} exceeded`
|
||||
}
|
||||
if (opts.length.min != null && modelValue.length < opts.length.min) {
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = `Password too short, minimum ${opts.length.min}`
|
||||
}
|
||||
ngModelCtrl.$setValidity('complexPassword', isValid)
|
||||
return modelValue
|
||||
})
|
||||
},
|
||||
}))
|
||||
if (opts.length.max != null && modelValue.length >= opts.length.max) {
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = `Maximum password length ${
|
||||
opts.length.max - 1
|
||||
} exceeded`
|
||||
}
|
||||
if (opts.length.min != null && modelValue.length < opts.length.min) {
|
||||
isValid = false
|
||||
scope.complexPasswordErrorMessage = `Password too short, minimum ${opts.length.min}`
|
||||
}
|
||||
ngModelCtrl.$setValidity('complexPassword', isValid)
|
||||
return modelValue
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -7,15 +7,17 @@
|
|||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.directive('equals', () => ({
|
||||
require: 'ngModel',
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
const firstField = `#${attrs.equals}`
|
||||
return elem.add(firstField).on('keyup', () =>
|
||||
scope.$apply(function () {
|
||||
const equal = elem.val() === $(firstField).val()
|
||||
return ctrl.$setValidity('areEqual', equal)
|
||||
})
|
||||
)
|
||||
},
|
||||
}))
|
||||
export default App.directive('equals', function () {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link(scope, elem, attrs, ctrl) {
|
||||
const firstField = `#${attrs.equals}`
|
||||
return elem.add(firstField).on('keyup', () =>
|
||||
scope.$apply(function () {
|
||||
const equal = elem.val() === $(firstField).val()
|
||||
return ctrl.$setValidity('areEqual', equal)
|
||||
})
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -63,68 +63,72 @@ const isInViewport = function (element) {
|
|||
|
||||
export default App.directive('eventTracking', [
|
||||
'eventTracking',
|
||||
eventTracking => ({
|
||||
scope: {
|
||||
eventTracking: '@',
|
||||
eventSegmentation: '=?',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
const sendGA = attrs.eventTrackingGa || false
|
||||
const sendMB = attrs.eventTrackingMb || false
|
||||
const sendMBFunction = attrs.eventTrackingSendOnce
|
||||
? 'sendMBOnce'
|
||||
: 'sendMB'
|
||||
const sendGAFunction = attrs.eventTrackingSendOnce ? 'sendGAOnce' : 'send'
|
||||
const segmentation = scope.eventSegmentation || {}
|
||||
segmentation.page = window.location.pathname
|
||||
function (eventTracking) {
|
||||
return {
|
||||
scope: {
|
||||
eventTracking: '@',
|
||||
eventSegmentation: '=?',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
const sendGA = attrs.eventTrackingGa || false
|
||||
const sendMB = attrs.eventTrackingMb || false
|
||||
const sendMBFunction = attrs.eventTrackingSendOnce
|
||||
? 'sendMBOnce'
|
||||
: 'sendMB'
|
||||
const sendGAFunction = attrs.eventTrackingSendOnce
|
||||
? 'sendGAOnce'
|
||||
: 'send'
|
||||
const segmentation = scope.eventSegmentation || {}
|
||||
segmentation.page = window.location.pathname
|
||||
|
||||
const sendEvent = function (scrollEvent) {
|
||||
/*
|
||||
const sendEvent = function (scrollEvent) {
|
||||
/*
|
||||
@param {boolean} scrollEvent Use to unbind scroll event
|
||||
*/
|
||||
if (sendMB) {
|
||||
eventTracking[sendMBFunction](scope.eventTracking, segmentation)
|
||||
if (sendMB) {
|
||||
eventTracking[sendMBFunction](scope.eventTracking, segmentation)
|
||||
}
|
||||
if (sendGA) {
|
||||
eventTracking[sendGAFunction](
|
||||
attrs.eventTrackingGa,
|
||||
attrs.eventTrackingAction || scope.eventTracking,
|
||||
attrs.eventTrackingLabel || ''
|
||||
)
|
||||
}
|
||||
if (scrollEvent) {
|
||||
return $(window).unbind('resize scroll')
|
||||
}
|
||||
}
|
||||
if (sendGA) {
|
||||
eventTracking[sendGAFunction](
|
||||
attrs.eventTrackingGa,
|
||||
attrs.eventTrackingAction || scope.eventTracking,
|
||||
attrs.eventTrackingLabel || ''
|
||||
|
||||
if (attrs.eventTrackingTrigger === 'load') {
|
||||
return sendEvent()
|
||||
} else if (attrs.eventTrackingTrigger === 'click') {
|
||||
return element.on('click', e => sendEvent())
|
||||
} else if (attrs.eventTrackingTrigger === 'hover') {
|
||||
let timer = null
|
||||
let timeoutAmt = 500
|
||||
if (attrs.eventHoverAmt) {
|
||||
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
|
||||
}
|
||||
return element
|
||||
.on('mouseenter', function () {
|
||||
timer = setTimeout(() => sendEvent(), timeoutAmt)
|
||||
})
|
||||
.on('mouseleave', () => clearTimeout(timer))
|
||||
} else if (
|
||||
attrs.eventTrackingTrigger === 'scroll' &&
|
||||
!eventTracking.eventInCache(scope.eventTracking)
|
||||
) {
|
||||
$(window).on(
|
||||
'resize scroll',
|
||||
_.throttle(() => {
|
||||
if (isInViewport(element)) {
|
||||
sendEvent(true)
|
||||
}
|
||||
}, 500)
|
||||
)
|
||||
}
|
||||
if (scrollEvent) {
|
||||
return $(window).unbind('resize scroll')
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.eventTrackingTrigger === 'load') {
|
||||
return sendEvent()
|
||||
} else if (attrs.eventTrackingTrigger === 'click') {
|
||||
return element.on('click', e => sendEvent())
|
||||
} else if (attrs.eventTrackingTrigger === 'hover') {
|
||||
let timer = null
|
||||
let timeoutAmt = 500
|
||||
if (attrs.eventHoverAmt) {
|
||||
timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
|
||||
}
|
||||
return element
|
||||
.on('mouseenter', function () {
|
||||
timer = setTimeout(() => sendEvent(), timeoutAmt)
|
||||
})
|
||||
.on('mouseleave', () => clearTimeout(timer))
|
||||
} else if (
|
||||
attrs.eventTrackingTrigger === 'scroll' &&
|
||||
!eventTracking.eventInCache(scope.eventTracking)
|
||||
) {
|
||||
$(window).on(
|
||||
'resize scroll',
|
||||
_.throttle(() => {
|
||||
if (isInViewport(element)) {
|
||||
sendEvent(true)
|
||||
}
|
||||
}, 500)
|
||||
)
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -7,23 +7,25 @@
|
|||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.directive('expandableTextArea', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, el) {
|
||||
const resetHeight = function () {
|
||||
const curHeight = el.outerHeight()
|
||||
const fitHeight = el.prop('scrollHeight')
|
||||
// clear height if text area is empty
|
||||
if (el.val() === '') {
|
||||
el.css('height', 'unset')
|
||||
export default App.directive('expandableTextArea', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, el) {
|
||||
const resetHeight = function () {
|
||||
const curHeight = el.outerHeight()
|
||||
const fitHeight = el.prop('scrollHeight')
|
||||
// clear height if text area is empty
|
||||
if (el.val() === '') {
|
||||
el.css('height', 'unset')
|
||||
}
|
||||
// otherwise expand to fit text
|
||||
else if (fitHeight > curHeight) {
|
||||
scope.$emit('expandable-text-area:resize')
|
||||
el.css('height', fitHeight)
|
||||
}
|
||||
}
|
||||
// otherwise expand to fit text
|
||||
else if (fitHeight > curHeight) {
|
||||
scope.$emit('expandable-text-area:resize')
|
||||
el.css('height', fitHeight)
|
||||
}
|
||||
}
|
||||
|
||||
return scope.$watch(() => el.val(), resetHeight)
|
||||
},
|
||||
}))
|
||||
return scope.$watch(() => el.val(), resetHeight)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,82 +12,96 @@
|
|||
import App from '../base'
|
||||
App.directive('focusWhen', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attr) {
|
||||
return scope.$watch(attr.focusWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => element.focus())
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attr) {
|
||||
return scope.$watch(attr.focusWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => element.focus())
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.directive('focusOn', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.focusOn, () => element.focus())
|
||||
},
|
||||
}))
|
||||
App.directive('focusOn', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.focusOn, () => element.focus())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
App.directive('selectWhen', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attr) {
|
||||
return scope.$watch(attr.selectWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => element.select())
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attr) {
|
||||
return scope.$watch(attr.selectWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => element.select())
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.directive('selectOn', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.selectOn, () => element.select())
|
||||
},
|
||||
}))
|
||||
App.directive('selectOn', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.selectOn, () => element.select())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
App.directive('selectNameWhen', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(attrs.selectNameWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => selectName(element))
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(attrs.selectNameWhen, function (value) {
|
||||
if (value) {
|
||||
return $timeout(() => selectName(element))
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
App.directive('selectNameOn', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.selectNameOn, () => selectName(element))
|
||||
},
|
||||
}))
|
||||
App.directive('selectNameOn', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$on(attrs.selectNameOn, () => selectName(element))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
App.directive('focus', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
scope: {
|
||||
trigger: '@focus',
|
||||
},
|
||||
function ($timeout) {
|
||||
return {
|
||||
scope: {
|
||||
trigger: '@focus',
|
||||
},
|
||||
|
||||
link(scope, element) {
|
||||
return scope.$watch('trigger', function (value) {
|
||||
if (value === 'true') {
|
||||
return $timeout(() => element[0].focus())
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
link(scope, element) {
|
||||
return scope.$watch('trigger', function (value) {
|
||||
if (value === 'true') {
|
||||
return $timeout(() => element[0].focus())
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
||||
function selectName(element) {
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.directive('maxHeight', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(attrs.maxHeight, function (value) {
|
||||
if (value != null) {
|
||||
return element.css({ 'max-height': value })
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
export default App.directive('maxHeight', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(attrs.maxHeight, function (value) {
|
||||
if (value != null) {
|
||||
return element.css({ 'max-height': value })
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -7,13 +7,12 @@
|
|||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.directive(
|
||||
'onEnter',
|
||||
() => (scope, element, attrs) =>
|
||||
export default App.directive('onEnter', function () {
|
||||
return (scope, element, attrs) =>
|
||||
element.bind('keydown keypress', function (event) {
|
||||
if (event.which === 13) {
|
||||
scope.$apply(() => scope.$eval(attrs.onEnter, { event }))
|
||||
return event.preventDefault()
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
*/
|
||||
import App from '../base'
|
||||
|
||||
export default App.directive('rightClick', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind('contextmenu', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return scope.$eval(attrs.rightClick)
|
||||
})
|
||||
},
|
||||
}))
|
||||
export default App.directive('rightClick', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind('contextmenu', function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return scope.$eval(attrs.rightClick)
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,47 +13,49 @@ import App from '../base'
|
|||
|
||||
export default App.directive('updateScrollBottomOn', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs, ctrls) {
|
||||
// We keep the offset from the bottom fixed whenever the event fires
|
||||
//
|
||||
// ^ | ^
|
||||
// | | | scrollTop
|
||||
// | | v
|
||||
// | |-----------
|
||||
// | | ^
|
||||
// | | |
|
||||
// | | | clientHeight (viewable area)
|
||||
// | | |
|
||||
// | | |
|
||||
// | | v
|
||||
// | |-----------
|
||||
// | | ^
|
||||
// | | | scrollBottom
|
||||
// v | v
|
||||
// \
|
||||
// scrollHeight
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs, ctrls) {
|
||||
// We keep the offset from the bottom fixed whenever the event fires
|
||||
//
|
||||
// ^ | ^
|
||||
// | | | scrollTop
|
||||
// | | v
|
||||
// | |-----------
|
||||
// | | ^
|
||||
// | | |
|
||||
// | | | clientHeight (viewable area)
|
||||
// | | |
|
||||
// | | |
|
||||
// | | v
|
||||
// | |-----------
|
||||
// | | ^
|
||||
// | | | scrollBottom
|
||||
// v | v
|
||||
// \
|
||||
// scrollHeight
|
||||
|
||||
let scrollBottom = 0
|
||||
element.on(
|
||||
'scroll',
|
||||
e =>
|
||||
(scrollBottom =
|
||||
element[0].scrollHeight -
|
||||
element[0].scrollTop -
|
||||
element[0].clientHeight)
|
||||
)
|
||||
|
||||
return scope.$on(attrs.updateScrollBottomOn, () =>
|
||||
$timeout(
|
||||
() =>
|
||||
element.scrollTop(
|
||||
element[0].scrollHeight - element[0].clientHeight - scrollBottom
|
||||
),
|
||||
0
|
||||
let scrollBottom = 0
|
||||
element.on(
|
||||
'scroll',
|
||||
e =>
|
||||
(scrollBottom =
|
||||
element[0].scrollHeight -
|
||||
element[0].scrollTop -
|
||||
element[0].clientHeight)
|
||||
)
|
||||
)
|
||||
},
|
||||
}),
|
||||
|
||||
return scope.$on(attrs.updateScrollBottomOn, () =>
|
||||
$timeout(
|
||||
() =>
|
||||
element.scrollTop(
|
||||
element[0].scrollHeight - element[0].clientHeight - scrollBottom
|
||||
),
|
||||
0
|
||||
)
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -11,88 +11,98 @@
|
|||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../base'
|
||||
App.directive('selectAllList', () => ({
|
||||
controller: [
|
||||
'$scope',
|
||||
function ($scope) {
|
||||
// Selecting or deselecting all should apply to all projects
|
||||
this.selectAll = () => $scope.$broadcast('select-all:select')
|
||||
App.directive('selectAllList', function () {
|
||||
return {
|
||||
controller: [
|
||||
'$scope',
|
||||
function ($scope) {
|
||||
// Selecting or deselecting all should apply to all projects
|
||||
this.selectAll = () => $scope.$broadcast('select-all:select')
|
||||
|
||||
this.deselectAll = () => $scope.$broadcast('select-all:deselect')
|
||||
this.deselectAll = () => $scope.$broadcast('select-all:deselect')
|
||||
|
||||
this.clearSelectAllState = () => $scope.$broadcast('select-all:clear')
|
||||
this.clearSelectAllState = () => $scope.$broadcast('select-all:clear')
|
||||
},
|
||||
],
|
||||
link(scope, element, attrs) {},
|
||||
}
|
||||
})
|
||||
|
||||
App.directive('selectAll', function () {
|
||||
return {
|
||||
require: '^selectAllList',
|
||||
link(scope, element, attrs, selectAllListController) {
|
||||
scope.$on('select-all:clear', () => element.prop('checked', false))
|
||||
|
||||
return element.change(function () {
|
||||
if (element.is(':checked')) {
|
||||
selectAllListController.selectAll()
|
||||
} else {
|
||||
selectAllListController.deselectAll()
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
],
|
||||
link(scope, element, attrs) {},
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
App.directive('selectAll', () => ({
|
||||
require: '^selectAllList',
|
||||
link(scope, element, attrs, selectAllListController) {
|
||||
scope.$on('select-all:clear', () => element.prop('checked', false))
|
||||
App.directive('selectIndividual', function () {
|
||||
return {
|
||||
require: '^selectAllList',
|
||||
scope: {
|
||||
ngModel: '=',
|
||||
},
|
||||
link(scope, element, attrs, selectAllListController) {
|
||||
let ignoreChanges = false
|
||||
|
||||
return element.change(function () {
|
||||
if (element.is(':checked')) {
|
||||
selectAllListController.selectAll()
|
||||
} else {
|
||||
selectAllListController.deselectAll()
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
App.directive('selectIndividual', () => ({
|
||||
require: '^selectAllList',
|
||||
scope: {
|
||||
ngModel: '=',
|
||||
},
|
||||
link(scope, element, attrs, selectAllListController) {
|
||||
let ignoreChanges = false
|
||||
|
||||
scope.$watch('ngModel', function (value) {
|
||||
if (value != null && !ignoreChanges) {
|
||||
return selectAllListController.clearSelectAllState()
|
||||
}
|
||||
})
|
||||
|
||||
scope.$on('select-all:select', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(() => (scope.ngModel = true))
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
|
||||
scope.$on('select-all:deselect', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(() => (scope.ngModel = false))
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
|
||||
return scope.$on('select-all:row-clicked', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(function () {
|
||||
scope.ngModel = !scope.ngModel
|
||||
if (!scope.ngModel) {
|
||||
scope.$watch('ngModel', function (value) {
|
||||
if (value != null && !ignoreChanges) {
|
||||
return selectAllListController.clearSelectAllState()
|
||||
}
|
||||
})
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
},
|
||||
}))
|
||||
|
||||
export default App.directive('selectRow', () => ({
|
||||
scope: true,
|
||||
link(scope, element, attrs) {
|
||||
return element.on('click', e => scope.$broadcast('select-all:row-clicked'))
|
||||
},
|
||||
}))
|
||||
scope.$on('select-all:select', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(() => (scope.ngModel = true))
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
|
||||
scope.$on('select-all:deselect', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(() => (scope.ngModel = false))
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
|
||||
return scope.$on('select-all:row-clicked', function () {
|
||||
if (element.prop('disabled')) {
|
||||
return
|
||||
}
|
||||
ignoreChanges = true
|
||||
scope.$apply(function () {
|
||||
scope.ngModel = !scope.ngModel
|
||||
if (!scope.ngModel) {
|
||||
return selectAllListController.clearSelectAllState()
|
||||
}
|
||||
})
|
||||
return (ignoreChanges = false)
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default App.directive('selectRow', function () {
|
||||
return {
|
||||
scope: true,
|
||||
link(scope, element, attrs) {
|
||||
return element.on('click', e =>
|
||||
scope.$broadcast('select-all:row-clicked')
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -9,16 +9,20 @@
|
|||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
import App from '../base'
|
||||
App.directive('stopPropagation', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind(attrs.stopPropagation, e => e.stopPropagation())
|
||||
},
|
||||
}))
|
||||
App.directive('stopPropagation', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind(attrs.stopPropagation, e => e.stopPropagation())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default App.directive('preventDefault', () => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind(attrs.preventDefault, e => e.preventDefault())
|
||||
},
|
||||
}))
|
||||
export default App.directive('preventDefault', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return element.bind(attrs.preventDefault, e => e.preventDefault())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,21 +12,23 @@ import App from '../base'
|
|||
|
||||
export default App.directive('videoPlayState', [
|
||||
'$parse',
|
||||
$parse => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
const videoDOMEl = element[0]
|
||||
return scope.$watch(
|
||||
() => $parse(attrs.videoPlayState)(scope),
|
||||
function (shouldPlay) {
|
||||
if (shouldPlay) {
|
||||
videoDOMEl.currentTime = 0
|
||||
return videoDOMEl.play()
|
||||
} else {
|
||||
return videoDOMEl.pause()
|
||||
function ($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
const videoDOMEl = element[0]
|
||||
return scope.$watch(
|
||||
() => $parse(attrs.videoPlayState)(scope),
|
||||
function (shouldPlay) {
|
||||
if (shouldPlay) {
|
||||
videoDOMEl.currentTime = 0
|
||||
return videoDOMEl.play()
|
||||
} else {
|
||||
return videoDOMEl.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -21,197 +21,199 @@ export default App.directive('layout', [
|
|||
'$parse',
|
||||
'$compile',
|
||||
'ide',
|
||||
($parse, $compile, ide) => ({
|
||||
compile() {
|
||||
return {
|
||||
pre(scope, element, attrs) {
|
||||
let customTogglerEl, spacingClosed, spacingOpen, state
|
||||
const name = attrs.layout
|
||||
function ($parse, $compile, ide) {
|
||||
return {
|
||||
compile() {
|
||||
return {
|
||||
pre(scope, element, attrs) {
|
||||
let customTogglerEl, spacingClosed, spacingOpen, state
|
||||
const name = attrs.layout
|
||||
|
||||
const { customTogglerPane } = attrs
|
||||
const { customTogglerMsgWhenOpen } = attrs
|
||||
const { customTogglerMsgWhenClosed } = attrs
|
||||
const hasCustomToggler =
|
||||
customTogglerPane != null &&
|
||||
customTogglerMsgWhenOpen != null &&
|
||||
customTogglerMsgWhenClosed != null
|
||||
const { customTogglerPane } = attrs
|
||||
const { customTogglerMsgWhenOpen } = attrs
|
||||
const { customTogglerMsgWhenClosed } = attrs
|
||||
const hasCustomToggler =
|
||||
customTogglerPane != null &&
|
||||
customTogglerMsgWhenOpen != null &&
|
||||
customTogglerMsgWhenClosed != null
|
||||
|
||||
if (attrs.spacingOpen != null) {
|
||||
spacingOpen = parseInt(attrs.spacingOpen, 10)
|
||||
} else {
|
||||
spacingOpen = 7
|
||||
}
|
||||
|
||||
if (attrs.spacingClosed != null) {
|
||||
spacingClosed = parseInt(attrs.spacingClosed, 10)
|
||||
} else {
|
||||
spacingClosed = 7
|
||||
}
|
||||
|
||||
const options = {
|
||||
spacing_open: spacingOpen,
|
||||
spacing_closed: spacingClosed,
|
||||
slidable: false,
|
||||
enableCursorHotkey: false,
|
||||
onopen: pane => {
|
||||
return onPaneOpen(pane)
|
||||
},
|
||||
onclose: pane => {
|
||||
return onPaneClose(pane)
|
||||
},
|
||||
onresize: () => {
|
||||
return onInternalResize()
|
||||
},
|
||||
maskIframesOnResize: scope.$eval(
|
||||
attrs.maskIframesOnResize || 'false'
|
||||
),
|
||||
east: {
|
||||
size: scope.$eval(attrs.initialSizeEast),
|
||||
initClosed: scope.$eval(attrs.initClosedEast),
|
||||
},
|
||||
west: {
|
||||
size: scope.$eval(attrs.initialSizeWest),
|
||||
initClosed: scope.$eval(attrs.initClosedWest),
|
||||
},
|
||||
}
|
||||
|
||||
// Restore previously recorded state
|
||||
if ((state = ide.localStorage(`layout.${name}`)) != null) {
|
||||
if (state.east != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeEast == null ||
|
||||
(state.east.size >= attrs.minimumRestoreSizeEast &&
|
||||
!state.east.initClosed)
|
||||
) {
|
||||
options.east = state.east
|
||||
}
|
||||
options.east.initClosed = state.east.initClosed
|
||||
if (attrs.spacingOpen != null) {
|
||||
spacingOpen = parseInt(attrs.spacingOpen, 10)
|
||||
} else {
|
||||
spacingOpen = 7
|
||||
}
|
||||
if (state.west != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeWest == null ||
|
||||
(state.west.size >= attrs.minimumRestoreSizeWest &&
|
||||
!state.west.initClosed)
|
||||
) {
|
||||
options.west = state.west
|
||||
}
|
||||
// NOTE: disabled so that the file tree re-opens on page load
|
||||
// options.west.initClosed = state.west.initClosed
|
||||
}
|
||||
}
|
||||
|
||||
options.east.resizerCursor = 'ew-resize'
|
||||
options.west.resizerCursor = 'ew-resize'
|
||||
|
||||
function repositionControls() {
|
||||
state = layout.readState()
|
||||
if (state.east != null) {
|
||||
const controls = element.find('> .ui-layout-resizer-controls')
|
||||
if (state.east.initClosed) {
|
||||
return controls.hide()
|
||||
} else {
|
||||
controls.show()
|
||||
return controls.css({
|
||||
right: state.east.size,
|
||||
})
|
||||
}
|
||||
if (attrs.spacingClosed != null) {
|
||||
spacingClosed = parseInt(attrs.spacingClosed, 10)
|
||||
} else {
|
||||
spacingClosed = 7
|
||||
}
|
||||
}
|
||||
|
||||
function repositionCustomToggler() {
|
||||
if (customTogglerEl == null) {
|
||||
return
|
||||
const options = {
|
||||
spacing_open: spacingOpen,
|
||||
spacing_closed: spacingClosed,
|
||||
slidable: false,
|
||||
enableCursorHotkey: false,
|
||||
onopen: pane => {
|
||||
return onPaneOpen(pane)
|
||||
},
|
||||
onclose: pane => {
|
||||
return onPaneClose(pane)
|
||||
},
|
||||
onresize: () => {
|
||||
return onInternalResize()
|
||||
},
|
||||
maskIframesOnResize: scope.$eval(
|
||||
attrs.maskIframesOnResize || 'false'
|
||||
),
|
||||
east: {
|
||||
size: scope.$eval(attrs.initialSizeEast),
|
||||
initClosed: scope.$eval(attrs.initClosedEast),
|
||||
},
|
||||
west: {
|
||||
size: scope.$eval(attrs.initialSizeWest),
|
||||
initClosed: scope.$eval(attrs.initClosedWest),
|
||||
},
|
||||
}
|
||||
state = layout.readState()
|
||||
const positionAnchor =
|
||||
customTogglerPane === 'east' ? 'right' : 'left'
|
||||
const paneState = state[customTogglerPane]
|
||||
if (paneState != null) {
|
||||
return customTogglerEl.css(
|
||||
positionAnchor,
|
||||
paneState.initClosed ? 0 : paneState.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function resetOpenStates() {
|
||||
state = layout.readState()
|
||||
if (attrs.openEast != null && state.east != null) {
|
||||
const openEast = $parse(attrs.openEast)
|
||||
return openEast.assign(scope, !state.east.initClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// Someone moved the resizer
|
||||
function onInternalResize() {
|
||||
state = layout.readState()
|
||||
scope.$broadcast(`layout:${name}:resize`, state)
|
||||
repositionControls()
|
||||
if (hasCustomToggler) {
|
||||
repositionCustomToggler()
|
||||
}
|
||||
return resetOpenStates()
|
||||
}
|
||||
|
||||
let oldWidth = element.width()
|
||||
// Something resized our parent element
|
||||
const onExternalResize = function () {
|
||||
if (
|
||||
attrs.resizeProportionally != null &&
|
||||
scope.$eval(attrs.resizeProportionally)
|
||||
) {
|
||||
const eastState = layout.readState().east
|
||||
if (eastState != null) {
|
||||
const currentWidth = element.width()
|
||||
if (currentWidth > 0) {
|
||||
const newInternalWidth =
|
||||
(eastState.size / oldWidth) * currentWidth
|
||||
oldWidth = currentWidth
|
||||
layout.sizePane('east', newInternalWidth)
|
||||
// Restore previously recorded state
|
||||
if ((state = ide.localStorage(`layout.${name}`)) != null) {
|
||||
if (state.east != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeEast == null ||
|
||||
(state.east.size >= attrs.minimumRestoreSizeEast &&
|
||||
!state.east.initClosed)
|
||||
) {
|
||||
options.east = state.east
|
||||
}
|
||||
options.east.initClosed = state.east.initClosed
|
||||
}
|
||||
if (state.west != null) {
|
||||
if (
|
||||
attrs.minimumRestoreSizeWest == null ||
|
||||
(state.west.size >= attrs.minimumRestoreSizeWest &&
|
||||
!state.west.initClosed)
|
||||
) {
|
||||
options.west = state.west
|
||||
}
|
||||
// NOTE: disabled so that the file tree re-opens on page load
|
||||
// options.west.initClosed = state.west.initClosed
|
||||
}
|
||||
}
|
||||
|
||||
options.east.resizerCursor = 'ew-resize'
|
||||
options.west.resizerCursor = 'ew-resize'
|
||||
|
||||
function repositionControls() {
|
||||
state = layout.readState()
|
||||
if (state.east != null) {
|
||||
const controls = element.find('> .ui-layout-resizer-controls')
|
||||
if (state.east.initClosed) {
|
||||
return controls.hide()
|
||||
} else {
|
||||
controls.show()
|
||||
return controls.css({
|
||||
right: state.east.size,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function repositionCustomToggler() {
|
||||
if (customTogglerEl == null) {
|
||||
return
|
||||
}
|
||||
state = layout.readState()
|
||||
const positionAnchor =
|
||||
customTogglerPane === 'east' ? 'right' : 'left'
|
||||
const paneState = state[customTogglerPane]
|
||||
if (paneState != null) {
|
||||
return customTogglerEl.css(
|
||||
positionAnchor,
|
||||
paneState.initClosed ? 0 : paneState.size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ide.$timeout(() => {
|
||||
layout.resizeAll()
|
||||
})
|
||||
}
|
||||
|
||||
const layout = element.layout(options)
|
||||
layout.resizeAll()
|
||||
|
||||
if (attrs.resizeOn != null) {
|
||||
for (const event of Array.from(attrs.resizeOn.split(','))) {
|
||||
scope.$on(event, () => onExternalResize())
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCustomToggler) {
|
||||
state = layout.readState()
|
||||
const customTogglerScope = scope.$new()
|
||||
|
||||
customTogglerScope.isOpen = true
|
||||
customTogglerScope.isVisible = true
|
||||
|
||||
if (
|
||||
(state[customTogglerPane] != null
|
||||
? state[customTogglerPane].initClosed
|
||||
: undefined) === true
|
||||
) {
|
||||
customTogglerScope.isOpen = false
|
||||
function resetOpenStates() {
|
||||
state = layout.readState()
|
||||
if (attrs.openEast != null && state.east != null) {
|
||||
const openEast = $parse(attrs.openEast)
|
||||
return openEast.assign(scope, !state.east.initClosed)
|
||||
}
|
||||
}
|
||||
|
||||
customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
|
||||
customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
|
||||
|
||||
customTogglerScope.tooltipPlacement =
|
||||
customTogglerPane === 'east' ? 'left' : 'right'
|
||||
customTogglerScope.handleClick = function () {
|
||||
layout.toggle(customTogglerPane)
|
||||
return repositionCustomToggler()
|
||||
// Someone moved the resizer
|
||||
function onInternalResize() {
|
||||
state = layout.readState()
|
||||
scope.$broadcast(`layout:${name}:resize`, state)
|
||||
repositionControls()
|
||||
if (hasCustomToggler) {
|
||||
repositionCustomToggler()
|
||||
}
|
||||
return resetOpenStates()
|
||||
}
|
||||
customTogglerEl = $compile(`\
|
||||
|
||||
let oldWidth = element.width()
|
||||
// Something resized our parent element
|
||||
const onExternalResize = function () {
|
||||
if (
|
||||
attrs.resizeProportionally != null &&
|
||||
scope.$eval(attrs.resizeProportionally)
|
||||
) {
|
||||
const eastState = layout.readState().east
|
||||
if (eastState != null) {
|
||||
const currentWidth = element.width()
|
||||
if (currentWidth > 0) {
|
||||
const newInternalWidth =
|
||||
(eastState.size / oldWidth) * currentWidth
|
||||
oldWidth = currentWidth
|
||||
layout.sizePane('east', newInternalWidth)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ide.$timeout(() => {
|
||||
layout.resizeAll()
|
||||
})
|
||||
}
|
||||
|
||||
const layout = element.layout(options)
|
||||
layout.resizeAll()
|
||||
|
||||
if (attrs.resizeOn != null) {
|
||||
for (const event of Array.from(attrs.resizeOn.split(','))) {
|
||||
scope.$on(event, () => onExternalResize())
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCustomToggler) {
|
||||
state = layout.readState()
|
||||
const customTogglerScope = scope.$new()
|
||||
|
||||
customTogglerScope.isOpen = true
|
||||
customTogglerScope.isVisible = true
|
||||
|
||||
if (
|
||||
(state[customTogglerPane] != null
|
||||
? state[customTogglerPane].initClosed
|
||||
: undefined) === true
|
||||
) {
|
||||
customTogglerScope.isOpen = false
|
||||
}
|
||||
|
||||
customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
|
||||
customTogglerScope.tooltipMsgWhenClosed =
|
||||
customTogglerMsgWhenClosed
|
||||
|
||||
customTogglerScope.tooltipPlacement =
|
||||
customTogglerPane === 'east' ? 'left' : 'right'
|
||||
customTogglerScope.handleClick = function () {
|
||||
layout.toggle(customTogglerPane)
|
||||
return repositionCustomToggler()
|
||||
}
|
||||
customTogglerEl = $compile(`\
|
||||
<a href \
|
||||
ng-show=\"isVisible\" \
|
||||
class=\"custom-toggler ${`custom-toggler-${customTogglerPane}`}\" \
|
||||
|
@ -221,100 +223,101 @@ tooltip-placement=\"{{ tooltipPlacement }}\" \
|
|||
ng-click=\"handleClick()\" \
|
||||
aria-label=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\">\
|
||||
`)(customTogglerScope)
|
||||
element.append(customTogglerEl)
|
||||
}
|
||||
|
||||
function onPaneOpen(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
element.append(customTogglerEl)
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = true))
|
||||
}
|
||||
|
||||
function onPaneClose(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
function onPaneOpen(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = true))
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = false))
|
||||
}
|
||||
|
||||
// Ensure editor resizes after loading. This is to handle the case where
|
||||
// the window has been resized while the editor is loading
|
||||
scope.$on('editor:loaded', () => {
|
||||
ide.$timeout(() => layout.resizeAll())
|
||||
})
|
||||
function onPaneClose(pane) {
|
||||
if (!hasCustomToggler || pane !== customTogglerPane) {
|
||||
return
|
||||
}
|
||||
return customTogglerEl
|
||||
.scope()
|
||||
.$applyAsync(() => (customTogglerEl.scope().isOpen = false))
|
||||
}
|
||||
|
||||
// Save state when exiting
|
||||
$(window).unload(() => {
|
||||
// Save only the state properties for the current layout, ignoring sublayouts inside it.
|
||||
// If we save sublayouts state (`children`), the layout library will use it when
|
||||
// initializing. This raises errors when the sublayout elements aren't available (due to
|
||||
// being loaded at init or just not existing for the current project/user).
|
||||
const stateToSave = _.mapValues(layout.readState(), pane =>
|
||||
_.omit(pane, 'children')
|
||||
)
|
||||
ide.localStorage(`layout.${name}`, stateToSave)
|
||||
})
|
||||
// Ensure editor resizes after loading. This is to handle the case where
|
||||
// the window has been resized while the editor is loading
|
||||
scope.$on('editor:loaded', () => {
|
||||
ide.$timeout(() => layout.resizeAll())
|
||||
})
|
||||
|
||||
if (attrs.openEast != null) {
|
||||
scope.$watch(attrs.openEast, function (value, oldValue) {
|
||||
if (value != null && value !== oldValue) {
|
||||
if (value) {
|
||||
layout.open('east')
|
||||
} else {
|
||||
layout.close('east')
|
||||
// Save state when exiting
|
||||
$(window).unload(() => {
|
||||
// Save only the state properties for the current layout, ignoring sublayouts inside it.
|
||||
// If we save sublayouts state (`children`), the layout library will use it when
|
||||
// initializing. This raises errors when the sublayout elements aren't available (due to
|
||||
// being loaded at init or just not existing for the current project/user).
|
||||
const stateToSave = _.mapValues(layout.readState(), pane =>
|
||||
_.omit(pane, 'children')
|
||||
)
|
||||
ide.localStorage(`layout.${name}`, stateToSave)
|
||||
})
|
||||
|
||||
if (attrs.openEast != null) {
|
||||
scope.$watch(attrs.openEast, function (value, oldValue) {
|
||||
if (value != null && value !== oldValue) {
|
||||
if (value) {
|
||||
layout.open('east')
|
||||
} else {
|
||||
layout.close('east')
|
||||
}
|
||||
if (hasCustomToggler && customTogglerPane === 'east') {
|
||||
repositionCustomToggler()
|
||||
customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = value
|
||||
})
|
||||
}
|
||||
}
|
||||
if (hasCustomToggler && customTogglerPane === 'east') {
|
||||
repositionCustomToggler()
|
||||
customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = value
|
||||
return setTimeout(() => scope.$digest(), 0)
|
||||
})
|
||||
}
|
||||
|
||||
if (attrs.allowOverflowOn != null) {
|
||||
const overflowPane = scope.$eval(attrs.allowOverflowOn)
|
||||
const overflowPaneEl = layout.panes[overflowPane]
|
||||
// Set the panel as overflowing (gives it higher z-index and sets overflow rules)
|
||||
layout.allowOverflow(overflowPane)
|
||||
// Read the given z-index value and increment it, so that it's higher than synctex controls.
|
||||
const overflowPaneZVal = overflowPaneEl.zIndex()
|
||||
overflowPaneEl.css('z-index', overflowPaneZVal + 1)
|
||||
}
|
||||
|
||||
resetOpenStates()
|
||||
onInternalResize()
|
||||
|
||||
if (attrs.layoutDisabled != null) {
|
||||
return scope.$watch(attrs.layoutDisabled, function (value) {
|
||||
if (value) {
|
||||
layout.hide('east')
|
||||
} else {
|
||||
layout.show('east')
|
||||
}
|
||||
if (hasCustomToggler) {
|
||||
return customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = !value
|
||||
return (customTogglerEl.scope().isVisible = !value)
|
||||
})
|
||||
}
|
||||
}
|
||||
return setTimeout(() => scope.$digest(), 0)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
if (attrs.allowOverflowOn != null) {
|
||||
const overflowPane = scope.$eval(attrs.allowOverflowOn)
|
||||
const overflowPaneEl = layout.panes[overflowPane]
|
||||
// Set the panel as overflowing (gives it higher z-index and sets overflow rules)
|
||||
layout.allowOverflow(overflowPane)
|
||||
// Read the given z-index value and increment it, so that it's higher than synctex controls.
|
||||
const overflowPaneZVal = overflowPaneEl.zIndex()
|
||||
overflowPaneEl.css('z-index', overflowPaneZVal + 1)
|
||||
}
|
||||
|
||||
resetOpenStates()
|
||||
onInternalResize()
|
||||
|
||||
if (attrs.layoutDisabled != null) {
|
||||
return scope.$watch(attrs.layoutDisabled, function (value) {
|
||||
if (value) {
|
||||
layout.hide('east')
|
||||
} else {
|
||||
layout.show('east')
|
||||
}
|
||||
if (hasCustomToggler) {
|
||||
return customTogglerEl.scope().$applyAsync(function () {
|
||||
customTogglerEl.scope().isOpen = !value
|
||||
return (customTogglerEl.scope().isVisible = !value)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
post(scope, element, attrs) {
|
||||
const name = attrs.layout
|
||||
const state = element.layout().readState()
|
||||
return scope.$broadcast(`layout:${name}:linked`, state)
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
post(scope, element, attrs) {
|
||||
const name = attrs.layout
|
||||
const state = element.layout().readState()
|
||||
return scope.$broadcast(`layout:${name}:linked`, state)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -12,10 +12,12 @@
|
|||
import App from '../../base'
|
||||
import SafePath from './SafePath'
|
||||
|
||||
export default App.directive('validFile', () => ({
|
||||
require: 'ngModel',
|
||||
link(scope, element, attrs, ngModelCtrl) {
|
||||
return (ngModelCtrl.$validators.validFile = filename =>
|
||||
SafePath.isCleanFilename(filename))
|
||||
},
|
||||
}))
|
||||
export default App.directive('validFile', function () {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link(scope, element, attrs, ngModelCtrl) {
|
||||
return (ngModelCtrl.$validators.validFile = filename =>
|
||||
SafePath.isCleanFilename(filename))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,131 +3,137 @@ import App from '../../base'
|
|||
export default App.directive('verticalResizablePanes', [
|
||||
'localStorage',
|
||||
'ide',
|
||||
(localStorage, ide) => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
const name = attrs.verticalResizablePanes
|
||||
const minSize = scope.$eval(attrs.verticalResizablePanesMinSize)
|
||||
const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize)
|
||||
const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize)
|
||||
let storedSize = null
|
||||
let manualResizeIncoming = false
|
||||
function (localStorage, ide) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
const name = attrs.verticalResizablePanes
|
||||
const minSize = scope.$eval(attrs.verticalResizablePanesMinSize)
|
||||
const maxSize = scope.$eval(attrs.verticalResizablePanesMaxSize)
|
||||
const defaultSize = scope.$eval(attrs.verticalResizablePanesDefaultSize)
|
||||
let storedSize = null
|
||||
let manualResizeIncoming = false
|
||||
|
||||
if (name) {
|
||||
const storageKey = `vertical-resizable:${name}:south-size`
|
||||
storedSize = localStorage(storageKey)
|
||||
$(window).unload(() => {
|
||||
if (storedSize) {
|
||||
localStorage(storageKey, storedSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const layoutOptions = {
|
||||
center: {
|
||||
paneSelector: '[vertical-resizable-top]',
|
||||
paneClass: 'vertical-resizable-top',
|
||||
size: 'auto',
|
||||
},
|
||||
south: {
|
||||
paneSelector: '[vertical-resizable-bottom]',
|
||||
paneClass: 'vertical-resizable-bottom',
|
||||
resizerClass: 'vertical-resizable-resizer',
|
||||
resizerCursor: 'ns-resize',
|
||||
size: 'auto',
|
||||
resizable: true,
|
||||
closable: false,
|
||||
slidable: false,
|
||||
spacing_open: 6,
|
||||
spacing_closed: 6,
|
||||
maxSize: '75%',
|
||||
},
|
||||
}
|
||||
|
||||
const toggledExternally = attrs.verticalResizablePanesToggledExternallyOn
|
||||
const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
|
||||
const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
|
||||
const resizeOn = attrs.verticalResizablePanesResizeOn
|
||||
const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
|
||||
|
||||
function enableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.removeClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
function disableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.addClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
manualResizeIncoming = true
|
||||
}
|
||||
|
||||
function handleResize(paneName, paneEl, paneState) {
|
||||
if (manualResizeIncoming) {
|
||||
storedSize = paneState.size
|
||||
}
|
||||
manualResizeIncoming = false
|
||||
}
|
||||
|
||||
if (toggledExternally) {
|
||||
scope.$on(toggledExternally, (e, open) => {
|
||||
if (open) {
|
||||
enableResizer()
|
||||
layoutHandle.sizePane('south', storedSize ?? defaultSize ?? 'auto')
|
||||
} else {
|
||||
disableResizer()
|
||||
layoutHandle.sizePane('south', 'auto')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hiddenExternally) {
|
||||
ide.$scope.$on(hiddenExternally, (e, open) => {
|
||||
if (open) {
|
||||
layoutHandle.show('south')
|
||||
} else {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (resizeOn) {
|
||||
ide.$scope.$on(resizeOn, () => {
|
||||
ide.$timeout(() => {
|
||||
layoutHandle.resizeAll()
|
||||
if (name) {
|
||||
const storageKey = `vertical-resizable:${name}:south-size`
|
||||
storedSize = localStorage(storageKey)
|
||||
$(window).unload(() => {
|
||||
if (storedSize) {
|
||||
localStorage(storageKey, storedSize)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
layoutOptions.south.maxSize = maxSize
|
||||
}
|
||||
const layoutOptions = {
|
||||
center: {
|
||||
paneSelector: '[vertical-resizable-top]',
|
||||
paneClass: 'vertical-resizable-top',
|
||||
size: 'auto',
|
||||
},
|
||||
south: {
|
||||
paneSelector: '[vertical-resizable-bottom]',
|
||||
paneClass: 'vertical-resizable-bottom',
|
||||
resizerClass: 'vertical-resizable-resizer',
|
||||
resizerCursor: 'ns-resize',
|
||||
size: 'auto',
|
||||
resizable: true,
|
||||
closable: false,
|
||||
slidable: false,
|
||||
spacing_open: 6,
|
||||
spacing_closed: 6,
|
||||
maxSize: '75%',
|
||||
},
|
||||
}
|
||||
|
||||
if (minSize) {
|
||||
layoutOptions.south.minSize = minSize
|
||||
}
|
||||
const toggledExternally =
|
||||
attrs.verticalResizablePanesToggledExternallyOn
|
||||
const hiddenExternally = attrs.verticalResizablePanesHiddenExternallyOn
|
||||
const hiddenInitially = attrs.verticalResizablePanesHiddenInitially
|
||||
const resizeOn = attrs.verticalResizablePanesResizeOn
|
||||
const resizerDisabledClass = `${layoutOptions.south.resizerClass}-disabled`
|
||||
|
||||
if (defaultSize) {
|
||||
layoutOptions.south.size = defaultSize
|
||||
}
|
||||
function enableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.removeClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
// The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
|
||||
// the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
|
||||
// listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
|
||||
// get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
|
||||
// The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
|
||||
// that it was user-initiated (therefore, storing the value).
|
||||
layoutOptions.south.ondrag_end = handleDragEnd
|
||||
layoutOptions.south.onresize = handleResize
|
||||
function disableResizer() {
|
||||
if (layoutHandle.resizers && layoutHandle.resizers.south) {
|
||||
layoutHandle.resizers.south.addClass(resizerDisabledClass)
|
||||
}
|
||||
}
|
||||
|
||||
const layoutHandle = element.layout(layoutOptions)
|
||||
if (hiddenInitially === 'true') {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
},
|
||||
}),
|
||||
function handleDragEnd() {
|
||||
manualResizeIncoming = true
|
||||
}
|
||||
|
||||
function handleResize(paneName, paneEl, paneState) {
|
||||
if (manualResizeIncoming) {
|
||||
storedSize = paneState.size
|
||||
}
|
||||
manualResizeIncoming = false
|
||||
}
|
||||
|
||||
if (toggledExternally) {
|
||||
scope.$on(toggledExternally, (e, open) => {
|
||||
if (open) {
|
||||
enableResizer()
|
||||
layoutHandle.sizePane(
|
||||
'south',
|
||||
storedSize ?? defaultSize ?? 'auto'
|
||||
)
|
||||
} else {
|
||||
disableResizer()
|
||||
layoutHandle.sizePane('south', 'auto')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hiddenExternally) {
|
||||
ide.$scope.$on(hiddenExternally, (e, open) => {
|
||||
if (open) {
|
||||
layoutHandle.show('south')
|
||||
} else {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (resizeOn) {
|
||||
ide.$scope.$on(resizeOn, () => {
|
||||
ide.$timeout(() => {
|
||||
layoutHandle.resizeAll()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (maxSize) {
|
||||
layoutOptions.south.maxSize = maxSize
|
||||
}
|
||||
|
||||
if (minSize) {
|
||||
layoutOptions.south.minSize = minSize
|
||||
}
|
||||
|
||||
if (defaultSize) {
|
||||
layoutOptions.south.size = defaultSize
|
||||
}
|
||||
|
||||
// The `drag` event fires only when the user manually resizes the panes; the `resize` event fires even when
|
||||
// the layout library internally resizes itself. In order to get explicit user-initiated resizes, we need to
|
||||
// listen to `drag` events. However, when the `drag` event fires, the panes aren't yet finished sizing so we
|
||||
// get the pane size *before* the resize happens. We do get the correct size in the next `resize` event.
|
||||
// The solution to work around this is to set up a flag in `drag` events which tells the next `resize` event
|
||||
// that it was user-initiated (therefore, storing the value).
|
||||
layoutOptions.south.ondrag_end = handleDragEnd
|
||||
layoutOptions.south.onresize = handleResize
|
||||
|
||||
const layoutHandle = element.layout(layoutOptions)
|
||||
if (hiddenInitially === 'true') {
|
||||
layoutHandle.hide('south')
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('formattingButtons', () => ({
|
||||
scope: {
|
||||
buttons: '=',
|
||||
opening: '=',
|
||||
isFullscreenEditor: '=',
|
||||
},
|
||||
export default App.directive('formattingButtons', function () {
|
||||
return {
|
||||
scope: {
|
||||
buttons: '=',
|
||||
opening: '=',
|
||||
isFullscreenEditor: '=',
|
||||
},
|
||||
|
||||
link(scope, element, attrs) {
|
||||
scope.showMore = false
|
||||
scope.shownButtons = scope.buttons
|
||||
scope.overflowedButtons = []
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.showMore = false
|
||||
scope.shownButtons = scope.buttons
|
||||
scope.overflowedButtons = []
|
||||
},
|
||||
|
||||
templateUrl: 'formattingButtonsTpl',
|
||||
}))
|
||||
templateUrl: 'formattingButtonsTpl',
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('toggleSwitch', () => ({
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
description: '@',
|
||||
labelFalse: '@',
|
||||
labelTrue: '@',
|
||||
ngModel: '=',
|
||||
},
|
||||
template: `\
|
||||
export default App.directive('toggleSwitch', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
description: '@',
|
||||
labelFalse: '@',
|
||||
labelTrue: '@',
|
||||
ngModel: '=',
|
||||
},
|
||||
template: `\
|
||||
<fieldset class="toggle-switch">
|
||||
<legend class="sr-only">{{description}}</legend>
|
||||
|
||||
|
@ -33,4 +34,5 @@ export default App.directive('toggleSwitch', () => ({
|
|||
<label for="toggle-switch-true-{{$id}}" class="toggle-switch-label"><span>{{labelTrue}}</span></label>
|
||||
</fieldset>\
|
||||
`,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('historyDraggableBoundary', () => ({
|
||||
scope: {
|
||||
historyDraggableBoundary: '@',
|
||||
historyDraggableBoundaryOnDragStart: '&',
|
||||
historyDraggableBoundaryOnDragStop: '&',
|
||||
},
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
element.data('selectionBoundary', {
|
||||
boundary: scope.historyDraggableBoundary,
|
||||
})
|
||||
element.draggable({
|
||||
axis: 'y',
|
||||
opacity: false,
|
||||
helper: 'clone',
|
||||
revert: true,
|
||||
scroll: true,
|
||||
cursor: 'row-resize',
|
||||
start(e, ui) {
|
||||
ui.helper.data('wasProperlyDropped', false)
|
||||
scope.historyDraggableBoundaryOnDragStart()
|
||||
},
|
||||
stop(e, ui) {
|
||||
scope.historyDraggableBoundaryOnDragStop({
|
||||
isValidDrop: ui.helper.data('wasProperlyDropped'),
|
||||
boundary: scope.historyDraggableBoundary,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}))
|
||||
export default App.directive('historyDraggableBoundary', function () {
|
||||
return {
|
||||
scope: {
|
||||
historyDraggableBoundary: '@',
|
||||
historyDraggableBoundaryOnDragStart: '&',
|
||||
historyDraggableBoundaryOnDragStop: '&',
|
||||
},
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
element.data('selectionBoundary', {
|
||||
boundary: scope.historyDraggableBoundary,
|
||||
})
|
||||
element.draggable({
|
||||
axis: 'y',
|
||||
opacity: false,
|
||||
helper: 'clone',
|
||||
revert: true,
|
||||
scroll: true,
|
||||
cursor: 'row-resize',
|
||||
start(e, ui) {
|
||||
ui.helper.data('wasProperlyDropped', false)
|
||||
scope.historyDraggableBoundaryOnDragStart()
|
||||
},
|
||||
stop(e, ui) {
|
||||
scope.historyDraggableBoundaryOnDragStop({
|
||||
isValidDrop: ui.helper.data('wasProperlyDropped'),
|
||||
boundary: scope.historyDraggableBoundary,
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,24 +1,28 @@
|
|||
import App from '../../../base'
|
||||
|
||||
export default App.directive('historyDroppableArea', () => ({
|
||||
scope: {
|
||||
historyDroppableAreaOnDrop: '&',
|
||||
historyDroppableAreaOnOver: '&',
|
||||
historyDroppableAreaOnOut: '&',
|
||||
},
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
element.droppable({
|
||||
accept: e => '.history-entry-toV-handle, .history-entry-fromV-handle',
|
||||
drop: (e, ui) => {
|
||||
const draggedBoundary = ui.draggable.data('selectionBoundary').boundary
|
||||
ui.helper.data('wasProperlyDropped', true)
|
||||
scope.historyDroppableAreaOnDrop({ boundary: draggedBoundary })
|
||||
},
|
||||
over: (e, ui) => {
|
||||
const draggedBoundary = ui.draggable.data('selectionBoundary').boundary
|
||||
scope.historyDroppableAreaOnOver({ boundary: draggedBoundary })
|
||||
},
|
||||
})
|
||||
},
|
||||
}))
|
||||
export default App.directive('historyDroppableArea', function () {
|
||||
return {
|
||||
scope: {
|
||||
historyDroppableAreaOnDrop: '&',
|
||||
historyDroppableAreaOnOver: '&',
|
||||
historyDroppableAreaOnOut: '&',
|
||||
},
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
element.droppable({
|
||||
accept: e => '.history-entry-toV-handle, .history-entry-fromV-handle',
|
||||
drop: (e, ui) => {
|
||||
const draggedBoundary =
|
||||
ui.draggable.data('selectionBoundary').boundary
|
||||
ui.helper.data('wasProperlyDropped', true)
|
||||
scope.historyDroppableAreaOnDrop({ boundary: draggedBoundary })
|
||||
},
|
||||
over: (e, ui) => {
|
||||
const draggedBoundary =
|
||||
ui.draggable.data('selectionBoundary').boundary
|
||||
scope.historyDroppableAreaOnOver({ boundary: draggedBoundary })
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -11,43 +11,45 @@
|
|||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.directive('infiniteScroll', () => ({
|
||||
link(scope, element, attrs, ctrl) {
|
||||
const innerElement = element.find('.infinite-scroll-inner')
|
||||
element.css({ 'overflow-y': 'auto' })
|
||||
export default App.directive('infiniteScroll', function () {
|
||||
return {
|
||||
link(scope, element, attrs, ctrl) {
|
||||
const innerElement = element.find('.infinite-scroll-inner')
|
||||
element.css({ 'overflow-y': 'auto' })
|
||||
|
||||
const atEndOfListView = function () {
|
||||
if (attrs.infiniteScrollUpwards != null) {
|
||||
return atTopOfListView()
|
||||
} else {
|
||||
return atBottomOfListView()
|
||||
const atEndOfListView = function () {
|
||||
if (attrs.infiniteScrollUpwards != null) {
|
||||
return atTopOfListView()
|
||||
} else {
|
||||
return atBottomOfListView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const atTopOfListView = () => element.scrollTop() < 30
|
||||
const atTopOfListView = () => element.scrollTop() < 30
|
||||
|
||||
const atBottomOfListView = () =>
|
||||
element.scrollTop() + element.height() >= innerElement.height() - 30
|
||||
const atBottomOfListView = () =>
|
||||
element.scrollTop() + element.height() >= innerElement.height() - 30
|
||||
|
||||
const listShorterThanContainer = () =>
|
||||
element.height() > innerElement.height()
|
||||
const listShorterThanContainer = () =>
|
||||
element.height() > innerElement.height()
|
||||
|
||||
function loadUntilFull() {
|
||||
if (
|
||||
(listShorterThanContainer() || atEndOfListView()) &&
|
||||
!scope.$eval(attrs.infiniteScrollDisabled)
|
||||
) {
|
||||
const promise = scope.$eval(attrs.infiniteScroll)
|
||||
return promise.then(() => setTimeout(() => loadUntilFull(), 0))
|
||||
function loadUntilFull() {
|
||||
if (
|
||||
(listShorterThanContainer() || atEndOfListView()) &&
|
||||
!scope.$eval(attrs.infiniteScrollDisabled)
|
||||
) {
|
||||
const promise = scope.$eval(attrs.infiniteScroll)
|
||||
return promise.then(() => setTimeout(() => loadUntilFull(), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.on('scroll', event => loadUntilFull())
|
||||
element.on('scroll', event => loadUntilFull())
|
||||
|
||||
return scope.$watch(attrs.infiniteScrollInitialize, function (value) {
|
||||
if (value) {
|
||||
return loadUntilFull()
|
||||
}
|
||||
})
|
||||
},
|
||||
}))
|
||||
return scope.$watch(attrs.infiniteScrollInitialize, function (value) {
|
||||
if (value) {
|
||||
return loadUntilFull()
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,60 +1,62 @@
|
|||
import App from '../../../base'
|
||||
let content = ''
|
||||
App.directive('addCommentEntry', () => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'addCommentEntryTemplate',
|
||||
scope: {
|
||||
onStartNew: '&',
|
||||
onSubmit: '&',
|
||||
onCancel: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.state = {
|
||||
isAdding: false,
|
||||
content,
|
||||
}
|
||||
App.directive('addCommentEntry', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'addCommentEntryTemplate',
|
||||
scope: {
|
||||
onStartNew: '&',
|
||||
onSubmit: '&',
|
||||
onCancel: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.state = {
|
||||
isAdding: false,
|
||||
content,
|
||||
}
|
||||
|
||||
scope.$on('comment:start_adding', () => scope.startNewComment())
|
||||
scope.$on('$destroy', function () {
|
||||
content = scope.state.content
|
||||
})
|
||||
scope.$on('comment:start_adding', () => scope.startNewComment())
|
||||
scope.$on('$destroy', function () {
|
||||
content = scope.state.content
|
||||
})
|
||||
|
||||
scope.startNewComment = function () {
|
||||
scope.state.isAdding = true
|
||||
scope.onStartNew()
|
||||
setTimeout(() => scope.$broadcast('comment:new:open'))
|
||||
}
|
||||
scope.startNewComment = function () {
|
||||
scope.state.isAdding = true
|
||||
scope.onStartNew()
|
||||
setTimeout(() => scope.$broadcast('comment:new:open'))
|
||||
}
|
||||
|
||||
scope.cancelNewComment = function () {
|
||||
scope.state.isAdding = false
|
||||
scope.state.content = ''
|
||||
scope.onCancel()
|
||||
}
|
||||
scope.cancelNewComment = function () {
|
||||
scope.state.isAdding = false
|
||||
scope.state.content = ''
|
||||
scope.onCancel()
|
||||
}
|
||||
|
||||
const ignoreKeysInTextAreas = ['PageDown', 'PageUp']
|
||||
const ignoreKeysInTextAreas = ['PageDown', 'PageUp']
|
||||
|
||||
scope.handleCommentKeyDown = function (ev) {
|
||||
if (ignoreKeysInTextAreas.includes(ev.key)) {
|
||||
if (ev.target.closest('textarea')) {
|
||||
scope.handleCommentKeyDown = function (ev) {
|
||||
if (ignoreKeysInTextAreas.includes(ev.key)) {
|
||||
if (ev.target.closest('textarea')) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.handleCommentKeyPress = function (ev) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
if (scope.state.content.length > 0) {
|
||||
scope.submitNewComment()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.handleCommentKeyPress = function (ev) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
if (scope.state.content.length > 0) {
|
||||
scope.submitNewComment()
|
||||
}
|
||||
scope.submitNewComment = function (event) {
|
||||
scope.onSubmit({ content: scope.state.content })
|
||||
content = scope.state.content
|
||||
scope.state.isAdding = false
|
||||
scope.state.content = ''
|
||||
}
|
||||
}
|
||||
|
||||
scope.submitNewComment = function (event) {
|
||||
scope.onSubmit({ content: scope.state.content })
|
||||
content = scope.state.content
|
||||
scope.state.isAdding = false
|
||||
scope.state.content = ''
|
||||
}
|
||||
},
|
||||
}))
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,60 +13,62 @@ import App from '../../../base'
|
|||
|
||||
export default App.directive('aggregateChangeEntry', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'aggregateChangeEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
user: '=',
|
||||
permissions: '=',
|
||||
onAccept: '&',
|
||||
onReject: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 17
|
||||
scope.isDeletionCollapsed = true
|
||||
scope.isInsertionCollapsed = true
|
||||
scope.deletionNeedsCollapsing = false
|
||||
scope.insertionNeedsCollapsing = false
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'aggregateChangeEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
user: '=',
|
||||
permissions: '=',
|
||||
onAccept: '&',
|
||||
onReject: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 17
|
||||
scope.isDeletionCollapsed = true
|
||||
scope.isInsertionCollapsed = true
|
||||
scope.deletionNeedsCollapsing = false
|
||||
scope.insertionNeedsCollapsing = false
|
||||
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
}
|
||||
})
|
||||
|
||||
scope.toggleDeletionCollapse = function () {
|
||||
scope.isDeletionCollapsed = !scope.isDeletionCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
})
|
||||
|
||||
scope.toggleDeletionCollapse = function () {
|
||||
scope.isDeletionCollapsed = !scope.isDeletionCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
scope.toggleInsertionCollapse = function () {
|
||||
scope.isInsertionCollapsed = !scope.isInsertionCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.toggleInsertionCollapse = function () {
|
||||
scope.isInsertionCollapsed = !scope.isInsertionCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
scope.$watch(
|
||||
'entry.metadata.replaced_content.length',
|
||||
deletionContentLength =>
|
||||
(scope.deletionNeedsCollapsing =
|
||||
deletionContentLength > scope.contentLimit)
|
||||
)
|
||||
|
||||
scope.$watch(
|
||||
'entry.metadata.replaced_content.length',
|
||||
deletionContentLength =>
|
||||
(scope.deletionNeedsCollapsing =
|
||||
deletionContentLength > scope.contentLimit)
|
||||
)
|
||||
|
||||
return scope.$watch(
|
||||
'entry.content.length',
|
||||
insertionContentLength =>
|
||||
(scope.insertionNeedsCollapsing =
|
||||
insertionContentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}),
|
||||
return scope.$watch(
|
||||
'entry.content.length',
|
||||
insertionContentLength =>
|
||||
(scope.insertionNeedsCollapsing =
|
||||
insertionContentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -10,16 +10,18 @@
|
|||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.directive('bulkActionsEntry', () => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'bulkActionsEntryTemplate',
|
||||
scope: {
|
||||
onBulkAccept: '&',
|
||||
onBulkReject: '&',
|
||||
nEntries: '=',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.bulkAccept = () => scope.onBulkAccept()
|
||||
return (scope.bulkReject = () => scope.onBulkReject())
|
||||
},
|
||||
}))
|
||||
export default App.directive('bulkActionsEntry', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'bulkActionsEntryTemplate',
|
||||
scope: {
|
||||
onBulkAccept: '&',
|
||||
onBulkReject: '&',
|
||||
nEntries: '=',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.bulkAccept = () => scope.onBulkAccept()
|
||||
return (scope.bulkReject = () => scope.onBulkReject())
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,45 +13,47 @@ import App from '../../../base'
|
|||
|
||||
export default App.directive('changeEntry', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'changeEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
user: '=',
|
||||
permissions: '=',
|
||||
onAccept: '&',
|
||||
onReject: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 40
|
||||
scope.isCollapsed = true
|
||||
scope.needsCollapsing = false
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'changeEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
user: '=',
|
||||
permissions: '=',
|
||||
onAccept: '&',
|
||||
onReject: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 40
|
||||
scope.isCollapsed = true
|
||||
scope.needsCollapsing = false
|
||||
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-entry-description, .rp-entry-body, .rp-entry-action-icon i'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
}
|
||||
})
|
||||
|
||||
scope.toggleCollapse = function () {
|
||||
scope.isCollapsed = !scope.isCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
})
|
||||
|
||||
scope.toggleCollapse = function () {
|
||||
scope.isCollapsed = !scope.isCollapsed
|
||||
return $timeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
return scope.$watch(
|
||||
'entry.content.length',
|
||||
contentLength =>
|
||||
(scope.needsCollapsing = contentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}),
|
||||
return scope.$watch(
|
||||
'entry.content.length',
|
||||
contentLength =>
|
||||
(scope.needsCollapsing = contentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -13,83 +13,85 @@ import App from '../../../base'
|
|||
|
||||
export default App.directive('commentEntry', [
|
||||
'$timeout',
|
||||
$timeout => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'commentEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
threads: '=',
|
||||
permissions: '=',
|
||||
onResolve: '&',
|
||||
onReply: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onSaveEdit: '&',
|
||||
onDelete: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.state = { animating: false }
|
||||
function ($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'commentEntryTemplate',
|
||||
scope: {
|
||||
entry: '=',
|
||||
threads: '=',
|
||||
permissions: '=',
|
||||
onResolve: '&',
|
||||
onReply: '&',
|
||||
onIndicatorClick: '&',
|
||||
onMouseEnter: '&',
|
||||
onMouseLeave: '&',
|
||||
onSaveEdit: '&',
|
||||
onDelete: '&',
|
||||
onBodyClick: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.state = { animating: false }
|
||||
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
}
|
||||
})
|
||||
element.on('click', function (e) {
|
||||
if (
|
||||
$(e.target).is(
|
||||
'.rp-entry, .rp-comment-loaded, .rp-comment-content, .rp-comment-reply, .rp-entry-metadata'
|
||||
)
|
||||
) {
|
||||
return scope.onBodyClick()
|
||||
}
|
||||
})
|
||||
|
||||
scope.handleCommentReplyKeyPress = function (ev) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
if (scope.entry.replyContent.length > 0) {
|
||||
ev.target.blur()
|
||||
return scope.onReply()
|
||||
scope.handleCommentReplyKeyPress = function (ev) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
if (scope.entry.replyContent.length > 0) {
|
||||
ev.target.blur()
|
||||
return scope.onReply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.animateAndCallOnResolve = function () {
|
||||
scope.state.animating = true
|
||||
element.find('.rp-entry').css('top', 0)
|
||||
$timeout(() => scope.onResolve(), 350)
|
||||
return true
|
||||
}
|
||||
|
||||
scope.startEditing = function (comment) {
|
||||
comment.editing = true
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.saveEdit = function (comment) {
|
||||
comment.editing = false
|
||||
return scope.onSaveEdit({ comment })
|
||||
}
|
||||
|
||||
scope.confirmDelete = function (comment) {
|
||||
comment.deleting = true
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.cancelDelete = function (comment) {
|
||||
comment.deleting = false
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.doDelete = function (comment) {
|
||||
comment.deleting = false
|
||||
return scope.onDelete({ comment })
|
||||
}
|
||||
|
||||
return (scope.saveEditOnEnter = function (ev, comment) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
return scope.saveEdit(comment)
|
||||
scope.animateAndCallOnResolve = function () {
|
||||
scope.state.animating = true
|
||||
element.find('.rp-entry').css('top', 0)
|
||||
$timeout(() => scope.onResolve(), 350)
|
||||
return true
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
|
||||
scope.startEditing = function (comment) {
|
||||
comment.editing = true
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.saveEdit = function (comment) {
|
||||
comment.editing = false
|
||||
return scope.onSaveEdit({ comment })
|
||||
}
|
||||
|
||||
scope.confirmDelete = function (comment) {
|
||||
comment.deleting = true
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.cancelDelete = function (comment) {
|
||||
comment.deleting = false
|
||||
return setTimeout(() => scope.$emit('review-panel:layout'))
|
||||
}
|
||||
|
||||
scope.doDelete = function (comment) {
|
||||
comment.deleting = false
|
||||
return scope.onDelete({ comment })
|
||||
}
|
||||
|
||||
return (scope.saveEditOnEnter = function (ev, comment) {
|
||||
if (ev.keyCode === 13 && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
ev.preventDefault()
|
||||
return scope.saveEdit(comment)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -11,26 +11,28 @@
|
|||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.directive('resolvedCommentEntry', () => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'resolvedCommentEntryTemplate',
|
||||
scope: {
|
||||
thread: '=',
|
||||
permissions: '=',
|
||||
onUnresolve: '&',
|
||||
onDelete: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 40
|
||||
scope.needsCollapsing = false
|
||||
scope.isCollapsed = true
|
||||
export default App.directive('resolvedCommentEntry', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'resolvedCommentEntryTemplate',
|
||||
scope: {
|
||||
thread: '=',
|
||||
permissions: '=',
|
||||
onUnresolve: '&',
|
||||
onDelete: '&',
|
||||
},
|
||||
link(scope, element, attrs) {
|
||||
scope.contentLimit = 40
|
||||
scope.needsCollapsing = false
|
||||
scope.isCollapsed = true
|
||||
|
||||
scope.toggleCollapse = () => (scope.isCollapsed = !scope.isCollapsed)
|
||||
scope.toggleCollapse = () => (scope.isCollapsed = !scope.isCollapsed)
|
||||
|
||||
return scope.$watch(
|
||||
'thread.content.length',
|
||||
contentLength =>
|
||||
(scope.needsCollapsing = contentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}))
|
||||
return scope.$watch(
|
||||
'thread.content.length',
|
||||
contentLength =>
|
||||
(scope.needsCollapsing = contentLength > scope.contentLimit)
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -14,96 +14,98 @@ import _ from 'lodash'
|
|||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.directive('resolvedCommentsDropdown', () => ({
|
||||
restrict: 'E',
|
||||
templateUrl: 'resolvedCommentsDropdownTemplate',
|
||||
scope: {
|
||||
entries: '=',
|
||||
threads: '=',
|
||||
resolvedIds: '=',
|
||||
docs: '=',
|
||||
permissions: '=',
|
||||
onOpen: '&',
|
||||
onUnresolve: '&',
|
||||
onDelete: '&',
|
||||
isLoading: '=',
|
||||
},
|
||||
export default App.directive('resolvedCommentsDropdown', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'resolvedCommentsDropdownTemplate',
|
||||
scope: {
|
||||
entries: '=',
|
||||
threads: '=',
|
||||
resolvedIds: '=',
|
||||
docs: '=',
|
||||
permissions: '=',
|
||||
onOpen: '&',
|
||||
onUnresolve: '&',
|
||||
onDelete: '&',
|
||||
isLoading: '=',
|
||||
},
|
||||
|
||||
link(scope, element, attrs) {
|
||||
let filterResolvedComments
|
||||
scope.state = { isOpen: false }
|
||||
link(scope, element, attrs) {
|
||||
let filterResolvedComments
|
||||
scope.state = { isOpen: false }
|
||||
|
||||
scope.toggleOpenState = function () {
|
||||
scope.state.isOpen = !scope.state.isOpen
|
||||
if (scope.state.isOpen) {
|
||||
return scope.onOpen().then(() => filterResolvedComments())
|
||||
scope.toggleOpenState = function () {
|
||||
scope.state.isOpen = !scope.state.isOpen
|
||||
if (scope.state.isOpen) {
|
||||
return scope.onOpen().then(() => filterResolvedComments())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.resolvedComments = []
|
||||
|
||||
scope.handleUnresolve = function (threadId) {
|
||||
scope.onUnresolve({ threadId })
|
||||
return (scope.resolvedComments = scope.resolvedComments.filter(
|
||||
c => c.threadId !== threadId
|
||||
))
|
||||
}
|
||||
|
||||
scope.handleDelete = function (entryId, docId, threadId) {
|
||||
scope.onDelete({ entryId, docId, threadId })
|
||||
return (scope.resolvedComments = scope.resolvedComments.filter(
|
||||
c => c.threadId !== threadId
|
||||
))
|
||||
}
|
||||
|
||||
const getDocNameById = function (docId) {
|
||||
const doc = _.find(scope.docs, doc => doc.doc.id === docId)
|
||||
if (doc != null) {
|
||||
return doc.path
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (filterResolvedComments = function () {
|
||||
scope.resolvedComments = []
|
||||
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const docId in scope.entries) {
|
||||
const docEntries = scope.entries[docId]
|
||||
result.push(
|
||||
(() => {
|
||||
const result1 = []
|
||||
for (const entryId in docEntries) {
|
||||
const entry = docEntries[entryId]
|
||||
if (
|
||||
entry.type === 'comment' &&
|
||||
(scope.threads[entry.thread_id] != null
|
||||
? scope.threads[entry.thread_id].resolved
|
||||
: undefined) != null
|
||||
) {
|
||||
const resolvedComment = angular.copy(
|
||||
scope.threads[entry.thread_id]
|
||||
)
|
||||
scope.handleUnresolve = function (threadId) {
|
||||
scope.onUnresolve({ threadId })
|
||||
return (scope.resolvedComments = scope.resolvedComments.filter(
|
||||
c => c.threadId !== threadId
|
||||
))
|
||||
}
|
||||
|
||||
resolvedComment.content = entry.content
|
||||
resolvedComment.threadId = entry.thread_id
|
||||
resolvedComment.entryId = entryId
|
||||
resolvedComment.docId = docId
|
||||
resolvedComment.docName = getDocNameById(docId)
|
||||
scope.handleDelete = function (entryId, docId, threadId) {
|
||||
scope.onDelete({ entryId, docId, threadId })
|
||||
return (scope.resolvedComments = scope.resolvedComments.filter(
|
||||
c => c.threadId !== threadId
|
||||
))
|
||||
}
|
||||
|
||||
result1.push(scope.resolvedComments.push(resolvedComment))
|
||||
} else {
|
||||
result1.push(undefined)
|
||||
}
|
||||
}
|
||||
return result1
|
||||
})()
|
||||
)
|
||||
const getDocNameById = function (docId) {
|
||||
const doc = _.find(scope.docs, doc => doc.doc.id === docId)
|
||||
if (doc != null) {
|
||||
return doc.path
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return result
|
||||
})()
|
||||
})
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return (filterResolvedComments = function () {
|
||||
scope.resolvedComments = []
|
||||
|
||||
return (() => {
|
||||
const result = []
|
||||
for (const docId in scope.entries) {
|
||||
const docEntries = scope.entries[docId]
|
||||
result.push(
|
||||
(() => {
|
||||
const result1 = []
|
||||
for (const entryId in docEntries) {
|
||||
const entry = docEntries[entryId]
|
||||
if (
|
||||
entry.type === 'comment' &&
|
||||
(scope.threads[entry.thread_id] != null
|
||||
? scope.threads[entry.thread_id].resolved
|
||||
: undefined) != null
|
||||
) {
|
||||
const resolvedComment = angular.copy(
|
||||
scope.threads[entry.thread_id]
|
||||
)
|
||||
|
||||
resolvedComment.content = entry.content
|
||||
resolvedComment.threadId = entry.thread_id
|
||||
resolvedComment.entryId = entryId
|
||||
resolvedComment.docId = docId
|
||||
resolvedComment.docName = getDocNameById(docId)
|
||||
|
||||
result1.push(scope.resolvedComments.push(resolvedComment))
|
||||
} else {
|
||||
result1.push(undefined)
|
||||
}
|
||||
}
|
||||
return result1
|
||||
})()
|
||||
)
|
||||
}
|
||||
return result
|
||||
})()
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,26 +12,28 @@ import App from '../../../base'
|
|||
|
||||
export default App.directive('reviewPanelCollapseHeight', [
|
||||
'$parse',
|
||||
$parse => ({
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(
|
||||
() => $parse(attrs.reviewPanelCollapseHeight)(scope),
|
||||
function (shouldCollapse) {
|
||||
const neededHeight = element.prop('scrollHeight')
|
||||
if (neededHeight > 0) {
|
||||
if (shouldCollapse) {
|
||||
return element.animate({ height: 0 }, 150)
|
||||
function ($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link(scope, element, attrs) {
|
||||
return scope.$watch(
|
||||
() => $parse(attrs.reviewPanelCollapseHeight)(scope),
|
||||
function (shouldCollapse) {
|
||||
const neededHeight = element.prop('scrollHeight')
|
||||
if (neededHeight > 0) {
|
||||
if (shouldCollapse) {
|
||||
return element.animate({ height: 0 }, 150)
|
||||
} else {
|
||||
return element.animate({ height: neededHeight }, 150)
|
||||
}
|
||||
} else {
|
||||
return element.animate({ height: neededHeight }, 150)
|
||||
}
|
||||
} else {
|
||||
if (shouldCollapse) {
|
||||
return element.height(0)
|
||||
if (shouldCollapse) {
|
||||
return element.height(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
}
|
||||
},
|
||||
])
|
||||
|
|
|
@ -16,285 +16,289 @@
|
|||
import App from '../../../base'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
|
||||
export default App.directive('reviewPanelSorted', () => ({
|
||||
link(scope, element, attrs) {
|
||||
let previous_focused_entry_index = 0
|
||||
export default App.directive('reviewPanelSorted', function () {
|
||||
return {
|
||||
link(scope, element, attrs) {
|
||||
let previous_focused_entry_index = 0
|
||||
|
||||
const applyEntryVisibility = function (entry) {
|
||||
const visible = entry.scope.entry.screenPos
|
||||
if (visible) {
|
||||
entry.$wrapper_el.removeClass('rp-entry-hidden')
|
||||
} else {
|
||||
entry.$wrapper_el.addClass('rp-entry-hidden')
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
const layout = function (animate) {
|
||||
let entry,
|
||||
height,
|
||||
i,
|
||||
original_top,
|
||||
overflowTop,
|
||||
screenPosHeight,
|
||||
OVERVIEW_TOGGLE_HEIGHT,
|
||||
PADDING,
|
||||
TOOLBAR_HEIGHT,
|
||||
top
|
||||
if (animate == null) {
|
||||
animate = true
|
||||
}
|
||||
if (animate) {
|
||||
element.removeClass('no-animate')
|
||||
} else {
|
||||
element.addClass('no-animate')
|
||||
}
|
||||
if (scope.ui.reviewPanelOpen) {
|
||||
PADDING = 8
|
||||
TOOLBAR_HEIGHT = 38
|
||||
OVERVIEW_TOGGLE_HEIGHT = 57
|
||||
} else {
|
||||
PADDING = 4
|
||||
TOOLBAR_HEIGHT = 4
|
||||
OVERVIEW_TOGGLE_HEIGHT = 0
|
||||
const applyEntryVisibility = function (entry) {
|
||||
const visible = entry.scope.entry.screenPos
|
||||
if (visible) {
|
||||
entry.$wrapper_el.removeClass('rp-entry-hidden')
|
||||
} else {
|
||||
entry.$wrapper_el.addClass('rp-entry-hidden')
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
const entries = []
|
||||
for (const el of Array.from(element.find('.rp-entry-wrapper'))) {
|
||||
entry = {
|
||||
$wrapper_el: $(el),
|
||||
$indicator_el: $(el).find('.rp-entry-indicator'),
|
||||
$box_el: $(el).find('.rp-entry'),
|
||||
$callout_el: $(el).find('.rp-entry-callout'),
|
||||
scope: angular.element(el).scope(),
|
||||
const layout = function (animate) {
|
||||
let entry,
|
||||
height,
|
||||
i,
|
||||
original_top,
|
||||
overflowTop,
|
||||
screenPosHeight,
|
||||
OVERVIEW_TOGGLE_HEIGHT,
|
||||
PADDING,
|
||||
TOOLBAR_HEIGHT,
|
||||
top
|
||||
if (animate == null) {
|
||||
animate = true
|
||||
}
|
||||
if (animate) {
|
||||
element.removeClass('no-animate')
|
||||
} else {
|
||||
element.addClass('no-animate')
|
||||
}
|
||||
if (scope.ui.reviewPanelOpen) {
|
||||
entry.$layout_el = entry.$box_el
|
||||
PADDING = 8
|
||||
TOOLBAR_HEIGHT = 38
|
||||
OVERVIEW_TOGGLE_HEIGHT = 57
|
||||
} else {
|
||||
entry.$layout_el = entry.$indicator_el
|
||||
PADDING = 4
|
||||
TOOLBAR_HEIGHT = 4
|
||||
OVERVIEW_TOGGLE_HEIGHT = 0
|
||||
}
|
||||
entry.height = entry.$layout_el.height() // Do all of our DOM reads first for performance, see http://wilsonpage.co.uk/preventing-layout-thrashing/
|
||||
entries.push(entry)
|
||||
}
|
||||
entries.sort((a, b) => a.scope.entry.offset - b.scope.entry.offset)
|
||||
|
||||
if (entries.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const line_height = scope.reviewPanel.rendererData.lineHeight
|
||||
|
||||
let focused_entry_index = Math.min(
|
||||
previous_focused_entry_index,
|
||||
entries.length - 1
|
||||
)
|
||||
for (i = 0; i < entries.length; i++) {
|
||||
entry = entries[i]
|
||||
if (entry.scope.entry.focused) {
|
||||
focused_entry_index = i
|
||||
break
|
||||
const entries = []
|
||||
for (const el of Array.from(element.find('.rp-entry-wrapper'))) {
|
||||
entry = {
|
||||
$wrapper_el: $(el),
|
||||
$indicator_el: $(el).find('.rp-entry-indicator'),
|
||||
$box_el: $(el).find('.rp-entry'),
|
||||
$callout_el: $(el).find('.rp-entry-callout'),
|
||||
scope: angular.element(el).scope(),
|
||||
}
|
||||
if (scope.ui.reviewPanelOpen) {
|
||||
entry.$layout_el = entry.$box_el
|
||||
} else {
|
||||
entry.$layout_el = entry.$indicator_el
|
||||
}
|
||||
entry.height = entry.$layout_el.height() // Do all of our DOM reads first for performance, see http://wilsonpage.co.uk/preventing-layout-thrashing/
|
||||
entries.push(entry)
|
||||
}
|
||||
}
|
||||
entries.sort((a, b) => a.scope.entry.offset - b.scope.entry.offset)
|
||||
|
||||
const focused_entry = entries[focused_entry_index]
|
||||
const focusedEntryVisible = applyEntryVisibility(focused_entry)
|
||||
|
||||
// If the focused entry has no screenPos, we can't position other entries
|
||||
// relative to it, so we position all other entries as though the focused
|
||||
// entry is at the top and they all follow it
|
||||
const entries_after = focusedEntryVisible
|
||||
? entries.slice(focused_entry_index + 1)
|
||||
: [...entries]
|
||||
const entries_before = focusedEntryVisible
|
||||
? entries.slice(0, focused_entry_index)
|
||||
: []
|
||||
previous_focused_entry_index = focused_entry_index
|
||||
|
||||
debugConsole.log('focused_entry_index', focused_entry_index)
|
||||
|
||||
const positionLayoutEl = function (
|
||||
$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
height
|
||||
) {
|
||||
if (original_top <= top) {
|
||||
$callout_el.removeClass('rp-entry-callout-inverted')
|
||||
return $callout_el.css({
|
||||
top: original_top + height - 1,
|
||||
height: top - original_top,
|
||||
})
|
||||
} else {
|
||||
$callout_el.addClass('rp-entry-callout-inverted')
|
||||
return $callout_el.css({
|
||||
top: top + height,
|
||||
height: original_top - top,
|
||||
})
|
||||
if (entries.length === 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Put the focused entry as close to where it wants to be as possible
|
||||
let focused_entry_top = 0
|
||||
let previousBottom = 0
|
||||
const line_height = scope.reviewPanel.rendererData.lineHeight
|
||||
|
||||
if (focusedEntryVisible) {
|
||||
const focusedEntryScreenPos = focused_entry.scope.entry.screenPos
|
||||
focused_entry_top = Math.max(focusedEntryScreenPos.y, TOOLBAR_HEIGHT)
|
||||
focused_entry.$box_el.css({
|
||||
top: focused_entry_top,
|
||||
// The entry element is invisible by default, to avoid flickering when positioning for
|
||||
// the first time. Here we make sure it becomes visible after having a "top" value.
|
||||
visibility: 'visible',
|
||||
})
|
||||
focused_entry.$indicator_el.css({ top: focused_entry_top })
|
||||
// use screenPos.height if set
|
||||
screenPosHeight = focusedEntryScreenPos.height ?? line_height
|
||||
positionLayoutEl(
|
||||
focused_entry.$callout_el,
|
||||
focusedEntryScreenPos.y,
|
||||
focused_entry_top,
|
||||
screenPosHeight
|
||||
let focused_entry_index = Math.min(
|
||||
previous_focused_entry_index,
|
||||
entries.length - 1
|
||||
)
|
||||
previousBottom = focused_entry_top + focused_entry.$layout_el.height()
|
||||
}
|
||||
for (i = 0; i < entries.length; i++) {
|
||||
entry = entries[i]
|
||||
if (entry.scope.entry.focused) {
|
||||
focused_entry_index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for (entry of entries_after) {
|
||||
const entryVisible = applyEntryVisibility(entry)
|
||||
if (entryVisible) {
|
||||
original_top = entry.scope.entry.screenPos.y
|
||||
// use screenPos.height if set
|
||||
screenPosHeight = entry.scope.entry.screenPos.height ?? line_height
|
||||
;({ height } = entry)
|
||||
top = Math.max(original_top, previousBottom + PADDING)
|
||||
previousBottom = top + height
|
||||
entry.$box_el.css({
|
||||
top,
|
||||
const focused_entry = entries[focused_entry_index]
|
||||
const focusedEntryVisible = applyEntryVisibility(focused_entry)
|
||||
|
||||
// If the focused entry has no screenPos, we can't position other entries
|
||||
// relative to it, so we position all other entries as though the focused
|
||||
// entry is at the top and they all follow it
|
||||
const entries_after = focusedEntryVisible
|
||||
? entries.slice(focused_entry_index + 1)
|
||||
: [...entries]
|
||||
const entries_before = focusedEntryVisible
|
||||
? entries.slice(0, focused_entry_index)
|
||||
: []
|
||||
previous_focused_entry_index = focused_entry_index
|
||||
|
||||
debugConsole.log('focused_entry_index', focused_entry_index)
|
||||
|
||||
const positionLayoutEl = function (
|
||||
$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
height
|
||||
) {
|
||||
if (original_top <= top) {
|
||||
$callout_el.removeClass('rp-entry-callout-inverted')
|
||||
return $callout_el.css({
|
||||
top: original_top + height - 1,
|
||||
height: top - original_top,
|
||||
})
|
||||
} else {
|
||||
$callout_el.addClass('rp-entry-callout-inverted')
|
||||
return $callout_el.css({
|
||||
top: top + height,
|
||||
height: original_top - top,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Put the focused entry as close to where it wants to be as possible
|
||||
let focused_entry_top = 0
|
||||
let previousBottom = 0
|
||||
|
||||
if (focusedEntryVisible) {
|
||||
const focusedEntryScreenPos = focused_entry.scope.entry.screenPos
|
||||
focused_entry_top = Math.max(focusedEntryScreenPos.y, TOOLBAR_HEIGHT)
|
||||
focused_entry.$box_el.css({
|
||||
top: focused_entry_top,
|
||||
// The entry element is invisible by default, to avoid flickering when positioning for
|
||||
// the first time. Here we make sure it becomes visible after having a "top" value.
|
||||
visibility: 'visible',
|
||||
})
|
||||
entry.$indicator_el.css({ top })
|
||||
positionLayoutEl(
|
||||
entry.$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
screenPosHeight
|
||||
)
|
||||
}
|
||||
debugConsole.log('ENTRY', { entry: entry.scope.entry, top })
|
||||
}
|
||||
|
||||
let previousTop = focused_entry_top
|
||||
entries_before.reverse() // Work through backwards, starting with the one just above
|
||||
for (entry of entries_before) {
|
||||
const entryVisible = applyEntryVisibility(entry)
|
||||
if (entryVisible) {
|
||||
original_top = entry.scope.entry.screenPos.y
|
||||
focused_entry.$indicator_el.css({ top: focused_entry_top })
|
||||
// use screenPos.height if set
|
||||
screenPosHeight = entry.scope.entry.screenPos.height ?? line_height
|
||||
;({ height } = entry)
|
||||
const original_bottom = original_top + height
|
||||
const bottom = Math.min(original_bottom, previousTop - PADDING)
|
||||
top = bottom - height
|
||||
previousTop = top
|
||||
entry.$box_el.css({
|
||||
top,
|
||||
// The entry element is invisible by default, to avoid flickering when positioning for
|
||||
// the first time. Here we make sure it becomes visible after having a "top" value.
|
||||
visibility: 'visible',
|
||||
})
|
||||
entry.$indicator_el.css({ top })
|
||||
screenPosHeight = focusedEntryScreenPos.height ?? line_height
|
||||
positionLayoutEl(
|
||||
entry.$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
focused_entry.$callout_el,
|
||||
focusedEntryScreenPos.y,
|
||||
focused_entry_top,
|
||||
screenPosHeight
|
||||
)
|
||||
previousBottom = focused_entry_top + focused_entry.$layout_el.height()
|
||||
}
|
||||
debugConsole.log('ENTRY', { entry: entry.scope.entry, top })
|
||||
}
|
||||
|
||||
const lastTop = top
|
||||
if (lastTop < TOOLBAR_HEIGHT) {
|
||||
overflowTop = -lastTop + TOOLBAR_HEIGHT
|
||||
} else {
|
||||
overflowTop = 0
|
||||
}
|
||||
for (entry of entries_after) {
|
||||
const entryVisible = applyEntryVisibility(entry)
|
||||
if (entryVisible) {
|
||||
original_top = entry.scope.entry.screenPos.y
|
||||
// use screenPos.height if set
|
||||
screenPosHeight = entry.scope.entry.screenPos.height ?? line_height
|
||||
;({ height } = entry)
|
||||
top = Math.max(original_top, previousBottom + PADDING)
|
||||
previousBottom = top + height
|
||||
entry.$box_el.css({
|
||||
top,
|
||||
// The entry element is invisible by default, to avoid flickering when positioning for
|
||||
// the first time. Here we make sure it becomes visible after having a "top" value.
|
||||
visibility: 'visible',
|
||||
})
|
||||
entry.$indicator_el.css({ top })
|
||||
positionLayoutEl(
|
||||
entry.$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
screenPosHeight
|
||||
)
|
||||
}
|
||||
debugConsole.log('ENTRY', { entry: entry.scope.entry, top })
|
||||
}
|
||||
|
||||
return scope.$emit('review-panel:sizes', {
|
||||
overflowTop,
|
||||
height: previousBottom + OVERVIEW_TOGGLE_HEIGHT,
|
||||
})
|
||||
}
|
||||
let previousTop = focused_entry_top
|
||||
entries_before.reverse() // Work through backwards, starting with the one just above
|
||||
for (entry of entries_before) {
|
||||
const entryVisible = applyEntryVisibility(entry)
|
||||
if (entryVisible) {
|
||||
original_top = entry.scope.entry.screenPos.y
|
||||
// use screenPos.height if set
|
||||
screenPosHeight = entry.scope.entry.screenPos.height ?? line_height
|
||||
;({ height } = entry)
|
||||
const original_bottom = original_top + height
|
||||
const bottom = Math.min(original_bottom, previousTop - PADDING)
|
||||
top = bottom - height
|
||||
previousTop = top
|
||||
entry.$box_el.css({
|
||||
top,
|
||||
// The entry element is invisible by default, to avoid flickering when positioning for
|
||||
// the first time. Here we make sure it becomes visible after having a "top" value.
|
||||
visibility: 'visible',
|
||||
})
|
||||
entry.$indicator_el.css({ top })
|
||||
positionLayoutEl(
|
||||
entry.$callout_el,
|
||||
original_top,
|
||||
top,
|
||||
screenPosHeight
|
||||
)
|
||||
}
|
||||
debugConsole.log('ENTRY', { entry: entry.scope.entry, top })
|
||||
}
|
||||
|
||||
scope.$applyAsync(() => layout())
|
||||
const lastTop = top
|
||||
if (lastTop < TOOLBAR_HEIGHT) {
|
||||
overflowTop = -lastTop + TOOLBAR_HEIGHT
|
||||
} else {
|
||||
overflowTop = 0
|
||||
}
|
||||
|
||||
scope.$on('review-panel:layout', function (e, animate) {
|
||||
if (animate == null) {
|
||||
animate = true
|
||||
}
|
||||
return scope.$applyAsync(() => layout(animate))
|
||||
})
|
||||
|
||||
scope.$watch('reviewPanel.rendererData.lineHeight', () => layout())
|
||||
|
||||
// # Scroll lock with Ace
|
||||
const scroller = element
|
||||
const list = element.find('.rp-entry-list-inner')
|
||||
|
||||
// If we listen for scroll events in the review panel natively, then with a Mac trackpad
|
||||
// the scroll is very smooth (natively done I'd guess), but we don't get polled regularly
|
||||
// enough to keep Ace in step, and it noticeably lags. If instead, we borrow the manual
|
||||
// mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into
|
||||
// scroll events ourselves, then it makes the review panel slightly less smooth (barely)
|
||||
// noticeable, but keeps it perfectly in step with Ace.
|
||||
ace
|
||||
.require('ace/lib/event')
|
||||
.addMouseWheelListener(scroller[0], function (e) {
|
||||
const deltaY = e.wheelY
|
||||
const old_top = parseInt(list.css('top'))
|
||||
const top = old_top - deltaY * 4
|
||||
scrollAce(-top)
|
||||
dispatchScrollEvent(deltaY * 4)
|
||||
return e.preventDefault()
|
||||
})
|
||||
|
||||
// We always scroll by telling Ace to scroll and then updating the
|
||||
// review panel. This lets Ace manage the size of the scroller and
|
||||
// when it overflows.
|
||||
let ignoreNextAceEvent = false
|
||||
|
||||
const scrollPanel = function (scrollTop, height) {
|
||||
if (ignoreNextAceEvent) {
|
||||
return (ignoreNextAceEvent = false)
|
||||
} else {
|
||||
list.height(height)
|
||||
return list.css({ top: -scrollTop })
|
||||
}
|
||||
}
|
||||
|
||||
const scrollAce = scrollTop =>
|
||||
scope.reviewPanelEventsBridge.emit('externalScroll', scrollTop)
|
||||
|
||||
scope.reviewPanelEventsBridge.on('aceScroll', scrollPanel)
|
||||
scope.$on('$destroy', () => scope.reviewPanelEventsBridge.off('aceScroll'))
|
||||
|
||||
// receive the scroll position from the CodeMirror 6 track changes extension
|
||||
window.addEventListener('editor:scroll', event => {
|
||||
const { scrollTop, height, paddingTop } = event.detail
|
||||
|
||||
scrollPanel(scrollTop - paddingTop, height)
|
||||
})
|
||||
|
||||
// Send scroll delta to the CodeMirror 6 track changes extension
|
||||
const dispatchScrollEvent = scrollTopDelta => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('review-panel:event', {
|
||||
detail: { type: 'scroll', payload: scrollTopDelta },
|
||||
return scope.$emit('review-panel:sizes', {
|
||||
overflowTop,
|
||||
height: previousBottom + OVERVIEW_TOGGLE_HEIGHT,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return scope.reviewPanelEventsBridge.emit('refreshScrollPosition')
|
||||
},
|
||||
}))
|
||||
scope.$applyAsync(() => layout())
|
||||
|
||||
scope.$on('review-panel:layout', function (e, animate) {
|
||||
if (animate == null) {
|
||||
animate = true
|
||||
}
|
||||
return scope.$applyAsync(() => layout(animate))
|
||||
})
|
||||
|
||||
scope.$watch('reviewPanel.rendererData.lineHeight', () => layout())
|
||||
|
||||
// # Scroll lock with Ace
|
||||
const scroller = element
|
||||
const list = element.find('.rp-entry-list-inner')
|
||||
|
||||
// If we listen for scroll events in the review panel natively, then with a Mac trackpad
|
||||
// the scroll is very smooth (natively done I'd guess), but we don't get polled regularly
|
||||
// enough to keep Ace in step, and it noticeably lags. If instead, we borrow the manual
|
||||
// mousewheel/trackpad scrolling behaviour from Ace, and turn mousewheel events into
|
||||
// scroll events ourselves, then it makes the review panel slightly less smooth (barely)
|
||||
// noticeable, but keeps it perfectly in step with Ace.
|
||||
ace
|
||||
.require('ace/lib/event')
|
||||
.addMouseWheelListener(scroller[0], function (e) {
|
||||
const deltaY = e.wheelY
|
||||
const old_top = parseInt(list.css('top'))
|
||||
const top = old_top - deltaY * 4
|
||||
scrollAce(-top)
|
||||
dispatchScrollEvent(deltaY * 4)
|
||||
return e.preventDefault()
|
||||
})
|
||||
|
||||
// We always scroll by telling Ace to scroll and then updating the
|
||||
// review panel. This lets Ace manage the size of the scroller and
|
||||
// when it overflows.
|
||||
let ignoreNextAceEvent = false
|
||||
|
||||
const scrollPanel = function (scrollTop, height) {
|
||||
if (ignoreNextAceEvent) {
|
||||
return (ignoreNextAceEvent = false)
|
||||
} else {
|
||||
list.height(height)
|
||||
return list.css({ top: -scrollTop })
|
||||
}
|
||||
}
|
||||
|
||||
const scrollAce = scrollTop =>
|
||||
scope.reviewPanelEventsBridge.emit('externalScroll', scrollTop)
|
||||
|
||||
scope.reviewPanelEventsBridge.on('aceScroll', scrollPanel)
|
||||
scope.$on('$destroy', () =>
|
||||
scope.reviewPanelEventsBridge.off('aceScroll')
|
||||
)
|
||||
|
||||
// receive the scroll position from the CodeMirror 6 track changes extension
|
||||
window.addEventListener('editor:scroll', event => {
|
||||
const { scrollTop, height, paddingTop } = event.detail
|
||||
|
||||
scrollPanel(scrollTop - paddingTop, height)
|
||||
})
|
||||
|
||||
// Send scroll delta to the CodeMirror 6 track changes extension
|
||||
const dispatchScrollEvent = scrollTopDelta => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('review-panel:event', {
|
||||
detail: { type: 'scroll', payload: scrollTopDelta },
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return scope.reviewPanelEventsBridge.emit('refreshScrollPosition')
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -12,40 +12,42 @@
|
|||
*/
|
||||
import App from '../../../base'
|
||||
|
||||
export default App.directive('reviewPanelToggle', () => ({
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
onToggle: '&',
|
||||
ngModel: '=',
|
||||
valWhenUndefined: '=?',
|
||||
isDisabled: '=?',
|
||||
onDisabledClick: '&?',
|
||||
description: '@',
|
||||
},
|
||||
link(scope) {
|
||||
if (scope.disabled == null) {
|
||||
scope.disabled = false
|
||||
}
|
||||
scope.onChange = (...args) => scope.onToggle({ isOn: scope.localModel })
|
||||
scope.handleClick = function () {
|
||||
if (scope.disabled && scope.onDisabledClick != null) {
|
||||
return scope.onDisabledClick()
|
||||
export default App.directive('reviewPanelToggle', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
onToggle: '&',
|
||||
ngModel: '=',
|
||||
valWhenUndefined: '=?',
|
||||
isDisabled: '=?',
|
||||
onDisabledClick: '&?',
|
||||
description: '@',
|
||||
},
|
||||
link(scope) {
|
||||
if (scope.disabled == null) {
|
||||
scope.disabled = false
|
||||
}
|
||||
}
|
||||
scope.localModel = scope.ngModel
|
||||
return scope.$watch('ngModel', function (value) {
|
||||
if (scope.valWhenUndefined != null && value == null) {
|
||||
value = scope.valWhenUndefined
|
||||
scope.onChange = (...args) => scope.onToggle({ isOn: scope.localModel })
|
||||
scope.handleClick = function () {
|
||||
if (scope.disabled && scope.onDisabledClick != null) {
|
||||
return scope.onDisabledClick()
|
||||
}
|
||||
}
|
||||
return (scope.localModel = value)
|
||||
})
|
||||
},
|
||||
scope.localModel = scope.ngModel
|
||||
return scope.$watch('ngModel', function (value) {
|
||||
if (scope.valWhenUndefined != null && value == null) {
|
||||
value = scope.valWhenUndefined
|
||||
}
|
||||
return (scope.localModel = value)
|
||||
})
|
||||
},
|
||||
|
||||
template: `\
|
||||
template: `\
|
||||
<fieldset class="input-switch" ng-click="handleClick();">
|
||||
<legend class="sr-only">{{description}}</legend>
|
||||
<input id="input-switch-{{$id}}" ng-disabled="isDisabled" type="checkbox" class="input-switch-hidden-input" ng-model="localModel" ng-change="onChange()" />
|
||||
<label for="input-switch-{{$id}}" class="input-switch-btn"></label>
|
||||
</fieldset>\
|
||||
`,
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue