From 2761a51807f0fc49310ef47a573ef10fe5473d4d Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Tue, 17 Oct 2023 09:36:46 +0100 Subject: [PATCH] Merge pull request #15248 from overleaf/mj-eslint-angular-components [web] Add eslint rules for angularjs components GitOrigin-RevId: 1343d584368faeb912f04c5879228bcbd07a042a --- services/web/.eslintrc | 27 +- .../web/frontend/js/directives/asyncForm.js | 318 +++++------ .../js/directives/bookmarkableTabset.js | 107 ++-- .../frontend/js/directives/complexPassword.js | 147 ++--- services/web/frontend/js/directives/equals.js | 26 +- .../frontend/js/directives/eventTracking.js | 120 ++-- .../js/directives/expandableTextArea.js | 38 +- services/web/frontend/js/directives/focus.js | 134 +++-- .../web/frontend/js/directives/maxHeight.js | 22 +- .../web/frontend/js/directives/onEnter.js | 7 +- .../web/frontend/js/directives/rightClick.js | 22 +- services/web/frontend/js/directives/scroll.js | 84 +-- .../web/frontend/js/directives/selectAll.js | 162 +++--- .../frontend/js/directives/stopPropagation.js | 28 +- .../frontend/js/directives/videoPlayState.js | 34 +- .../web/frontend/js/ide/directives/layout.js | 525 +++++++++--------- .../frontend/js/ide/directives/validFile.js | 16 +- .../ide/directives/verticalResizablePanes.js | 246 ++++---- .../editor/directives/formattingButtons.js | 28 +- .../js/ide/editor/directives/toggleSwitch.js | 22 +- .../directives/historyDraggableBoundary.js | 64 +-- .../directives/historyDroppableArea.js | 48 +- .../ide/history/directives/infiniteScroll.js | 64 +-- .../directives/addCommentEntry.js | 98 ++-- .../directives/aggregateChangeEntry.js | 104 ++-- .../directives/bulkActionsEntry.js | 28 +- .../review-panel/directives/changeEntry.js | 78 +-- .../review-panel/directives/commentEntry.js | 148 ++--- .../directives/resolvedCommentEntry.js | 44 +- .../directives/resolvedCommentsDropdown.js | 170 +++--- .../directives/reviewPanelCollapseHeight.js | 40 +- .../directives/reviewPanelSorted.js | 508 ++++++++--------- .../directives/reviewPanelToggle.js | 58 +- 33 files changed, 1846 insertions(+), 1719 deletions(-) diff --git a/services/web/.eslintrc b/services/web/.eslintrc index 85819a2a86..239ea7d1c3 100644 --- a/services/web/.eslintrc +++ b/services/web/.eslintrc @@ -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 diff --git a/services/web/frontend/js/directives/asyncForm.js b/services/web/frontend/js/directives/asyncForm.js index 626e63a5be..9348b756df 100644 --- a/services/web/frontend/js/directives/asyncForm.js +++ b/services/web/frontend/js/directives/asyncForm.js @@ -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: `\
\ `)(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) + }, + } + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/directives/validFile.js b/services/web/frontend/js/ide/directives/validFile.js index 3cc131476c..339bc99d67 100644 --- a/services/web/frontend/js/ide/directives/validFile.js +++ b/services/web/frontend/js/ide/directives/validFile.js @@ -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)) + }, + } +}) diff --git a/services/web/frontend/js/ide/directives/verticalResizablePanes.js b/services/web/frontend/js/ide/directives/verticalResizablePanes.js index cc91746a20..b6fe36867d 100644 --- a/services/web/frontend/js/ide/directives/verticalResizablePanes.js +++ b/services/web/frontend/js/ide/directives/verticalResizablePanes.js @@ -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') + } + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/editor/directives/formattingButtons.js b/services/web/frontend/js/ide/editor/directives/formattingButtons.js index 0508f8280a..71bbc32991 100644 --- a/services/web/frontend/js/ide/editor/directives/formattingButtons.js +++ b/services/web/frontend/js/ide/editor/directives/formattingButtons.js @@ -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', + } +}) diff --git a/services/web/frontend/js/ide/editor/directives/toggleSwitch.js b/services/web/frontend/js/ide/editor/directives/toggleSwitch.js index 2dbfc46878..ee4335d74b 100644 --- a/services/web/frontend/js/ide/editor/directives/toggleSwitch.js +++ b/services/web/frontend/js/ide/editor/directives/toggleSwitch.js @@ -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: `\
{{description}} @@ -33,4 +34,5 @@ export default App.directive('toggleSwitch', () => ({
\ `, -})) + } +}) diff --git a/services/web/frontend/js/ide/history/directives/historyDraggableBoundary.js b/services/web/frontend/js/ide/history/directives/historyDraggableBoundary.js index 34aa84bba6..9a115108d6 100644 --- a/services/web/frontend/js/ide/history/directives/historyDraggableBoundary.js +++ b/services/web/frontend/js/ide/history/directives/historyDraggableBoundary.js @@ -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, + }) + }, + }) + }, + } +}) diff --git a/services/web/frontend/js/ide/history/directives/historyDroppableArea.js b/services/web/frontend/js/ide/history/directives/historyDroppableArea.js index 8f0b44ab38..082c1b553e 100644 --- a/services/web/frontend/js/ide/history/directives/historyDroppableArea.js +++ b/services/web/frontend/js/ide/history/directives/historyDroppableArea.js @@ -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 }) + }, + }) + }, + } +}) diff --git a/services/web/frontend/js/ide/history/directives/infiniteScroll.js b/services/web/frontend/js/ide/history/directives/infiniteScroll.js index b559b28990..dbc5cc704c 100644 --- a/services/web/frontend/js/ide/history/directives/infiniteScroll.js +++ b/services/web/frontend/js/ide/history/directives/infiniteScroll.js @@ -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() + } + }) + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/addCommentEntry.js b/services/web/frontend/js/ide/review-panel/directives/addCommentEntry.js index 4255e630f1..d74258e613 100644 --- a/services/web/frontend/js/ide/review-panel/directives/addCommentEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/addCommentEntry.js @@ -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 = '' - } - }, -})) + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js b/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js index f2595b7d1b..568b3c7b52 100644 --- a/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/aggregateChangeEntry.js @@ -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) + ) + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/review-panel/directives/bulkActionsEntry.js b/services/web/frontend/js/ide/review-panel/directives/bulkActionsEntry.js index be154d2d7f..2c2c132e8b 100644 --- a/services/web/frontend/js/ide/review-panel/directives/bulkActionsEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/bulkActionsEntry.js @@ -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()) + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/changeEntry.js b/services/web/frontend/js/ide/review-panel/directives/changeEntry.js index ee52ede57a..2ee343efe1 100644 --- a/services/web/frontend/js/ide/review-panel/directives/changeEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/changeEntry.js @@ -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) + ) + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/review-panel/directives/commentEntry.js b/services/web/frontend/js/ide/review-panel/directives/commentEntry.js index 6d091da9d9..befddc3bcd 100644 --- a/services/web/frontend/js/ide/review-panel/directives/commentEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/commentEntry.js @@ -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) + } + }) + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/review-panel/directives/resolvedCommentEntry.js b/services/web/frontend/js/ide/review-panel/directives/resolvedCommentEntry.js index 4404203937..a3c3ea589d 100644 --- a/services/web/frontend/js/ide/review-panel/directives/resolvedCommentEntry.js +++ b/services/web/frontend/js/ide/review-panel/directives/resolvedCommentEntry.js @@ -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) + ) + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/resolvedCommentsDropdown.js b/services/web/frontend/js/ide/review-panel/directives/resolvedCommentsDropdown.js index 8ad9ff2e79..08d1e576c5 100644 --- a/services/web/frontend/js/ide/review-panel/directives/resolvedCommentsDropdown.js +++ b/services/web/frontend/js/ide/review-panel/directives/resolvedCommentsDropdown.js @@ -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 + })() + }) + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js index 835c9f5d58..c808a5f9e2 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelCollapseHeight.js @@ -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) + } } } - } - ) - }, - }), + ) + }, + } + }, ]) diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js index 6c69f26562..b706148816 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelSorted.js @@ -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') + }, + } +}) diff --git a/services/web/frontend/js/ide/review-panel/directives/reviewPanelToggle.js b/services/web/frontend/js/ide/review-panel/directives/reviewPanelToggle.js index ac0ad18cbf..810e4d16dc 100644 --- a/services/web/frontend/js/ide/review-panel/directives/reviewPanelToggle.js +++ b/services/web/frontend/js/ide/review-panel/directives/reviewPanelToggle.js @@ -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: `\
{{description}}
\ `, -})) + } +})