如何仅使用角度指令将放置元素拖放到日历中
Posted
技术标签:
【中文标题】如何仅使用角度指令将放置元素拖放到日历中【英文标题】:How to drag&drop elements onto a calendar with angular directives only 【发布时间】:2014-04-17 14:19:35 【问题描述】:我正在尝试使用角度指令实现拖放日历。日历使用 ui-calendar (https://github.com/angular-ui/ui-calendar),它是 Arshaw FullCalendar 的完整 AngularJS 指令。
将元素拖放到日历上由 angular-dragdrop (https://github.com/codef0rmer/angular-dragdrop) 提供支持。
这里是文件夹 demo/ui-calendar/demo 中的my try,但是当我将元素放到日历上时,不会触发任何事件...
html如下:
<html lang="en" ng-app="calendarDemoApp" id="top" class="ng-scope">
<head>
<style type="text/css">@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hidedisplay:none !important;ng\:formdisplay:block;.ng-animate-block-transitionstransition:0s all!important;-webkit-transition:0s all!important;</style>
<link rel="stylesheet" href="bootstrap.css">
<link rel="stylesheet" href="fullcalendar.css">
<link rel="stylesheet" href="calendarDemo.css">
<script src="jquery.js"></script>
<script src="jquery-ui.js"></script>
<script src="angular.js"></script>
<script src="https://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.9.0.js"></script>
<script src="fullcalendar.js"></script>
<script src="calendar.js"></script>
<script>
$.fn.ngattr = function(name, value)
var element = angular.element(this).get(0);
return element.getAttribute(name) || element.getAttribute('data-' + name);
;
</script>
</head>
<body>
<div ng-controller="dragdropController">
<ul>
<li class="btn btn-primary"
ng-repeat='item in list1'
ng-show="item.title"
data-drag="true"
data-jqyoui-options="revert: 'invalid', helper: 'clone'"
ng-model="list1"
jqyoui-draggable="index: $index, animate: true, placeholder: 'keep'">item.title</li>
</ul>
<div class="alert-success calAlert" ng-show="alertMessage != undefined && alertMessage != ''">
<h4>alertMessage</h4>
</div>
<div ng-controller="CalendarCtrl" select="renderCalender(myCalendar1);">
<div class="btn-toolbar">
<div class="btn-group">
<button type="button" class="btn btn-primary" ng-click="addEvent()">Add Event</button>
</div>
</div>
<div data-drop="true"
jqyoui-droppable="multiple:true"
id="calendar"
class="calendar"
ng-model="eventSources"
calendar="myCalendar1"
config="uiConfig.calendar"
ui-calendar="uiConfig.calendar">
</div>
</div>
</div>
</body>
</html>
为了简单起见,我将所有指令、控制器和服务合并到了下面的 js 文件中:
function CalendarCtrl($scope)
/* event source that contains custom events on the scope */
$scope.events = [
title: 'All Day Event',start: new Date()
];
/* add custom event*/
$scope.addEvent = function()
$scope.events.push(
title: 'Open Sesame',
start: new Date(),
className: ['openSesame']
);
;
$scope.drop = function(date, allDay)
$scope.alertMessage = ('Event Droped on ' + date);
;
/* config object */
$scope.uiConfig =
calendar:
height: 450,
editable: true,
header:
left: 'title',
center: '',
right: 'today prev,next'
,
drop: $scope.drop
;
/* event sources array*/
$scope.eventSources = [$scope.events];
angular.module('calendarDemoApp', [])
.constant('uiCalendarConfig', )
.controller('dragdropController', ['$scope','$timeout', function($scope, $timeout)
var addEvent = function (title, length)
length = length.length == 0 ? "0" : length;
title = title.length == 0 ? "Untitled Event (" + length + " min)" : title + " (" + length + " min)";
$scope.list1.push('title': title, 'length': length);
$('#event_add').unbind('click').click(function ()
var title = $('#event_title').val();
var length = $('#event_length').val();
addEvent(title, length);
);
$scope.list1 = [
title: 'Full check up', length: '25',
title: 'Whitening', length: '90',
title: 'Filling', length: '30'];
])
.controller('uiCalendarCtrl', ['$scope', '$timeout', function($scope, $timeout)
var sourceSerialId = 1,
eventSerialId = 1,
sources = $scope.eventSources,
extraEventSignature = $scope.calendarWatchEvent ? $scope.calendarWatchEvent : angular.noop,
wrapFunctionWithScopeApply = function(functionToWrap)
var wrapper;
if (functionToWrap)
wrapper = function()
// This happens outside of angular context so we need to wrap it in a timeout which has an implied apply.
// In this way the function will be safely executed on the next digest.
var args = arguments;
$timeout(function()
functionToWrap.apply(this, args);
);
;
return wrapper;
;
this.eventsFingerprint = function(e)
if (!e.__uiCalId)
e.__uiCalId = eventSerialId++;
// This extracts all the information we need from the event. http://jsperf.com/angular-calendar-events-fingerprint/3
return "" + e.__uiCalId + (e.id || '') + (e.title || '') + (e.url || '') + (+e.start || '') + (+e.end || '') +
(e.allDay || '') + (e.className || '') + extraEventSignature(e) || '';
;
this.sourcesFingerprint = function(source)
return source.__id || (source.__id = sourceSerialId++);
;
this.allEvents = function()
// return sources.flatten(); but we don't have flatten
var arraySources = [];
for (var i = 0, srcLen = sources.length; i < srcLen; i++)
var source = sources[i];
if (angular.isArray(source))
// event source as array
arraySources.push(source);
else if(angular.isObject(source) && angular.isArray(source.events))
// event source as object, ie extended form
var extEvent = ;
for(var key in source)
if(key !== '_uiCalId' && key !== 'events')
extEvent[key] = source[key];
for(var eI = 0;eI < source.events.length;eI++)
angular.extend(source.events[eI],extEvent);
arraySources.push(source.events);
return Array.prototype.concat.apply([], arraySources);
;
// Track changes in array by assigning id tokens to each element and watching the scope for changes in those tokens
// arguments:
// arraySource array of function that returns array of objects to watch
// tokenFn function(object) that returns the token for a given object
this.changeWatcher = function(arraySource, tokenFn)
var self;
var getTokens = function()
var array = angular.isFunction(arraySource) ? arraySource() : arraySource;
var result = [], token, el;
for (var i = 0, n = array.length; i < n; i++)
el = array[i];
token = tokenFn(el);
map[token] = el;
result.push(token);
return result;
;
// returns elements in that are in a but not in b
// subtractAsSets([4, 5, 6], [4, 5, 7]) => [6]
var subtractAsSets = function(a, b)
var result = [], inB = , i, n;
for (i = 0, n = b.length; i < n; i++)
inB[b[i]] = true;
for (i = 0, n = a.length; i < n; i++)
if (!inB[a[i]])
result.push(a[i]);
return result;
;
// Map objects to tokens and vice-versa
var map = ;
var applyChanges = function(newTokens, oldTokens)
var i, n, el, token;
var replacedTokens = ;
var removedTokens = subtractAsSets(oldTokens, newTokens);
for (i = 0, n = removedTokens.length; i < n; i++)
var removedToken = removedTokens[i];
el = map[removedToken];
delete map[removedToken];
var newToken = tokenFn(el);
// if the element wasn't removed but simply got a new token, its old token will be different from the current one
if (newToken === removedToken)
self.onRemoved(el);
else
replacedTokens[newToken] = removedToken;
self.onChanged(el);
var addedTokens = subtractAsSets(newTokens, oldTokens);
for (i = 0, n = addedTokens.length; i < n; i++)
token = addedTokens[i];
el = map[token];
if (!replacedTokens[token])
self.onAdded(el);
;
return self =
subscribe: function(scope, onChanged)
scope.$watch(getTokens, function(newTokens, oldTokens)
if (!onChanged || onChanged(newTokens, oldTokens) !== false)
applyChanges(newTokens, oldTokens);
, true);
,
onAdded: angular.noop,
onChanged: angular.noop,
onRemoved: angular.noop
;
;
this.getFullCalendarConfig = function(calendarSettings, uiCalendarConfig)
var config = ;
angular.extend(config, uiCalendarConfig);
angular.extend(config, calendarSettings);
angular.forEach(config, function(value,key)
if (typeof value === 'function')
config[key] = wrapFunctionWithScopeApply(config[key]);
);
return config;
;
])
.directive('jqyouiDraggable', ['ngDragDropService', function(ngDragDropService)
return
require: '?jqyouiDroppable',
restrict: 'A',
link: function(scope, element, attrs)
var dragSettings, jqyouiOptions, zIndex;
var updateDraggable = function(newValue, oldValue)
if (newValue)
dragSettings = scope.$eval(element.attr('jqyoui-draggable') || element.attr('data-jqyoui-draggable')) || ;
jqyouiOptions = scope.$eval(attrs.jqyouiOptions) || ;
element
.draggable(disabled: false)
.draggable(jqyouiOptions)
.draggable(
start: function(event, ui)
zIndex = angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index');
angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', 9999);
angular.startXY = angular.element(this).offset();
ngDragDropService.callEventCallback(scope, dragSettings.onStart, event, ui);
,
stop: function(event, ui)
angular.element(jqyouiOptions.helper ? ui.helper : this).css('z-index', zIndex);
ngDragDropService.callEventCallback(scope, dragSettings.onStop, event, ui);
,
drag: function(event, ui)
ngDragDropService.callEventCallback(scope, dragSettings.onDrag, event, ui);
);
else
element.draggable(disabled: true);
;
scope.$watch(function() return scope.$eval(attrs.drag); , updateDraggable);
updateDraggable();
element.on('$destroy', function()
element.draggable('destroy');
);
;
])
.directive('jqyouiDroppable', ['ngDragDropService', function(ngDragDropService)
return
restrict: 'A',
priority: 1,
link: function(scope, element, attrs)
var dropSettings;
var updateDroppable = function(newValue, oldValue)
if (newValue)
dropSettings = scope.$eval(angular.element(element).attr('jqyoui-droppable') || angular.element(element).attr('data-jqyoui-droppable')) || ;
element
.droppable(disabled: false)
.droppable(scope.$eval(attrs.jqyouiOptions) || )
.droppable(
over: function(event, ui)
ngDragDropService.callEventCallback(scope, dropSettings.onOver, event, ui);
,
out: function(event, ui)
ngDragDropService.callEventCallback(scope, dropSettings.onOut, event, ui);
,
drop: function(event, ui)
if (angular.element(ui.draggable).ngattr('ng-model') && attrs.ngModel)
ngDragDropService.invokeDrop(scope, angular.element(ui.draggable), angular.element(this), event, ui);
else
ngDragDropService.callEventCallback(scope, dropSettings.onDrop, event, ui);
);
else
element.droppable(disabled: true);
;
scope.$watch(function() return scope.$eval(attrs.drop); , updateDroppable);
updateDroppable();
element.on('$destroy', function()
element.droppable('destroy');
);
;
])
.directive('uiCalendar', ['uiCalendarConfig', '$locale', function(uiCalendarConfig, $locale)
return
restrict: 'A',
scope: eventSources:'=ngModel',calendarWatchEvent: '&',
controller: 'uiCalendarCtrl',
link: function(scope, elm, attrs, controller)
var sources = scope.eventSources,
sourcesChanged = false,
eventSourcesWatcher = controller.changeWatcher(sources, controller.sourcesFingerprint),
eventsWatcher = controller.changeWatcher(controller.allEvents, controller.eventsFingerprint),
options = null;
function getOptions()
var calendarSettings = attrs.uiCalendar ? scope.$parent.$eval(attrs.uiCalendar) : ,
fullCalendarConfig;
fullCalendarConfig = controller.getFullCalendarConfig(calendarSettings, uiCalendarConfig);
options = eventSources: sources ;
angular.extend(options, fullCalendarConfig);
var options2 = ;
for(var o in options)
if(o !== 'eventSources')
options2[o] = options[o];
return JSON.stringify(options2);
scope.destroy = function()
if(attrs.calendar)
scope.calendar = scope.$parent[attrs.calendar] = elm.html('');
else
scope.calendar = elm.html('');
;
scope.init = function()
scope.calendar.fullCalendar(options);
;
eventSourcesWatcher.onAdded = function(source)
scope.calendar.fullCalendar('addEventSource', source);
sourcesChanged = true;
;
eventSourcesWatcher.onRemoved = function(source)
scope.calendar.fullCalendar('removeEventSource', source);
sourcesChanged = true;
;
eventsWatcher.onAdded = function(event)
scope.calendar.fullCalendar('renderEvent', event, true);
;
eventsWatcher.onRemoved = function(event)
scope.calendar.fullCalendar('removeEvents', function(e) return e === event; );
;
eventsWatcher.onChanged = function(event)
scope.calendar.fullCalendar('updateEvent', event);
;
eventSourcesWatcher.subscribe(scope);
eventsWatcher.subscribe(scope, function(newTokens, oldTokens)
if (sourcesChanged === true)
sourcesChanged = false;
// prevent incremental updates in this case
return false;
);
scope.$watch(getOptions, function(newO,oldO)
scope.destroy();
scope.init();
);
;
])
.service('ngDragDropService', ['$timeout', '$parse', function($timeout, $parse)
this.callEventCallback = function (scope, callbackName, event, ui)
if (!callbackName) return;
var objExtract = extract(callbackName),
callback = objExtract.callback,
constructor = objExtract.constructor,
args = [event, ui].concat(objExtract.args);
// call either $scoped method i.e. $scope.dropCallback or constructor's method i.e. this.dropCallback
scope.$apply((scope[callback] || scope[constructor][callback]).apply(scope, args));
function extract(callbackName)
var atStartBracket = callbackName.indexOf('(') !== -1 ? callbackName.indexOf('(') : callbackName.length,
atEndBracket = callbackName.lastIndexOf(')') !== -1 ? callbackName.lastIndexOf(')') : callbackName.length,
args = callbackName.substring(atStartBracket + 1, atEndBracket), // matching function arguments inside brackets
constructor = callbackName.match(/^[^.]+.\s*/)[0].slice(0, -1); // matching a string upto a dot to check ctrl as syntax
constructor = scope[constructor] && typeof scope[constructor].constructor === 'function' ? constructor : null;
return
callback: callbackName.substring(constructor && constructor.length + 1 || 0, atStartBracket),
args: (args && args.split(',') || []).map(function(item) return $parse(item)(scope); ),
constructor: constructor
;
this.invokeDrop = function (scope, $draggable, $droppable, event, ui)
var dragModel = '',
dropModel = '',
dragSettings = ,
dropSettings = ,
jqyoui_pos = null,
dragItem = ,
dropItem = ,
dragModelValue,
dropModelValue,
$droppableDraggable = null,
droppableScope = $droppable.scope(),
draggableScope = $draggable.scope();
dragModel = $draggable.ngattr('ng-model');
dropModel = $droppable.ngattr('ng-model');
dragModelValue = draggableScope.$eval(dragModel);
dropModelValue = droppableScope.$eval(dropModel);
$droppableDraggable = $droppable.find('[jqyoui-draggable]:last,[data-jqyoui-draggable]:last');
dropSettings = droppableScope.$eval($droppable.attr('jqyoui-droppable') || $droppable.attr('data-jqyoui-droppable')) || [];
dragSettings = draggableScope.$eval($draggable.attr('jqyoui-draggable') || $draggable.attr('data-jqyoui-draggable')) || [];
// Helps pick up the right item
dragSettings.index = this.fixIndex(draggableScope, dragSettings, dragModelValue);
dropSettings.index = this.fixIndex(droppableScope, dropSettings, dropModelValue);
jqyoui_pos = angular.isArray(dragModelValue) ? dragSettings.index : null;
dragItem = angular.isArray(dragModelValue) ? dragModelValue[jqyoui_pos] : dragModelValue;
if (angular.isArray(dropModelValue) && dropSettings && dropSettings.index !== undefined)
dropItem = dropModelValue[dropSettings.index];
else if (!angular.isArray(dropModelValue))
dropItem = dropModelValue;
else
dropItem = ;
if (dragSettings.animate === true)
this.move($draggable, $droppableDraggable.length > 0 ? $droppableDraggable : $droppable, null, 'fast', dropSettings, null);
this.move($droppableDraggable.length > 0 && !dropSettings.multiple ? $droppableDraggable : [], $draggable.parent('[jqyoui-droppable],[data-jqyoui-droppable]'), angular.startXY, 'fast', dropSettings, angular.bind(this, function()
$timeout(angular.bind(this, function()
// Do not move this into move() to avoid flickering issue
$draggable.css('position': 'relative', 'left': '', 'top': '');
// Angular v1.2 uses ng-hide to hide an element not display property
// so we've to manually remove display:none set in this.move()
$droppableDraggable.css('position': 'relative', 'left': '', 'top': '', 'display': '');
this.mutateDraggable(draggableScope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable);
this.mutateDroppable(droppableScope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos);
this.callEventCallback(droppableScope, dropSettings.onDrop, event, ui);
));
));
else
$timeout(angular.bind(this, function()
this.mutateDraggable(draggableScope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable);
this.mutateDroppable(droppableScope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos);
this.callEventCallback(droppableScope, dropSettings.onDrop, event, ui);
));
;
this.move = function($fromEl, $toEl, toPos, duration, dropSettings, callback)
if ($fromEl.length === 0)
if (callback)
window.setTimeout(function()
callback();
, 300);
return false;
var zIndex = 9999,
fromPos = $fromEl.offset(),
wasVisible = $toEl && $toEl.is(':visible'),
hadNgHideCls = $toEl.hasClass('ng-hide');
if (toPos === null && $toEl.length > 0)
if (($toEl.attr('jqyoui-draggable') || $toEl.attr('data-jqyoui-draggable')) !== undefined && $toEl.ngattr('ng-model') !== undefined && $toEl.is(':visible') && dropSettings && dropSettings.multiple)
toPos = $toEl.offset();
if (dropSettings.stack === false)
toPos.left+= $toEl.outerWidth(true);
else
toPos.top+= $toEl.outerHeight(true);
else
// Angular v1.2 uses ng-hide to hide an element
// so we've to remove it in order to grab its position
if (hadNgHideCls) $toEl.removeClass('ng-hide');
toPos = $toEl.css('visibility': 'hidden', 'display': 'block').offset();
$toEl.css('visibility': '','display': wasVisible ? 'block' : 'none');
$fromEl.css('position': 'absolute', 'z-index': zIndex)
.css(fromPos)
.animate(toPos, duration, function()
// Angular v1.2 uses ng-hide to hide an element
// and as we remove it above, we've to put it back to
// hide the element (while swapping) if it was hidden already
// because we remove the display:none in this.invokeDrop()
if (hadNgHideCls) $toEl.addClass('ng-hide');
if (callback) callback();
);
;
this.mutateDroppable = function(scope, dropSettings, dragSettings, dropModel, dragItem, jqyoui_pos)
var dropModelValue = scope.$eval(dropModel);
scope.dndDragItem = dragItem;
if (angular.isArray(dropModelValue))
if (dropSettings && dropSettings.index >= 0)
dropModelValue[dropSettings.index] = dragItem;
else
dropModelValue.push(dragItem);
if (dragSettings && dragSettings.placeholder === true)
dropModelValue[dropModelValue.length - 1]['jqyoui_pos'] = jqyoui_pos;
else
$parse(dropModel + ' = dndDragItem')(scope);
if (dragSettings && dragSettings.placeholder === true)
dropModelValue['jqyoui_pos'] = jqyoui_pos;
;
this.mutateDraggable = function(scope, dropSettings, dragSettings, dragModel, dropModel, dropItem, $draggable)
var isEmpty = angular.equals(angular.copy(dropItem), ),
dragModelValue = scope.$eval(dragModel);
scope.dndDropItem = dropItem;
if (dragSettings && dragSettings.placeholder)
if (dragSettings.placeholder != 'keep')
if (angular.isArray(dragModelValue) && dragSettings.index !== undefined)
dragModelValue[dragSettings.index] = dropItem;
else
$parse(dragModel + ' = dndDropItem')(scope);
else
if (angular.isArray(dragModelValue))
if (isEmpty)
if (dragSettings && ( dragSettings.placeholder !== true && dragSettings.placeholder !== 'keep' ))
dragModelValue.splice(dragSettings.index, 1);
else
dragModelValue[dragSettings.index] = dropItem;
else
// Fix: LIST(object) to LIST(array) - model does not get updated using just scope[dragModel] = ...
// P.S.: Could not figure out why it happened
$parse(dragModel + ' = dndDropItem')(scope);
if (scope.$parent)
$parse(dragModel + ' = dndDropItem')(scope.$parent);
$draggable.css('z-index': '', 'left': '', 'top': '');
;
this.fixIndex = function(scope, settings, modelValue)
if (settings.applyFilter && angular.isArray(modelValue) && modelValue.length > 0)
var dragModelValueFiltered = scope[settings.applyFilter](),
lookup = dragModelValueFiltered[settings.index],
actualIndex = undefined;
modelValue.forEach(function(item, i)
if (angular.equals(item, lookup))
actualIndex = i;
);
return actualIndex;
return settings.index;
;
])
;
【问题讨论】:
你有没有让这个工作? 是的,安德斯的回答是正确的 【参考方案1】:没有使用 angular-dragdrop,但是文档说配置对象应该包含一个 onDrop 属性。尝试将jqyoui-droppable="multiple:true"
替换为jqyoui-droppable="multiple:true, onDrop: 'drop'"
。 angular-dragdrop 似乎期望 onDrop
是一个字符串,其中包含作用域上的函数名称。
【讨论】:
感谢安德斯!我正在尝试触发日历的 drop 事件,但它仍然非常安静:( 所以你的意思是你的 $scope.drop() 方法根本没有被调用,即使我提到了改变? 是的 $scope.drop 是由于回调而被调用的,但它应该从 $scope.uiConfig 中的 drop 函数中调用,因此我可以获得元素被放置到的日历日期。 【参考方案2】:实现此目的的另一种方法是使用可拖动的 JqueryUI。创建指令并通过“elem”传递属性。您也可以将标题作为属性包含在内。
.directive('dragMe', function()
return
restrict: 'A',
link: function(scope, elem, attr, ctrl)
elem.data('event',
title: $.trim($(elem).text()), // use the element's text as the event title
stick: true // maintain when user navigates (see docs on the renderEvent method)
);
elem.draggable(
zIndex: 999,
revert: true, // will cause the event to go back to its
revertDuration: 0 // original position after the drag
);
;
)
【讨论】:
以上是关于如何仅使用角度指令将放置元素拖放到日历中的主要内容,如果未能解决你的问题,请参考以下文章