Picker.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. /**
  2. * A ratings picker based on `Ext.Gadget`.
  3. *
  4. * @example
  5. * Ext.create({
  6. * xtype: 'rating',
  7. * renderTo: Ext.getBody(),
  8. * listeners: {
  9. * change: function (picker, value) {
  10. * console.log('Rating ' + value);
  11. * }
  12. * }
  13. * });
  14. */
  15. Ext.define('Ext.ux.rating.Picker', {
  16. extend: 'Ext.Gadget',
  17. xtype: 'rating',
  18. focusable: true,
  19. /*
  20. * The "cachedConfig" block is basically the same as "config" except that these
  21. * values are applied specially to the first instance of the class. After processing
  22. * these configs, the resulting values are stored on the class `prototype` and the
  23. * template DOM element also reflects these default values.
  24. */
  25. cachedConfig: {
  26. /**
  27. * @cfg {String} [family]
  28. * The CSS `font-family` to use for displaying the `{@link #glyphs}`.
  29. */
  30. family: 'monospace',
  31. /**
  32. * @cfg {String/String[]/Number[]} [glyphs]
  33. * Either a string containing the two glyph characters, or an array of two strings
  34. * containing the individual glyph characters or an array of two numbers with the
  35. * character codes for the individual glyphs.
  36. *
  37. * For example:
  38. *
  39. * @example
  40. * Ext.create({
  41. * xtype: 'rating',
  42. * renderTo: Ext.getBody(),
  43. * glyphs: [ 9671, 9670 ], // '◇◆',
  44. * listeners: {
  45. * change: function (picker, value) {
  46. * console.log('Rating ' + value);
  47. * }
  48. * }
  49. * });
  50. */
  51. glyphs: '☆★',
  52. /**
  53. * @cfg {Number} [minimum=1]
  54. * The minimum allowed `{@link #value}` (rating).
  55. */
  56. minimum: 1,
  57. /**
  58. * @cfg {Number} [limit]
  59. * The maximum allowed `{@link #value}` (rating).
  60. */
  61. limit: 5,
  62. /**
  63. * @cfg {String/Object} [overStyle]
  64. * Optional styles to apply to the rating glyphs when `{@link #trackOver}` is
  65. * enabled.
  66. */
  67. overStyle: null,
  68. /**
  69. * @cfg {Number} [rounding=1]
  70. * The rounding to apply to values. Common choices are 0.5 (for half-steps) or
  71. * 0.25 (for quarter steps).
  72. */
  73. rounding: 1,
  74. /**
  75. * @cfg {String} [scale="125%"]
  76. * The CSS `font-size` to apply to the glyphs. This value defaults to 125% because
  77. * glyphs in the stock font tend to be too small. When using specially designed
  78. * "icon fonts" you may want to set this to 100%.
  79. */
  80. scale: '125%',
  81. /**
  82. * @cfg {String/Object} [selectedStyle]
  83. * Optional styles to apply to the rating value glyphs.
  84. */
  85. selectedStyle: null,
  86. /**
  87. * @cfg {Object/String/String[]/Ext.XTemplate/Function} tip
  88. * A template or a function that produces the tooltip text. The `Object`, `String`
  89. * and `String[]` forms are converted to an `Ext.XTemplate`. If a function is given,
  90. * it will be called with an object parameter and should return the tooltip text.
  91. * The object contains these properties:
  92. *
  93. * - component: The rating component requesting the tooltip.
  94. * - tracking: The current value under the mouse cursor.
  95. * - trackOver: The value of the `{@link #trackOver}` config.
  96. * - value: The current value.
  97. *
  98. * Templates can use these properties to generate the proper text.
  99. */
  100. tip: null,
  101. /**
  102. * @cfg {Boolean} [trackOver=true]
  103. * Determines if mouse movements should temporarily update the displayed value.
  104. * The actual `value` is only updated on `click` but this rather acts as the
  105. * "preview" of the value prior to click.
  106. */
  107. trackOver: true,
  108. /**
  109. * @cfg {Number} value
  110. * The rating value. This value is bounded by `minimum` and `limit` and is also
  111. * adjusted by the `rounding`.
  112. */
  113. value: null,
  114. //---------------------------------------------------------------------
  115. // Private configs
  116. /**
  117. * @cfg {String} tooltipText
  118. * The current tooltip text. This value is set into the DOM by the updater (hence
  119. * only when it changes). This is intended for use by the tip manager
  120. * (`{@link Ext.tip.QuickTipManager}`). Developers should never need to set this
  121. * config since it is handled by virtue of setting other configs (such as the
  122. * {@link #tooltip} or the {@link #value}.).
  123. * @private
  124. */
  125. tooltipText: null,
  126. /**
  127. * @cfg {Number} trackingValue
  128. * This config is used to when `trackOver` is `true` and represents the tracked
  129. * value. This config is maintained by our `mousemove` handler. This should not
  130. * need to be set directly by user code.
  131. * @private
  132. */
  133. trackingValue: null
  134. },
  135. config: {
  136. /**
  137. * @cfg {Boolean/Object} [animate=false]
  138. * Specifies an animation to use when changing the `{@link #value}`. When setting
  139. * this config, it is probably best to set `{@link #trackOver}` to `false`.
  140. */
  141. animate: null
  142. },
  143. // This object describes our element tree from the root.
  144. element: {
  145. cls: 'u' + Ext.baseCSSPrefix + 'rating-picker',
  146. // Since we are replacing the entire "element" tree, we have to assign this
  147. // "reference" as would our base class.
  148. reference: 'element',
  149. children: [{
  150. reference: 'innerEl',
  151. cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-inner',
  152. listeners: {
  153. click: 'onClick',
  154. mousemove: 'onMouseMove',
  155. mouseenter: 'onMouseEnter',
  156. mouseleave: 'onMouseLeave'
  157. },
  158. children: [{
  159. reference: 'valueEl',
  160. cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-value'
  161. },{
  162. reference: 'trackerEl',
  163. cls: 'u' + Ext.baseCSSPrefix + 'rating-picker-tracker'
  164. }]
  165. }]
  166. },
  167. // Tell the Binding system to default to our "value" config.
  168. defaultBindProperty: 'value',
  169. // Enable two-way data binding for the "value" config.
  170. twoWayBindable: 'value',
  171. overCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-over',
  172. trackOverCls: 'u' + Ext.baseCSSPrefix + 'rating-picker-track-over',
  173. //-------------------------------------------------------------------------
  174. // Config Appliers
  175. applyGlyphs: function (value) {
  176. if (typeof value === 'string') {
  177. //<debug>
  178. if (value.length !== 2) {
  179. Ext.raise('Expected 2 characters for "glyphs" not "' + value +'".');
  180. }
  181. //</debug>
  182. value = [ value.charAt(0), value.charAt(1) ];
  183. }
  184. else if (typeof value[0] === 'number') {
  185. value = [
  186. String.fromCharCode(value[0]),
  187. String.fromCharCode(value[1])
  188. ];
  189. }
  190. return value;
  191. },
  192. applyOverStyle: function(style) {
  193. this.trackerEl.applyStyles(style);
  194. },
  195. applySelectedStyle: function(style) {
  196. this.valueEl.applyStyles(style);
  197. },
  198. applyTip: function (tip) {
  199. if (tip && typeof tip !== 'function') {
  200. if (!tip.isTemplate) {
  201. tip = new Ext.XTemplate(tip);
  202. }
  203. tip = tip.apply.bind(tip);
  204. }
  205. return tip;
  206. },
  207. applyTrackingValue: function (value) {
  208. return this.applyValue(value); // same rounding as normal value
  209. },
  210. applyValue: function (v) {
  211. if (v !== null) {
  212. var rounding = this.getRounding(),
  213. limit = this.getLimit(),
  214. min = this.getMinimum();
  215. v = Math.round(Math.round(v / rounding) * rounding * 1000) / 1000;
  216. v = (v < min) ? min : (v > limit ? limit : v);
  217. }
  218. return v;
  219. },
  220. //-------------------------------------------------------------------------
  221. // Event Handlers
  222. onClick: function (event) {
  223. var value = this.valueFromEvent(event);
  224. this.setValue(value);
  225. },
  226. onMouseEnter: function () {
  227. this.element.addCls(this.overCls);
  228. },
  229. onMouseLeave: function () {
  230. this.element.removeCls(this.overCls);
  231. },
  232. onMouseMove: function (event) {
  233. var value = this.valueFromEvent(event);
  234. this.setTrackingValue(value);
  235. },
  236. //-------------------------------------------------------------------------
  237. // Config Updaters
  238. updateFamily: function (family) {
  239. this.element.setStyle('fontFamily', "'" + family + "'");
  240. },
  241. updateGlyphs: function () {
  242. this.refreshGlyphs();
  243. },
  244. updateLimit: function () {
  245. this.refreshGlyphs();
  246. },
  247. updateScale: function (size) {
  248. this.element.setStyle('fontSize', size);
  249. },
  250. updateTip: function () {
  251. this.refreshTip();
  252. },
  253. updateTooltipText: function (text) {
  254. this.setTooltip(text); // modern only (replaced by classic override)
  255. },
  256. updateTrackingValue: function (value) {
  257. var me = this,
  258. trackerEl = me.trackerEl,
  259. newWidth = me.valueToPercent(value);
  260. trackerEl.setStyle('width', newWidth);
  261. me.refreshTip();
  262. },
  263. updateTrackOver: function (trackOver) {
  264. this.element.toggleCls(this.trackOverCls, trackOver);
  265. },
  266. updateValue: function (value, oldValue) {
  267. var me = this,
  268. animate = me.getAnimate(),
  269. valueEl = me.valueEl,
  270. newWidth = me.valueToPercent(value),
  271. column, record;
  272. if (me.isConfiguring || !animate) {
  273. valueEl.setStyle('width', newWidth);
  274. } else {
  275. valueEl.stopAnimation();
  276. valueEl.animate(Ext.merge({
  277. from: { width: me.valueToPercent(oldValue) },
  278. to: { width: newWidth }
  279. }, animate));
  280. }
  281. me.refreshTip();
  282. if (!me.isConfiguring) {
  283. // Since we are (re)configured many times as we are used in a grid cell, we
  284. // avoid firing the change event unless there are listeners.
  285. if (me.hasListeners.change) {
  286. me.fireEvent('change', me, value, oldValue);
  287. }
  288. column = me.getWidgetColumn && me.getWidgetColumn();
  289. record = column && me.getWidgetRecord && me.getWidgetRecord();
  290. if (record && column.dataIndex) {
  291. // When used in a widgetcolumn, we should update the backing field. The
  292. // linkages will be cleared as we are being recycled, so this will only
  293. // reach this line when we are properly attached to a record and the
  294. // change is coming from the user (or a call to setValue).
  295. record.set(column.dataIndex, value);
  296. }
  297. }
  298. },
  299. //-------------------------------------------------------------------------
  300. // Config System Optimizations
  301. //
  302. // These are to deal with configs that combine to determine what should be
  303. // rendered in the DOM. For example, "glyphs" and "limit" must both be known
  304. // to render the proper text nodes. The "tip" and "value" likewise are
  305. // used to update the tooltipText.
  306. //
  307. // To avoid multiple updates to the DOM (one for each config), we simply mark
  308. // the rendering as invalid and post-process these flags on the tail of any
  309. // bulk updates.
  310. afterCachedConfig: function () {
  311. // Now that we are done setting up the initial values we need to refresh the
  312. // DOM before we allow Ext.Widget's implementation to cloneNode on it.
  313. this.refresh();
  314. return this.callParent(arguments);
  315. },
  316. initConfig: function (instanceConfig) {
  317. this.isConfiguring = true;
  318. this.callParent([ instanceConfig ]);
  319. // The firstInstance will already have refreshed the DOM (in afterCacheConfig)
  320. // but all instances beyond the first need to refresh if they have custom values
  321. // for one or more configs that affect the DOM (such as "glyphs" and "limit").
  322. this.refresh();
  323. },
  324. setConfig: function () {
  325. var me = this;
  326. // Since we could be updating multiple configs, save any updates that need
  327. // multiple values for afterwards.
  328. me.isReconfiguring = true;
  329. me.callParent(arguments);
  330. me.isReconfiguring = false;
  331. // Now that all new values are set, we can refresh the DOM.
  332. me.refresh();
  333. return me;
  334. },
  335. //-------------------------------------------------------------------------
  336. privates: {
  337. /**
  338. * This method returns the DOM text node into which glyphs are placed.
  339. * @param {HTMLElement} dom The DOM node parent of the text node.
  340. * @return {HTMLElement} The text node.
  341. * @private
  342. */
  343. getGlyphTextNode: function (dom) {
  344. var node = dom.lastChild;
  345. // We want all our text nodes to be at the end of the child list, most
  346. // especially the text node on the innerEl. That text node affects the
  347. // default left/right position of our absolutely positioned child divs
  348. // (trackerEl and valueEl).
  349. if (!node || node.nodeType !== 3) {
  350. node = dom.ownerDocument.createTextNode('');
  351. dom.appendChild(node);
  352. }
  353. return node;
  354. },
  355. getTooltipData: function () {
  356. var me = this;
  357. return {
  358. component: me,
  359. tracking: me.getTrackingValue(),
  360. trackOver: me.getTrackOver(),
  361. value: me.getValue()
  362. };
  363. },
  364. /**
  365. * Forcibly refreshes both glyph and tooltip rendering.
  366. * @private
  367. */
  368. refresh: function () {
  369. var me = this;
  370. if (me.invalidGlyphs) {
  371. me.refreshGlyphs(true);
  372. }
  373. if (me.invalidTip) {
  374. me.refreshTip(true);
  375. }
  376. },
  377. /**
  378. * Refreshes the glyph text rendering unless we are currently performing a
  379. * bulk config change (initConfig or setConfig).
  380. * @param {Boolean} now Pass `true` to force the refresh to happen now.
  381. * @private
  382. */
  383. refreshGlyphs: function (now) {
  384. var me = this,
  385. later = !now && (me.isConfiguring || me.isReconfiguring),
  386. el, glyphs, limit, on, off, trackerEl, valueEl;
  387. if (!later) {
  388. el = me.getGlyphTextNode(me.innerEl.dom);
  389. valueEl = me.getGlyphTextNode(me.valueEl.dom);
  390. trackerEl = me.getGlyphTextNode(me.trackerEl.dom);
  391. glyphs = me.getGlyphs();
  392. limit = me.getLimit();
  393. for (on = off = ''; limit--; ) {
  394. off += glyphs[0];
  395. on += glyphs[1];
  396. }
  397. el.nodeValue = off;
  398. valueEl.nodeValue = on;
  399. trackerEl.nodeValue = on;
  400. }
  401. me.invalidGlyphs = later;
  402. },
  403. /**
  404. * Refreshes the tooltip text rendering unless we are currently performing a
  405. * bulk config change (initConfig or setConfig).
  406. * @param {Boolean} now Pass `true` to force the refresh to happen now.
  407. * @private
  408. */
  409. refreshTip: function (now) {
  410. var me = this,
  411. later = !now && (me.isConfiguring || me.isReconfiguring),
  412. data, text, tooltip;
  413. if (!later) {
  414. tooltip = me.getTip();
  415. if (tooltip) {
  416. data = me.getTooltipData();
  417. text = tooltip(data);
  418. me.setTooltipText(text);
  419. }
  420. }
  421. me.invalidTip = later;
  422. },
  423. /**
  424. * Convert the coordinates of the given `Event` into a rating value.
  425. * @param {Ext.event.Event} event The event.
  426. * @return {Number} The rating based on the given event coordinates.
  427. * @private
  428. */
  429. valueFromEvent: function (event) {
  430. var me = this,
  431. el = me.innerEl,
  432. ex = event.getX(),
  433. rounding = me.getRounding(),
  434. cx = el.getX(),
  435. x = ex - cx,
  436. w = el.getWidth(),
  437. limit = me.getLimit(),
  438. v;
  439. if (me.getInherited().rtl) {
  440. x = w - x;
  441. }
  442. v = x / w * limit;
  443. // We have to round up here so that the area we are over is considered
  444. // the value.
  445. v = Math.ceil(v / rounding) * rounding;
  446. return v;
  447. },
  448. /**
  449. * Convert the given rating into a width percentage.
  450. * @param {Number} value The rating value to convert.
  451. * @return {String} The width percentage to represent the given value.
  452. * @private
  453. */
  454. valueToPercent: function (value) {
  455. value = (value / this.getLimit()) * 100;
  456. return value + '%';
  457. }
  458. }
  459. });