API Docs for: 3.18.1
Show:

File: dd/js/constrain.js

  1.  
  2. /**
  3. * The Drag & Drop Utility allows you to create a draggable interface efficiently,
  4. * buffering you from browser-level abnormalities and enabling you to focus on the interesting
  5. * logic surrounding your particular implementation. This component enables you to create a
  6. * variety of standard draggable objects with just a few lines of code and then,
  7. * using its extensive API, add your own specific implementation logic.
  8. * @module dd
  9. * @main dd
  10. * @submodule dd-constrain
  11. */
  12. /**
  13. * Plugin for the dd-drag module to add the constraining methods to it.
  14. * It supports constraining to a node or viewport. It supports tick based moves and XY axis constraints.
  15. * @class DDConstrained
  16. * @extends Base
  17. * @constructor
  18. * @namespace Plugin
  19. */
  20.  
  21. var DRAG_NODE = 'dragNode',
  22. OFFSET_HEIGHT = 'offsetHeight',
  23. OFFSET_WIDTH = 'offsetWidth',
  24. HOST = 'host',
  25. TICK_X_ARRAY = 'tickXArray',
  26. TICK_Y_ARRAY = 'tickYArray',
  27. DDM = Y.DD.DDM,
  28. TOP = 'top',
  29. RIGHT = 'right',
  30. BOTTOM = 'bottom',
  31. LEFT = 'left',
  32. VIEW = 'view',
  33. proto = null,
  34.  
  35. /**
  36. * Fires when this node is aligned with the tickX value.
  37. * @event drag:tickAlignX
  38. * @param {EventFacade} event An Event Facade object
  39. * @type {CustomEvent}
  40. */
  41. EV_TICK_ALIGN_X = 'drag:tickAlignX',
  42.  
  43. /**
  44. * Fires when this node is aligned with the tickY value.
  45. * @event drag:tickAlignY
  46. * @param {EventFacade} event An Event Facade object
  47. * @type {CustomEvent}
  48. */
  49. EV_TICK_ALIGN_Y = 'drag:tickAlignY',
  50.  
  51. C = function() {
  52. this._lazyAddAttrs = false;
  53. C.superclass.constructor.apply(this, arguments);
  54. };
  55.  
  56. C.NAME = 'ddConstrained';
  57. /**
  58. * The Constrained instance will be placed on the Drag instance under the con namespace.
  59. * @property NS
  60. * @default con
  61. * @readonly
  62. * @protected
  63. * @static
  64. * @type {String}
  65. */
  66. C.NS = 'con';
  67.  
  68. C.ATTRS = {
  69. host: {
  70. },
  71. /**
  72. * Stick the drag movement to the X-Axis. Default: false
  73. * @attribute stickX
  74. * @type Boolean
  75. */
  76. stickX: {
  77. value: false
  78. },
  79. /**
  80. * Stick the drag movement to the Y-Axis
  81. * @type Boolean
  82. * @attribute stickY
  83. */
  84. stickY: {
  85. value: false
  86. },
  87. /**
  88. * The X tick offset the drag node should snap to on each drag move. False for no ticks. Default: false
  89. * @type Number/false
  90. * @attribute tickX
  91. */
  92. tickX: {
  93. value: false
  94. },
  95. /**
  96. * The Y tick offset the drag node should snap to on each drag move. False for no ticks. Default: false
  97. * @type Number/false
  98. * @attribute tickY
  99. */
  100. tickY: {
  101. value: false
  102. },
  103. /**
  104. * An array of page coordinates to use as X ticks for drag movement.
  105. * @type Array
  106. * @attribute tickXArray
  107. */
  108. tickXArray: {
  109. value: false
  110. },
  111. /**
  112. * An array of page coordinates to use as Y ticks for drag movement.
  113. * @type Array
  114. * @attribute tickYArray
  115. */
  116. tickYArray: {
  117. value: false
  118. },
  119. /**
  120. * CSS style string for the gutter of a region (supports negative values): '5 0'
  121. * (sets top and bottom to 5px, left and right to 0px), '1 2 3 4' (top 1px, right 2px, bottom 3px, left 4px)
  122. * @attribute gutter
  123. * @type String
  124. */
  125. gutter: {
  126. value: '0',
  127. setter: function(gutter) {
  128. return Y.DD.DDM.cssSizestoObject(gutter);
  129. }
  130. },
  131. /**
  132. * Will attempt to constrain the drag node to the boundaries. Arguments:<br>
  133. * 'view': Contrain to Viewport<br>
  134. * '#selector_string': Constrain to this node<br>
  135. * '{Region Object}': An Object Literal containing a valid region (top, right, bottom, left) of page positions
  136. * @attribute constrain
  137. * @type {String/Object/Node}
  138. */
  139. constrain: {
  140. value: VIEW,
  141. setter: function(con) {
  142. var node = Y.one(con);
  143. if (node) {
  144. con = node;
  145. }
  146. return con;
  147. }
  148. },
  149. /**
  150. * An Object Literal containing a valid region (top, right, bottom, left) of page positions to constrain the drag node to.
  151. * @deprecated
  152. * @attribute constrain2region
  153. * @type Object
  154. */
  155. constrain2region: {
  156. setter: function(r) {
  157. return this.set('constrain', r);
  158. }
  159. },
  160. /**
  161. * Will attempt to constrain the drag node to the boundaries of this node.
  162. * @deprecated
  163. * @attribute constrain2node
  164. * @type Object
  165. */
  166. constrain2node: {
  167. setter: function(n) {
  168. return this.set('constrain', Y.one(n));
  169. }
  170. },
  171. /**
  172. * Will attempt to constrain the drag node to the boundaries of the viewport region.
  173. * @deprecated
  174. * @attribute constrain2view
  175. * @type Object
  176. */
  177. constrain2view: {
  178. setter: function() {
  179. return this.set('constrain', VIEW);
  180. }
  181. },
  182. /**
  183. * Should the region be cached for performace. Default: true
  184. * @attribute cacheRegion
  185. * @type Boolean
  186. */
  187. cacheRegion: {
  188. value: true
  189. }
  190. };
  191.  
  192. proto = {
  193. _lastTickXFired: null,
  194. _lastTickYFired: null,
  195.  
  196. initializer: function() {
  197. this._createEvents();
  198.  
  199. this._eventHandles = [
  200. this.get(HOST).on('drag:end', Y.bind(this._handleEnd, this)),
  201. this.get(HOST).on('drag:start', Y.bind(this._handleStart, this)),
  202. this.get(HOST).after('drag:align', Y.bind(this.align, this)),
  203. this.get(HOST).after('drag:drag', Y.bind(this.drag, this))
  204. ];
  205. },
  206. destructor: function() {
  207. Y.Array.each(
  208. this._eventHandles,
  209. function(handle) {
  210. handle.detach();
  211. }
  212. );
  213.  
  214. this._eventHandles.length = 0;
  215. },
  216. /**
  217. * This method creates all the events for this Event Target and publishes them so we get Event Bubbling.
  218. * @private
  219. * @method _createEvents
  220. */
  221. _createEvents: function() {
  222. var ev = [
  223. EV_TICK_ALIGN_X,
  224. EV_TICK_ALIGN_Y
  225. ];
  226.  
  227. Y.Array.each(ev, function(v) {
  228. this.publish(v, {
  229. type: v,
  230. emitFacade: true,
  231. bubbles: true,
  232. queuable: false,
  233. prefix: 'drag'
  234. });
  235. }, this);
  236. },
  237. /**
  238. * Fires on drag:end
  239. * @private
  240. * @method _handleEnd
  241. */
  242. _handleEnd: function() {
  243. this._lastTickYFired = null;
  244. this._lastTickXFired = null;
  245. },
  246. /**
  247. * Fires on drag:start and clears the _regionCache
  248. * @private
  249. * @method _handleStart
  250. */
  251. _handleStart: function() {
  252. this.resetCache();
  253. },
  254. /**
  255. * Store a cache of the region that we are constraining to
  256. * @private
  257. * @property _regionCache
  258. * @type Object
  259. */
  260. _regionCache: null,
  261. /**
  262. * Get's the region and caches it, called from window.resize and when the cache is null
  263. * @private
  264. * @method _cacheRegion
  265. */
  266. _cacheRegion: function() {
  267. this._regionCache = this.get('constrain').get('region');
  268. },
  269. /**
  270. * Reset the internal region cache.
  271. * @method resetCache
  272. */
  273. resetCache: function() {
  274. this._regionCache = null;
  275. },
  276. /**
  277. * Standardizes the 'constraint' attribute
  278. * @private
  279. * @method _getConstraint
  280. */
  281. _getConstraint: function() {
  282. var con = this.get('constrain'),
  283. g = this.get('gutter'),
  284. region;
  285.  
  286. if (con) {
  287. if (con instanceof Y.Node) {
  288. if (!this._regionCache) {
  289. this._eventHandles.push(Y.on('resize', Y.bind(this._cacheRegion, this), Y.config.win));
  290. this._cacheRegion();
  291. }
  292. region = Y.clone(this._regionCache);
  293. if (!this.get('cacheRegion')) {
  294. this.resetCache();
  295. }
  296. } else if (Y.Lang.isObject(con)) {
  297. region = Y.clone(con);
  298. }
  299. }
  300. if (!con || !region) {
  301. con = VIEW;
  302. }
  303. if (con === VIEW) {
  304. region = this.get(HOST).get(DRAG_NODE).get('viewportRegion');
  305. }
  306.  
  307. Y.Object.each(g, function(i, n) {
  308. if ((n === RIGHT) || (n === BOTTOM)) {
  309. region[n] -= i;
  310. } else {
  311. region[n] += i;
  312. }
  313. });
  314. return region;
  315. },
  316.  
  317. /**
  318. * Get the active region: viewport, node, custom region
  319. * @method getRegion
  320. * @param {Boolean} inc Include the node's height and width
  321. * @return {Object} The active region.
  322. */
  323. getRegion: function(inc) {
  324. var r = {}, oh = null, ow = null,
  325. host = this.get(HOST);
  326.  
  327. r = this._getConstraint();
  328.  
  329. if (inc) {
  330. oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT);
  331. ow = host.get(DRAG_NODE).get(OFFSET_WIDTH);
  332. r[RIGHT] = r[RIGHT] - ow;
  333. r[BOTTOM] = r[BOTTOM] - oh;
  334. }
  335. return r;
  336. },
  337. /**
  338. * Check if xy is inside a given region, if not change to it be inside.
  339. * @private
  340. * @method _checkRegion
  341. * @param {Array} _xy The XY to check if it's in the current region, if it isn't
  342. * inside the region, it will reset the xy array to be inside the region.
  343. * @return {Array} The new XY that is inside the region
  344. */
  345. _checkRegion: function(_xy) {
  346. var oxy = _xy,
  347. r = this.getRegion(),
  348. host = this.get(HOST),
  349. oh = host.get(DRAG_NODE).get(OFFSET_HEIGHT),
  350. ow = host.get(DRAG_NODE).get(OFFSET_WIDTH);
  351.  
  352. if (oxy[1] > (r[BOTTOM] - oh)) {
  353. _xy[1] = (r[BOTTOM] - oh);
  354. }
  355. if (r[TOP] > oxy[1]) {
  356. _xy[1] = r[TOP];
  357.  
  358. }
  359. if (oxy[0] > (r[RIGHT] - ow)) {
  360. _xy[0] = (r[RIGHT] - ow);
  361. }
  362. if (r[LEFT] > oxy[0]) {
  363. _xy[0] = r[LEFT];
  364. }
  365.  
  366. return _xy;
  367. },
  368. /**
  369. * Checks if the XY passed or the dragNode is inside the active region.
  370. * @method inRegion
  371. * @param {Array} xy Optional XY to check, if not supplied this.get('dragNode').getXY() is used.
  372. * @return {Boolean} True if the XY is inside the region, false otherwise.
  373. */
  374. inRegion: function(xy) {
  375. xy = xy || this.get(HOST).get(DRAG_NODE).getXY();
  376.  
  377. var _xy = this._checkRegion([xy[0], xy[1]]),
  378. inside = false;
  379. if ((xy[0] === _xy[0]) && (xy[1] === _xy[1])) {
  380. inside = true;
  381. }
  382. return inside;
  383. },
  384. /**
  385. * Modifies the Drag.actXY method from the after drag:align event. This is where the constraining happens.
  386. * @method align
  387. */
  388. align: function() {
  389. var host = this.get(HOST),
  390. _xy = [host.actXY[0], host.actXY[1]],
  391. r = this.getRegion(true);
  392.  
  393. if (this.get('stickX')) {
  394. _xy[1] = (host.startXY[1] - host.deltaXY[1]);
  395. }
  396. if (this.get('stickY')) {
  397. _xy[0] = (host.startXY[0] - host.deltaXY[0]);
  398. }
  399.  
  400. if (r) {
  401. _xy = this._checkRegion(_xy);
  402. }
  403.  
  404. _xy = this._checkTicks(_xy, r);
  405.  
  406. host.actXY = _xy;
  407. },
  408. /**
  409. * Fires after drag:drag. Handle the tickX and tickX align events.
  410. * @method drag
  411. */
  412. drag: function() {
  413. var host = this.get(HOST),
  414. xt = this.get('tickX'),
  415. yt = this.get('tickY'),
  416. _xy = [host.actXY[0], host.actXY[1]];
  417.  
  418. if ((Y.Lang.isNumber(xt) || this.get(TICK_X_ARRAY)) && (this._lastTickXFired !== _xy[0])) {
  419. this._tickAlignX();
  420. this._lastTickXFired = _xy[0];
  421. }
  422.  
  423. if ((Y.Lang.isNumber(yt) || this.get(TICK_Y_ARRAY)) && (this._lastTickYFired !== _xy[1])) {
  424. this._tickAlignY();
  425. this._lastTickYFired = _xy[1];
  426. }
  427. },
  428. /**
  429. * This method delegates the proper helper method for tick calculations
  430. * @private
  431. * @method _checkTicks
  432. * @param {Array} xy The XY coords for the Drag
  433. * @param {Object} r The optional region that we are bound to.
  434. * @return {Array} The calced XY coords
  435. */
  436. _checkTicks: function(xy, r) {
  437. var host = this.get(HOST),
  438. lx = (host.startXY[0] - host.deltaXY[0]),
  439. ly = (host.startXY[1] - host.deltaXY[1]),
  440. xt = this.get('tickX'),
  441. yt = this.get('tickY');
  442. if (xt && !this.get(TICK_X_ARRAY)) {
  443. xy[0] = DDM._calcTicks(xy[0], lx, xt, r[LEFT], r[RIGHT]);
  444. }
  445. if (yt && !this.get(TICK_Y_ARRAY)) {
  446. xy[1] = DDM._calcTicks(xy[1], ly, yt, r[TOP], r[BOTTOM]);
  447. }
  448. if (this.get(TICK_X_ARRAY)) {
  449. xy[0] = DDM._calcTickArray(xy[0], this.get(TICK_X_ARRAY), r[LEFT], r[RIGHT]);
  450. }
  451. if (this.get(TICK_Y_ARRAY)) {
  452. xy[1] = DDM._calcTickArray(xy[1], this.get(TICK_Y_ARRAY), r[TOP], r[BOTTOM]);
  453. }
  454.  
  455. return xy;
  456. },
  457. /**
  458. * Fires when the actXY[0] reach a new value respecting the tickX gap.
  459. * @private
  460. * @method _tickAlignX
  461. */
  462. _tickAlignX: function() {
  463. this.fire(EV_TICK_ALIGN_X);
  464. },
  465. /**
  466. * Fires when the actXY[1] reach a new value respecting the tickY gap.
  467. * @private
  468. * @method _tickAlignY
  469. */
  470. _tickAlignY: function() {
  471. this.fire(EV_TICK_ALIGN_Y);
  472. }
  473. };
  474.  
  475. Y.namespace('Plugin');
  476. Y.extend(C, Y.Base, proto);
  477. Y.Plugin.DDConstrained = C;
  478.  
  479. Y.mix(DDM, {
  480. /**
  481. * Helper method to calculate the tick offsets for a given position
  482. * @for DDM
  483. * @namespace DD
  484. * @private
  485. * @method _calcTicks
  486. * @param {Number} pos The current X or Y position
  487. * @param {Number} start The start X or Y position
  488. * @param {Number} tick The X or Y tick increment
  489. * @param {Number} off1 The min offset that we can't pass (region)
  490. * @param {Number} off2 The max offset that we can't pass (region)
  491. * @return {Number} The new position based on the tick calculation
  492. */
  493. _calcTicks: function(pos, start, tick, off1, off2) {
  494. var ix = ((pos - start) / tick),
  495. min = Math.floor(ix),
  496. max = Math.ceil(ix);
  497. if ((min !== 0) || (max !== 0)) {
  498. if ((ix >= min) && (ix <= max)) {
  499. pos = (start + (tick * min));
  500. if (off1 && off2) {
  501. if (pos < off1) {
  502. pos = (start + (tick * (min + 1)));
  503. }
  504. if (pos > off2) {
  505. pos = (start + (tick * (min - 1)));
  506. }
  507. }
  508. }
  509. }
  510. return pos;
  511. },
  512. /**
  513. * This method is used with the tickXArray and tickYArray config options
  514. * @for DDM
  515. * @namespace DD
  516. * @private
  517. * @method _calcTickArray
  518. * @param {Number} pos The current X or Y position
  519. * @param {Number} ticks The array containing our custom tick positions.
  520. * @param {Number} off1 The min offset that we can't pass (region)
  521. * @param {Number} off2 The max offset that we can't pass (region)
  522. * @return The tick position
  523. */
  524. _calcTickArray: function(pos, ticks, off1, off2) {
  525. var i = 0, len = ticks.length, next = 0,
  526. diff1, diff2, ret;
  527.  
  528. if (!ticks || (ticks.length === 0)) {
  529. return pos;
  530. }
  531. if (ticks[0] >= pos) {
  532. return ticks[0];
  533. }
  534.  
  535. for (i = 0; i < len; i++) {
  536. next = (i + 1);
  537. if (ticks[next] && ticks[next] >= pos) {
  538. diff1 = pos - ticks[i];
  539. diff2 = ticks[next] - pos;
  540. ret = (diff2 > diff1) ? ticks[i] : ticks[next];
  541. if (off1 && off2) {
  542. if (ret > off2) {
  543. if (ticks[i]) {
  544. ret = ticks[i];
  545. } else {
  546. ret = ticks[len - 1];
  547. }
  548. }
  549. }
  550. return ret;
  551. }
  552.  
  553. }
  554. return ticks[ticks.length - 1];
  555. }
  556. });
  557.  
  558.