| /* |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import { Injectable } from '@angular/core'; |
| import * as angular from 'angular'; |
| import * as JQuery from 'jquery'; |
| |
| interface CustomDragDropService { |
| draggableScope: angular.IScope | null; |
| dragData: unknown | null; |
| dragIndex: number | null; |
| dragModelPath: null | string; |
| dragSettings: { placeholder?: string } | null; |
| draggableElement: HTMLElement | null; |
| |
| callEventCallback(scope: angular.IScope, callbackName: string, event: DragEvent, ui?: UI): void; |
| updateModel(scope: angular.IScope, modelPath: string, newValue: unknown, index?: number): void; |
| removeFromModel(scope: angular.IScope, modelPath: string, index?: number): void; |
| } |
| |
| interface UI { |
| draggable: JQuery<HTMLElement> | null; |
| helper: null; |
| position: { top: number; left: number }; |
| offset: { top: number; left: number }; |
| } |
| |
| @Injectable({ |
| providedIn: 'root' |
| }) |
| export class AngularDragDropService { |
| constructor() {} |
| |
| addDragDropDirectives(module: angular.IModule): void { |
| // Drag and drop service to maintain state across directives |
| module.factory('customDragDropService', [ |
| '$parse', |
| function ($parse: angular.IParseService): CustomDragDropService { |
| return { |
| draggableScope: null, |
| dragData: null, |
| dragIndex: null, |
| dragModelPath: null, |
| dragSettings: null, |
| draggableElement: null, |
| |
| callEventCallback(scope, callbackStr, event, ui) { |
| if (!callbackStr) { |
| return; |
| } |
| |
| const { targetCallback, targetScope, args: extractedArgs } = extract(callbackStr); |
| const fullArgs = [event, ui].concat(extractedArgs); |
| |
| if (typeof targetCallback === 'function') { |
| return targetCallback.apply(targetScope, fullArgs); |
| } |
| |
| function extract(_callbackStr: string) { |
| const atStartBracket = _callbackStr.indexOf('(') !== -1 ? _callbackStr.indexOf('(') : _callbackStr.length; |
| const atEndBracket = |
| _callbackStr.lastIndexOf(')') !== -1 ? _callbackStr.lastIndexOf(')') : _callbackStr.length; |
| |
| const argsString = _callbackStr.substring(atStartBracket + 1, atEndBracket); |
| const identifierTokens = argsString ? argsString.split(',') : []; |
| const args = identifierTokens.map(item => $parse(item.trim())(scope)); |
| |
| const constructorName = |
| _callbackStr.indexOf('.') !== -1 ? _callbackStr.substr(0, _callbackStr.indexOf('.')) : null; |
| // @ts-ignore |
| const constructorCandid = constructorName && scope[constructorName]; |
| const constructor = |
| constructorCandid && typeof constructorCandid.constructor === 'function' ? constructorCandid : null; |
| |
| const callbackName = _callbackStr.substring((constructor && constructor.length + 1) || 0, atStartBracket); |
| // @ts-ignore |
| const callbackCandid = scope[callbackName]; |
| // If the expression is a method call, then the parsed constructor becomes its bound scope. |
| const _scope = callbackCandid ? scope : constructor; |
| const callback = callbackCandid || constructor[callbackName]; |
| |
| return { |
| args, |
| targetScope: _scope, |
| targetCallback: callback |
| }; |
| } |
| }, |
| |
| updateModel(scope, modelPath, newValue, index) { |
| const getter = $parse(modelPath); |
| const setter = getter.assign; |
| const modelValue = getter(scope); |
| |
| if (angular.isArray(modelValue)) { |
| if (typeof index === 'number') { |
| modelValue[index] = newValue; |
| } else { |
| modelValue.push(newValue); |
| } |
| } else { |
| if (setter) { |
| setter(scope, newValue); |
| } |
| } |
| |
| scope.$apply(); |
| }, |
| |
| removeFromModel(scope, modelPath, index) { |
| const getter = $parse(modelPath); |
| const modelValue = getter(scope); |
| |
| if (angular.isArray(modelValue) && typeof index === 'number') { |
| modelValue.splice(index, 1); |
| scope.$apply(); |
| } |
| } |
| }; |
| } |
| ]); |
| |
| // jqyoui-draggable directive |
| module.directive('jqyouiDraggable', [ |
| 'customDragDropService', |
| function (dragDropService: CustomDragDropService) { |
| return { |
| restrict: 'A', |
| link(scope, element, attrs) { |
| const el = element[0]; |
| |
| // Check if dragging is enabled |
| const isDragEnabled = function () { |
| return attrs.drag === 'true' || scope.$eval(attrs.drag) === true; |
| }; |
| |
| // Make element draggable when enabled |
| const updateDraggable = function () { |
| el.draggable = isDragEnabled(); |
| }; |
| |
| updateDraggable(); |
| |
| // Watch for changes in drag attribute |
| scope.$watch(function () { |
| return scope.$eval(attrs.drag); |
| }, updateDraggable); |
| |
| el.addEventListener('dragstart', function (event) { |
| if (!isDragEnabled()) { |
| event.preventDefault(); |
| return; |
| } |
| |
| const dragSettings = scope.$eval(attrs.jqyouiDraggable) || {}; |
| const ngModel = attrs.ngModel; |
| |
| if (ngModel) { |
| dragDropService.draggableScope = scope; |
| dragDropService.dragIndex = dragSettings.index; |
| dragDropService.dragModelPath = ngModel; |
| dragDropService.dragSettings = dragSettings; |
| dragDropService.draggableElement = el; |
| |
| const modelValue = scope.$eval(ngModel); |
| if (angular.isArray(modelValue) && typeof dragSettings.index === 'number') { |
| dragDropService.dragData = angular.copy(modelValue[dragSettings.index]); |
| } else { |
| dragDropService.dragData = angular.copy(modelValue); |
| } |
| |
| // Store data in dataTransfer for compatibility |
| if (event.dataTransfer) { |
| event.dataTransfer.setData( |
| 'text/plain', |
| JSON.stringify({ |
| data: dragDropService.dragData, |
| index: dragDropService.dragIndex, |
| modelPath: ngModel |
| }) |
| ); |
| event.dataTransfer.effectAllowed = 'move'; |
| } |
| } |
| |
| // Add visual feedback |
| el.style.opacity = '0.5'; |
| |
| // Call onStart callback if provided |
| if (dragSettings.onStart) { |
| const ui: UI = { |
| draggable: angular.element(el), |
| helper: null, |
| position: { top: event.clientY, left: event.clientX }, |
| offset: { top: event.pageY, left: event.pageX } |
| }; |
| dragDropService.callEventCallback(scope, dragSettings.onStart, event, ui); |
| } |
| }); |
| |
| el.addEventListener('dragend', function (event) { |
| // Remove visual feedback |
| el.style.opacity = ''; |
| |
| const dragSettings = scope.$eval(attrs.jqyouiDraggable) || {}; |
| |
| // Call onStop callback if provided |
| if (dragSettings.onStop) { |
| const ui = { |
| draggable: angular.element(el), |
| helper: null, |
| position: { top: event.clientY, left: event.clientX }, |
| offset: { top: event.pageY, left: event.pageX } |
| }; |
| dragDropService.callEventCallback(scope, dragSettings.onStop, event, ui); |
| } |
| }); |
| } |
| }; |
| } |
| ]); |
| |
| // jqyoui-droppable directive |
| module.directive('jqyouiDroppable', [ |
| 'customDragDropService', |
| function (dragDropService: CustomDragDropService) { |
| return { |
| restrict: 'A', |
| link(scope, element, attrs) { |
| const el = element[0]; |
| |
| // Check if dropping is enabled |
| const isDropEnabled = function () { |
| return attrs.drop === 'true' || scope.$eval(attrs.drop) === true; |
| }; |
| |
| el.addEventListener('dragover', function (event) { |
| if (!isDropEnabled()) { |
| return; |
| } |
| |
| event.preventDefault(); |
| if (event.dataTransfer) { |
| event.dataTransfer.dropEffect = 'move'; |
| } |
| |
| // Add visual feedback |
| el.style.backgroundColor = '#f0f0f0'; |
| }); |
| |
| el.addEventListener('dragleave', function () { |
| // Remove visual feedback |
| el.style.backgroundColor = ''; |
| }); |
| |
| el.addEventListener('drop', function (event) { |
| if (!isDropEnabled()) { |
| return; |
| } |
| |
| event.preventDefault(); |
| |
| // Remove visual feedback |
| el.style.backgroundColor = ''; |
| |
| const dropSettings = scope.$eval(attrs.jqyouiDroppable) || {}; |
| const dropModel = attrs.ngModel; |
| |
| if (!dropModel || !dragDropService.dragData) { |
| return; |
| } |
| |
| // Get drag data |
| const dragData = dragDropService.dragData; |
| const dragIndex = dragDropService.dragIndex; |
| const draggableScope = dragDropService.draggableScope; |
| const dragModelPath = dragDropService.dragModelPath; |
| |
| // Handle different drop scenarios |
| if (dropSettings.multiple) { |
| // Multiple items can be dropped - add to array |
| dragDropService.updateModel(scope, dropModel, dragData); |
| } else { |
| // Single item drop - replace existing value |
| const dropIndex = dropSettings.index; |
| if (typeof dropIndex === 'number') { |
| dragDropService.updateModel(scope, dropModel, dragData, dropIndex); |
| } else { |
| dragDropService.updateModel(scope, dropModel, dragData); |
| } |
| } |
| |
| // Remove from source if it's a move operation (not copy) |
| // Check placeholder setting to determine if we should remove from source |
| const dragSettings: CustomDragDropService['dragSettings'] = dragDropService.dragSettings || {}; |
| const shouldRemoveFromSource = |
| draggableScope && |
| typeof dragIndex === 'number' && |
| dragModelPath && |
| dragSettings.placeholder !== 'keep' && |
| !dropSettings.deepCopy; |
| |
| if (shouldRemoveFromSource && draggableScope) { |
| dragDropService.removeFromModel(draggableScope, dragModelPath ?? '', dragIndex ?? undefined); |
| } |
| |
| // Call onDrop callback if provided |
| if (dropSettings.onDrop) { |
| // Create ui object similar to jQuery UI for compatibility |
| const ui = { |
| draggable: dragDropService.draggableElement |
| ? angular.element(dragDropService.draggableElement) |
| : null, |
| helper: null, // HTML5 drag doesn't have helper concept |
| position: { top: event.clientY, left: event.clientX }, |
| offset: { top: event.pageY, left: event.pageX } |
| }; |
| |
| dragDropService.callEventCallback(scope, dropSettings.onDrop, event, ui); |
| } |
| |
| // Clear drag data |
| dragDropService.dragData = null; |
| dragDropService.dragIndex = null; |
| dragDropService.draggableScope = null; |
| dragDropService.dragModelPath = null; |
| dragDropService.dragSettings = null; |
| dragDropService.draggableElement = null; |
| }); |
| } |
| }; |
| } |
| ]); |
| } |
| } |