// Generated by CoffeeScript 2.0.0-beta4 (function() { // ------------------------------------------------------------------------ // 變數與常數設置 // ------------------------------------------------------------------------ // 模組名稱。 var Attribute, ClassName, EVENT_NAMESPACE, Error, Event, MODULE_NAMESPACE, Metadata, Mode, NAME, Order, Selector, Settings; NAME = 'sortable'; // 模組事件鍵名。 EVENT_NAMESPACE = `.${NAME}`; // 模組命名空間。 MODULE_NAMESPACE = `module-${NAME}`; // 滑鼠拖曳出外,回來可以點擊依然可以放到 pull 容器 // 模組設定。 Settings = { // 消音所有提示,甚至是錯誤訊息。 silent: false, // 顯示除錯訊息。 debug: true, // 監聽 DOM 結構異動並自動重整快取。 observeChanges: true, // 指定的拖曳把手選擇器,設置為 `false` 則為整個元素皆可拖曳。 handle: false, // 到拖曳開始之前必須按住的指定毫秒數,避免點擊成為不必要的拖曳。 delay: 350, // 是否能在相同拖放排序內重新排序。 sort: true, // 群組名稱,相同的名稱拖放排序清單可以交替其項目。 group: false, // 此拖放排序的支援模式。(`all` 表示可拖放、`put` 表示僅可放入、`pull` 表示僅可移出、`all` 表示都可) mode: 'all', // 這個拖放排序是否已垂直清單為主,改為 `false` 會有利於水平清單。 vertical: true, // 當拖拉開始時所會呼叫的回呼函式。 onDragStart: (value) => {}, // 當拖拉途中所會呼叫的回呼函式,間隔是 350 毫秒。 onDrag: (value) => {}, // 當拖拉結束並丟下元素時所會呼叫的回呼函式。 onDrop: (value) => {}, // 當放下被禁止(如:範圍外、被回呼函式拒絕)時所會呼叫的函式。 onDeny: (value) => {}, // 當放下時跟一開始沒有差異時所會呼叫的回呼函式。 onCancel: (value) => {}, // 當有變動(新增、移除、重新排序)時所會呼叫的回呼函式。 onChange: (value) => {}, // 當項目新增時所會呼叫的回呼函式,回傳 `false` 表示不接受此新增。 onAdd: (value) => {}, // 當項目被移出時所會呼叫的回呼函式。 onRemove: (value) => {} }; // 事件名稱。 Event = { DRAGSTART: `dragstart${EVENT_NAMESPACE}`, DRAG: `drag${EVENT_NAMESPACE}`, DROP: `drop${EVENT_NAMESPACE}`, DENY: `deny${EVENT_NAMESPACE}`, CANCEL: `cancel${EVENT_NAMESPACE}`, CHANGE: `change${EVENT_NAMESPACE}`, ADD: `add${EVENT_NAMESPACE}`, REMOVE: `remove${EVENT_NAMESPACE}`, MOUSEMOVE: `mousemove${EVENT_NAMESPACE}`, MOUSEUP: `mouseup${EVENT_NAMESPACE}`, MOUSEDOWN: `mousedown${EVENT_NAMESPACE}` }; // 中繼資料名稱。 Metadata = { X_OFFSET: 'xOffset', Y_OFFSET: 'yOffset', ENABLE: 'enable' }; // 模式 = Mode = { PULL: 'pull', PUT: 'put', ALL: 'all' }; // 順序。 Order = { AFTER: 'after', BEFORE: 'before' }; // 元素標籤。 Attribute = { GROUP: 'data-draggable-group', CONTAINER: 'data-draggable-container', DRAGGABLE: 'data-draggable', DRAGGING: 'data-draggable-dragging', VALUE: 'data-value', GHOST: 'data-draggable-ghost', PLACEHOLDER: 'data-draggable-placeholder', NATIVE_DRAGGABLE: 'draggable', HIDDEN: 'hidden' }; // 樣式名稱。 ClassName = {}; // 選擇器名稱。 Selector = { BODY: 'body', NATIVE_DRAGGABLE: `[${Attribute.NATIVE_DRAGGABLE}]`, DRAGGING: `[${Attribute.DRAGGING}]`, DRAGGABLE: `[${Attribute.DRAGGABLE}]`, GHOST: `[${Attribute.GHOST}]`, PLACEHOLDER: `[${Attribute.PLACEHOLDER}]`, CONTAINER: `[${Attribute.CONTAINER}]`, PLACEHOLDER: `[${Attribute.PLACEHOLDER}]`, GROUP: `[${Attribute.GROUP}]`, HIDDEN_DRAGGABLE: `[${Attribute.DRAGGABLE}][${Attribute.HIDDEN}]`, TRUE_DRAGGABLE: `[${Attribute.DRAGGABLE}]:not([${Attribute.HIDDEN}])`, DRAGGABLE_VALUE: (value) => { return `[${Attribute.VALUE}='${value}']`; } }; // 錯誤訊息。 Error = {}; // ------------------------------------------------------------------------ // 模組註冊 // ------------------------------------------------------------------------ ts.register({NAME, MODULE_NAMESPACE, Error, Settings}, ({$allModules, $this, element, debug, settings}) => { var $original, $placeholder, draggingInterval, draggingTimer, module; // ------------------------------------------------------------------------ // 區域變數 // ------------------------------------------------------------------------ $original = ts(); $placeholder = ts(); draggingTimer = null; draggingInterval = 350; // ------------------------------------------------------------------------ // 模組定義 // ------------------------------------------------------------------------ return module = { sort: (values) => { var i, len, value; for (i = 0, len = values.length; i < len; i++) { value = values[i]; $this.find(Selector.DRAGGABLE_VALUE(value)).appendTo(element); } return $allModules; }, enable: () => { $this.data(Metadata.ENABLE, true); return $allModules; }, disable: () => { $this.data(Metadata.ENABLE, false); return $allModules; }, start: { dragging: () => { draggingTimer = setInterval(() => { return module.trigger.drag(); }, draggingInterval); return $original.attr(Attribute.DRAGGING, 'true'); } }, stop: { dragging: () => { return clearInterval(draggingTimer); } }, reset: { dragging: () => { return ts(Selector.DRAGGING).removeAttr(Attribute.DRAGGING); } }, hide: { original: () => { return $original.attr(Attribute.HIDDEN, 'hidden'); } }, unhide: { original: () => { return $(Selector.HIDDEN_DRAGGABLE).removeAttr(Attribute.HIDDEN); } }, set: { group: (name) => { return $this.attr(Attribute.GROUP, settings.group); } }, get: { $dragging: () => { return ts(Selector.DRAGGING); }, $draggable: (element) => { return ts(element).closest(Selector.DRAGGABLE); }, last: { $item: () => { return $this.find(Selector.DRAGGABLE).last(); } }, dragging: { element: () => { return ts(Selector.HIDDEN_DRAGGABLE).get(); }, value: () => { return ts(Selector.HIDDEN_DRAGGABLE).attr(Attribute.VALUE); } }, container: (element) => { return ts(element).closest(Selector.CONTAINER); }, group: { name: () => { return $this.attr(Attribute.GROUP); } }, mode: () => { return settings.mode; }, sort: () => { return settings.sort; }, value: () => { var values; values = []; $this.find(Selector.DRAGGABLE).each(function() { var value; value = ts(this).attr(Attribute.VALUE); if (value) { return values.push(value); } }); return values; } }, is: { draggable: (element) => { return ts(element).attr(Attribute.DRAGGABLE) === 'true'; }, child: (element) => { return $this.contains(element); }, sortable: () => { return settings.sort === true; }, vertical: () => { return settings.vertical === true; }, enable: () => { return $this.data(Metadata.ENABLE); }, disable: () => { return !$this.data(Metadata.ENABLE); }, same: { group: ($target) => { var $container, name; $container = module.get.container($target); if ($container.is(element)) { return true; } name = $container.sortable('get group name'); return name !== null && name === module.get.group.name(); }, container: ($container) => { return $container.is(element); } }, handle: (element) => { return ts(element).is(settings.handle); } }, has: { dragging: () => { return $this.find(Selector.DRAGGING).exists(); }, placeholder: () => { return $this.find(Selector.PLACEHOLDER).exists(); }, item: () => { return $this.find(Selector.DRAGGABLE).exists(); }, handle: () => { return settings.handle !== false; } }, create: { ghost: (x, y) => { return $original.clone().attr(Attribute.GHOST, 'true').data({ [`${Metadata.X_OFFSET}`]: x - $original.rect().x, [`${Metadata.Y_OFFSET}`]: y - $original.rect().y }).css({ width: $original.rect().width, height: $original.rect().height }).appendTo(Selector.BODY); }, placeholder: () => { return $placeholder = $original.clone().attr(Attribute.PLACEHOLDER, 'true').removeAttr(Attribute.DRAGGING).appendTo(Selector.BODY); } }, remove: { ghost: () => { return ts(Selector.GHOST).remove(); }, placeholder: () => { return ts(Selector.PLACEHOLDER).remove(); } }, move: { ghost: (x, y) => { var $ghost; $ghost = ts(Selector.GHOST); return $ghost.css({ top: y - $ghost.data(Metadata.Y_OFFSET), left: x - $ghost.data(Metadata.X_OFFSET) }); }, placeholder: (x, y) => { var $container, $draggable, $last, $pointing, isAllOrSin, isTanOrCos, isVertical; $pointing = ts.fromPoint(x, y); $draggable = $pointing.closest(Selector.DRAGGABLE); $container = module.get.container($draggable); if ($draggable.exists()) { isAllOrSin = y - $draggable.rect().y < $draggable.rect().height / 2; isTanOrCos = x - $draggable.rect().x < $draggable.rect().width / 2; isVertical = $container.sortable('is vertical'); if ((isAllOrSin && isVertical) || (isTanOrCos && !isVertical)) { $placeholder.insertBefore($draggable); } else { $placeholder.insertAfter($draggable); } return; } $container = module.get.container($pointing); $draggable = $container.find(Selector.TRUE_DRAGGABLE); if (!$container.exists()) { return; } if (!$draggable.exists()) { module.insert.placeholder($container); return; } $last = $draggable.last(); if ($last.exists()) { return module.append.placeholder(Order.AFTER, $last); } }, original: () => { return module.get.$dragging().insertAfter(ts(Selector.PLACEHOLDER)); } }, append: { placeholder: (order, to) => { switch (order) { case Order.BEFORE: return $placeholder.insertBefore(to); case Order.AFTER: return $placeholder.insertAfter(to); } } }, insert: { placeholder: (to) => { return $placeholder.appendTo(to); } }, trigger: { dragStart: () => { return $this.trigger(Event.DRAGSTART, module.get.dragging.element(), module.get.dragging.value()); }, drag: () => { return $this.trigger(Event.DRAG, module.get.dragging.element(), module.get.dragging.value()); }, drop: () => { return $this.trigger(Event.DROP, module.get.dragging.element(), module.get.dragging.value()); }, deny: () => { return $this.trigger(Event.DENY, module.get.dragging.element(), module.get.dragging.value()); }, cancel: () => { return $this.trigger(Event.CANCEL, module.get.dragging.element(), module.get.dragging.value()); }, change: () => { return $this.trigger(Event.CHANGE, module.get.dragging.element(), module.get.dragging.value()); }, add: (valueElement, value) => { debug('發生 ADD 事件', valueElement, value); return settings.onAdd.call(valueElement, value); }, remove: () => { return $this.trigger(Event.REMOVE, module.get.dragging.element(), module.get.dragging.value()); } }, unbind: { mousemove: () => { return ts(Selector.BODY).off(Event.MOUSEMOVE); } }, bind: { mousemove: () => { return ts(Selector.BODY).on(Event.MOUSEMOVE, (event) => { var $container, $element, $placeholderContainer, hasContainerItem, hasPlaceholder, isContainerSortable, isPullContainer, isPutContainer, isSameContainer, isSortable; debug('發生 MOUSEMOVE 事件', element, this); if (module.is.disable()) { return; } module.move.ghost(event.clientX, event.clientY); if (!module.has.dragging()) { return; } $element = ts.fromPoint(event.clientX, event.clientY); $container = module.get.container($element); if (!module.is.same.group($element)) { return; } if ($container.sortable('is disable')) { return; } hasPlaceholder = module.has.placeholder(); isSortable = module.is.sortable(); isPullContainer = $container.sortable('get mode') === Mode.PULL; isSameContainer = module.is.same.container($container); switch (false) { case !isPullContainer: return; case !(!isSortable && isSameContainer && hasPlaceholder): return; } $placeholderContainer = module.get.container($placeholder); hasContainerItem = $container.sortable('has item'); isContainerSortable = $container.sortable('is sortable'); isPutContainer = $placeholderContainer.sortable('get mode') === Mode.PUT; isSortable = $placeholderContainer.sortable('is sortable'); switch (false) { case !(!isSameContainer && isPutContainer && isSortable): return; case !(!isSameContainer && !isContainerSortable): if (!hasContainerItem) { module.insert.placeholder($container); } else { module.append.placeholder(Order.AFTER, $container.sortable('get last $item')); } return; case !(!isSortable && isSameContainer && !hasPlaceholder): module.append.placeholder(Order.AFTER, $original); return; } return module.move.placeholder(event.clientX, event.clientY); }); }, events: () => { $this.on(Event.DRAGSTART, (event, context, value) => { debug('發生 DRAGSTART 事件', context, value); return settings.onDragStart.call(context, event, value); }); $this.on(Event.DRAG, (event, context, value) => { debug('發生 DRAG 事件', context, value); return settings.onDrag.call(context, event, value); }); $this.on(Event.DROP, (event, context, value) => { debug('發生 DROP 事件', context, value); return settings.onDrop.call(context, event, value); }); $this.on(Event.DENY, (event, context, value) => { debug('發生 DENY 事件', context, value); return settings.onDeny.call(context, event, value); }); $this.on(Event.CANCEL, (event, context, value) => { debug('發生 CANCEL 事件', context, value); return settings.onCancel.call(context, event, value); }); $this.on(Event.CHANGE, (event, context, value) => { debug('發生 CHANGE 事件', context, value); return settings.onChange.call(context, event, value); }); $this.on(Event.REMOVE, (event, context, value) => { debug('發生 REMOVE 事件', context, value); return settings.onRemove.call(context, event, value); }); ts(Selector.BODY).on(Event.MOUSEDOWN, (event) => { var $target; debug('發生 MOUSEDOWN 事件', element, this); if (module.is.disable()) { return; } $target = ts(event.target); if (module.has.handle() && !module.is.handle($target)) { return; } if (!module.is.draggable($target)) { $target = module.get.$draggable($target); } switch (false) { case !!$target.exists(): return; case !!module.is.child($target): return; case !(module.get.mode() === Mode.PUT && !module.is.sortable()): return; } $original = $target; module.trigger.dragStart(); module.start.dragging(); module.create.ghost(event.clientX, event.clientY); module.move.ghost(event.clientX, event.clientY); module.create.placeholder(); module.move.placeholder(event.clientX, event.clientY); module.hide.original(); return module.bind.mousemove(); }); return ts(Selector.BODY).on(Event.MOUSEUP, (event) => { var $container, newValue, oldValue; debug('發生 MOUSEUP 事件', element, this); if (module.is.disable()) { return; } module.stop.dragging(); module.remove.ghost(); module.unbind.mousemove(); if (!module.has.dragging()) { return; } module.trigger.drop(); $container = module.get.container(event.target); if ($container.exists() && module.is.same.container($container)) { oldValue = module.get.value().toString(); module.move.original(); newValue = module.get.value().toString(); if (oldValue !== '' || newValue !== '') { if (oldValue === newValue) { module.trigger.cancel(); } else { module.trigger.change(); } } } else { if ($container.sortable('trigger add', module.get.dragging.element(), module.get.dragging.value())) { module.move.original(); $container.sortable('trigger change'); module.trigger.remove(); module.trigger.change(); } else { module.trigger.cancel(); module.trigger.deny(); } } module.remove.placeholder(); module.unhide.original(); return module.reset.dragging(); }); } }, // ------------------------------------------------------------------------ // 基礎方法 // ------------------------------------------------------------------------ initialize: () => { debug('初始化拖放排序', element); module.bind.events(); module.enable(); $this.attr(Attribute.CONTAINER, true).find(Selector.NATIVE_DRAGGABLE).removeAttr(Attribute.NATIVE_DRAGGABLE).attr(Attribute.DRAGGABLE, 'true'); if (settings.group !== false) { return module.set.group(settings.group); } }, instantiate: () => { return debug('實例化拖放排序', element); }, refresh: () => { return $allModules; }, destroy: () => { debug('摧毀拖放排序', element); $this.removeData(MODULE_NAMESPACE).off(EVENT_NAMESPACE); return $allModules; } }; }); }).call(this);