hsvpalette.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview An HSV (hue/saturation/value) color palette/picker
  16. * implementation. Inspired by examples like
  17. * http://johndyer.name/lab/colorpicker/ and the author's initial work. This
  18. * control allows for more control in picking colors than a simple swatch-based
  19. * palette. Without the styles from the demo css file, only a hex color label
  20. * and input field show up.
  21. *
  22. * @author arv@google.com (Erik Arvidsson)
  23. * @see ../demos/hsvpalette.html
  24. */
  25. goog.provide('goog.ui.HsvPalette');
  26. goog.require('goog.color');
  27. goog.require('goog.dom.InputType');
  28. goog.require('goog.dom.TagName');
  29. goog.require('goog.events');
  30. goog.require('goog.events.EventType');
  31. goog.require('goog.events.InputHandler');
  32. goog.require('goog.style');
  33. goog.require('goog.style.bidi');
  34. goog.require('goog.ui.Component');
  35. goog.require('goog.userAgent');
  36. /**
  37. * Creates an HSV palette. Allows a user to select the hue, saturation and
  38. * value/brightness.
  39. * @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper.
  40. * @param {string=} opt_color Optional initial color (default is red).
  41. * @param {string=} opt_class Optional base for creating classnames (default is
  42. * goog.getCssName('goog-hsv-palette')).
  43. * @extends {goog.ui.Component}
  44. * @constructor
  45. */
  46. goog.ui.HsvPalette = function(opt_domHelper, opt_color, opt_class) {
  47. goog.ui.Component.call(this, opt_domHelper);
  48. this.setColorInternal(opt_color || '#f00');
  49. /**
  50. * The base class name for the component.
  51. * @type {string}
  52. * @protected
  53. */
  54. this.className = opt_class || goog.getCssName('goog-hsv-palette');
  55. /**
  56. * The document which is being listened to.
  57. * type {HTMLDocument}
  58. * @private
  59. */
  60. this.document_ = this.getDomHelper().getDocument();
  61. };
  62. goog.inherits(goog.ui.HsvPalette, goog.ui.Component);
  63. // TODO(user): Make this inherit from goog.ui.Control and split this into
  64. // a control and a renderer.
  65. goog.tagUnsealableClass(goog.ui.HsvPalette);
  66. /**
  67. * @desc Label for an input field where a user can enter a hexadecimal color
  68. * specification, such as #ff0000 for red.
  69. * @private
  70. */
  71. goog.ui.HsvPalette.MSG_HSV_PALETTE_HEX_COLOR_ = goog.getMsg('Hex color');
  72. /**
  73. * DOM element representing the hue/saturation background image.
  74. * @type {HTMLElement}
  75. * @private
  76. */
  77. goog.ui.HsvPalette.prototype.hsImageEl_;
  78. /**
  79. * DOM element representing the hue/saturation handle.
  80. * @type {HTMLElement}
  81. * @private
  82. */
  83. goog.ui.HsvPalette.prototype.hsHandleEl_;
  84. /**
  85. * DOM element representing the value background image.
  86. * @type {HTMLElement}
  87. * @protected
  88. */
  89. goog.ui.HsvPalette.prototype.valueBackgroundImageElement;
  90. /**
  91. * DOM element representing the value handle.
  92. * @type {HTMLElement}
  93. * @private
  94. */
  95. goog.ui.HsvPalette.prototype.vHandleEl_;
  96. /**
  97. * DOM element representing the current color swatch.
  98. * @type {Element}
  99. * @protected
  100. */
  101. goog.ui.HsvPalette.prototype.swatchElement;
  102. /**
  103. * DOM element representing the hex color input text field.
  104. * @type {Element}
  105. * @protected
  106. */
  107. goog.ui.HsvPalette.prototype.inputElement;
  108. /**
  109. * Input handler object for the hex value input field.
  110. * @type {goog.events.InputHandler}
  111. * @private
  112. */
  113. goog.ui.HsvPalette.prototype.inputHandler_;
  114. /**
  115. * Listener key for the mousemove event (during a drag operation).
  116. * @type {goog.events.Key}
  117. * @protected
  118. */
  119. goog.ui.HsvPalette.prototype.mouseMoveListener;
  120. /**
  121. * Listener key for the mouseup event (during a drag operation).
  122. * @type {goog.events.Key}
  123. * @protected
  124. */
  125. goog.ui.HsvPalette.prototype.mouseUpListener;
  126. /** @private {!goog.color.Hsv} */
  127. goog.ui.HsvPalette.prototype.hsv_;
  128. /**
  129. * Hex representation of the color.
  130. * @protected {string}
  131. */
  132. goog.ui.HsvPalette.prototype.color;
  133. /**
  134. * Gets the color that is currently selected in this color picker.
  135. * @return {string} The string of the selected color.
  136. */
  137. goog.ui.HsvPalette.prototype.getColor = function() {
  138. return this.color;
  139. };
  140. /**
  141. * Alpha transparency of the currently selected color, in [0, 1].
  142. * For the HSV palette this always returns 1. The HSVA palette overrides
  143. * this method.
  144. * @return {number} The current alpha value.
  145. */
  146. goog.ui.HsvPalette.prototype.getAlpha = function() {
  147. return 1;
  148. };
  149. /**
  150. * Updates the text entry field.
  151. * @protected
  152. */
  153. goog.ui.HsvPalette.prototype.updateInput = function() {
  154. var parsed;
  155. try {
  156. parsed = goog.color.parse(this.inputElement.value).hex;
  157. } catch (e) {
  158. // ignore
  159. }
  160. if (this.color != parsed) {
  161. this.inputElement.value = this.color;
  162. }
  163. };
  164. /**
  165. * Sets which color is selected and update the UI.
  166. * @param {string} color The selected color.
  167. */
  168. goog.ui.HsvPalette.prototype.setColor = function(color) {
  169. if (color != this.color) {
  170. this.setColorInternal(color);
  171. this.updateUi();
  172. this.dispatchEvent(goog.ui.Component.EventType.ACTION);
  173. }
  174. };
  175. /**
  176. * Sets which color is selected.
  177. * @param {string} color The selected color.
  178. * @protected
  179. */
  180. goog.ui.HsvPalette.prototype.setColorInternal = function(color) {
  181. var rgbHex = goog.color.parse(color).hex;
  182. var rgbArray = goog.color.hexToRgb(rgbHex);
  183. this.hsv_ = goog.color.rgbArrayToHsv(rgbArray);
  184. // Hue is divided by 360 because the documentation for goog.color is currently
  185. // incorrect.
  186. // TODO(user): Fix this, see http://1324469 .
  187. this.hsv_[0] = this.hsv_[0] / 360;
  188. this.color = rgbHex;
  189. };
  190. /**
  191. * Alters the hue, saturation, and/or value of the currently selected color and
  192. * updates the UI.
  193. * @param {?number=} opt_hue (optional) hue in [0, 1].
  194. * @param {?number=} opt_saturation (optional) saturation in [0, 1].
  195. * @param {?number=} opt_value (optional) value in [0, 255].
  196. */
  197. goog.ui.HsvPalette.prototype.setHsv = function(
  198. opt_hue, opt_saturation, opt_value) {
  199. if (opt_hue != null || opt_saturation != null || opt_value != null) {
  200. this.setHsv_(opt_hue, opt_saturation, opt_value);
  201. this.updateUi();
  202. this.dispatchEvent(goog.ui.Component.EventType.ACTION);
  203. }
  204. };
  205. /**
  206. * Alters the hue, saturation, and/or value of the currently selected color.
  207. * @param {?number=} opt_hue (optional) hue in [0, 1].
  208. * @param {?number=} opt_saturation (optional) saturation in [0, 1].
  209. * @param {?number=} opt_value (optional) value in [0, 255].
  210. * @private
  211. */
  212. goog.ui.HsvPalette.prototype.setHsv_ = function(
  213. opt_hue, opt_saturation, opt_value) {
  214. this.hsv_[0] = (opt_hue != null) ? opt_hue : this.hsv_[0];
  215. this.hsv_[1] = (opt_saturation != null) ? opt_saturation : this.hsv_[1];
  216. this.hsv_[2] = (opt_value != null) ? opt_value : this.hsv_[2];
  217. // Hue is multiplied by 360 because the documentation for goog.color is
  218. // currently incorrect.
  219. // TODO(user): Fix this, see http://1324469 .
  220. this.color = goog.color.hsvArrayToHex(
  221. [this.hsv_[0] * 360, this.hsv_[1], this.hsv_[2]]);
  222. };
  223. /**
  224. * HsvPalettes cannot be used to decorate pre-existing html, since the
  225. * structure they build is fairly complicated.
  226. * @param {Element} element Element to decorate.
  227. * @return {boolean} Returns always false.
  228. * @override
  229. */
  230. goog.ui.HsvPalette.prototype.canDecorate = function(element) {
  231. return false;
  232. };
  233. /** @override */
  234. goog.ui.HsvPalette.prototype.createDom = function() {
  235. var dom = this.getDomHelper();
  236. var noalpha = (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7')) ?
  237. ' ' + goog.getCssName(this.className, 'noalpha') :
  238. '';
  239. var backdrop = dom.createDom(
  240. goog.dom.TagName.DIV, goog.getCssName(this.className, 'hs-backdrop'));
  241. this.hsHandleEl_ = /** @type {!HTMLElement} */ (
  242. dom.createDom(
  243. goog.dom.TagName.DIV, goog.getCssName(this.className, 'hs-handle')));
  244. this.hsImageEl_ = /** @type {!HTMLElement} */ (
  245. dom.createDom(
  246. goog.dom.TagName.DIV, goog.getCssName(this.className, 'hs-image'),
  247. this.hsHandleEl_));
  248. this.valueBackgroundImageElement = /** @type {!HTMLElement} */ (
  249. dom.createDom(
  250. goog.dom.TagName.DIV, goog.getCssName(this.className, 'v-image')));
  251. this.vHandleEl_ = /** @type {!HTMLElement} */ (
  252. dom.createDom(
  253. goog.dom.TagName.DIV, goog.getCssName(this.className, 'v-handle')));
  254. this.swatchElement = dom.createDom(
  255. goog.dom.TagName.DIV, goog.getCssName(this.className, 'swatch'));
  256. this.inputElement = dom.createDom(goog.dom.TagName.INPUT, {
  257. 'class': goog.getCssName(this.className, 'input'),
  258. 'aria-label': goog.ui.HsvPalette.MSG_HSV_PALETTE_HEX_COLOR_,
  259. 'type': goog.dom.InputType.TEXT,
  260. 'dir': 'ltr'
  261. });
  262. var labelElement =
  263. dom.createDom(goog.dom.TagName.LABEL, null, this.inputElement);
  264. var element = dom.createDom(
  265. goog.dom.TagName.DIV, this.className + noalpha, backdrop, this.hsImageEl_,
  266. this.valueBackgroundImageElement, this.vHandleEl_, this.swatchElement,
  267. labelElement);
  268. this.setElementInternal(element);
  269. // TODO(arv): Set tabIndex
  270. };
  271. /**
  272. * Renders the color picker inside the provided element. This will override the
  273. * current content of the element.
  274. * @override
  275. */
  276. goog.ui.HsvPalette.prototype.enterDocument = function() {
  277. goog.ui.HsvPalette.superClass_.enterDocument.call(this);
  278. // TODO(user): Accessibility.
  279. this.updateUi();
  280. var handler = this.getHandler();
  281. handler.listen(
  282. this.getElement(), goog.events.EventType.MOUSEDOWN, this.handleMouseDown);
  283. // Cannot create InputHandler in createDom because IE throws an exception
  284. // on document.activeElement
  285. if (!this.inputHandler_) {
  286. this.inputHandler_ = new goog.events.InputHandler(this.inputElement);
  287. }
  288. handler.listen(
  289. this.inputHandler_, goog.events.InputHandler.EventType.INPUT,
  290. this.handleInput);
  291. };
  292. /** @override */
  293. goog.ui.HsvPalette.prototype.disposeInternal = function() {
  294. goog.ui.HsvPalette.superClass_.disposeInternal.call(this);
  295. delete this.hsImageEl_;
  296. delete this.hsHandleEl_;
  297. delete this.valueBackgroundImageElement;
  298. delete this.vHandleEl_;
  299. delete this.swatchElement;
  300. delete this.inputElement;
  301. if (this.inputHandler_) {
  302. this.inputHandler_.dispose();
  303. delete this.inputHandler_;
  304. }
  305. goog.events.unlistenByKey(this.mouseMoveListener);
  306. goog.events.unlistenByKey(this.mouseUpListener);
  307. };
  308. /**
  309. * Updates the position, opacity, and styles for the UI representation of the
  310. * palette.
  311. * @protected
  312. */
  313. goog.ui.HsvPalette.prototype.updateUi = function() {
  314. if (this.isInDocument()) {
  315. var h = this.hsv_[0];
  316. var s = this.hsv_[1];
  317. var v = this.hsv_[2];
  318. var left = this.hsImageEl_.offsetWidth * h;
  319. // We don't use a flipped gradient image in RTL, so we need to flip the
  320. // offset in RTL so that it still hovers over the correct color on the
  321. // gradiant.
  322. if (this.isRightToLeft()) {
  323. left = this.hsImageEl_.offsetWidth - left;
  324. }
  325. // We also need to account for the handle size.
  326. var handleOffset = Math.ceil(this.hsHandleEl_.offsetWidth / 2);
  327. left -= handleOffset;
  328. var top = this.hsImageEl_.offsetHeight * (1 - s);
  329. // Account for the handle size.
  330. top -= Math.ceil(this.hsHandleEl_.offsetHeight / 2);
  331. goog.style.bidi.setPosition(
  332. this.hsHandleEl_, left, top, this.isRightToLeft());
  333. top = this.valueBackgroundImageElement.offsetTop -
  334. Math.floor(this.vHandleEl_.offsetHeight / 2) +
  335. this.valueBackgroundImageElement.offsetHeight * ((255 - v) / 255);
  336. this.vHandleEl_.style.top = top + 'px';
  337. goog.style.setOpacity(this.hsImageEl_, (v / 255));
  338. goog.style.setStyle(
  339. this.valueBackgroundImageElement, 'background-color',
  340. goog.color.hsvToHex(this.hsv_[0] * 360, this.hsv_[1], 255));
  341. goog.style.setStyle(this.swatchElement, 'background-color', this.color);
  342. goog.style.setStyle(
  343. this.swatchElement, 'color',
  344. (this.hsv_[2] > 255 / 2) ? '#000' : '#fff');
  345. this.updateInput();
  346. }
  347. };
  348. /**
  349. * Handles mousedown events on palette UI elements.
  350. * @param {goog.events.BrowserEvent} e Event object.
  351. * @protected
  352. */
  353. goog.ui.HsvPalette.prototype.handleMouseDown = function(e) {
  354. if (e.target == this.valueBackgroundImageElement ||
  355. e.target == this.vHandleEl_) {
  356. // Setup value change listeners
  357. var b = goog.style.getBounds(this.valueBackgroundImageElement);
  358. this.handleMouseMoveV_(b, e);
  359. this.mouseMoveListener = goog.events.listen(
  360. this.document_, goog.events.EventType.MOUSEMOVE,
  361. goog.bind(this.handleMouseMoveV_, this, b));
  362. this.mouseUpListener = goog.events.listen(
  363. this.document_, goog.events.EventType.MOUSEUP, this.handleMouseUp,
  364. false, this);
  365. } else if (e.target == this.hsImageEl_ || e.target == this.hsHandleEl_) {
  366. // Setup hue/saturation change listeners
  367. var b = goog.style.getBounds(this.hsImageEl_);
  368. this.handleMouseMoveHs_(b, e);
  369. this.mouseMoveListener = goog.events.listen(
  370. this.document_, goog.events.EventType.MOUSEMOVE,
  371. goog.bind(this.handleMouseMoveHs_, this, b));
  372. this.mouseUpListener = goog.events.listen(
  373. this.document_, goog.events.EventType.MOUSEUP, this.handleMouseUp,
  374. false, this);
  375. }
  376. };
  377. /**
  378. * Handles mousemove events on the document once a drag operation on the value
  379. * slider has started.
  380. * @param {goog.math.Rect} b Boundaries of the value slider object at the start
  381. * of the drag operation.
  382. * @param {goog.events.BrowserEvent} e Event object.
  383. * @private
  384. */
  385. goog.ui.HsvPalette.prototype.handleMouseMoveV_ = function(b, e) {
  386. e.preventDefault();
  387. var vportPos = this.getDomHelper().getDocumentScroll();
  388. var height =
  389. Math.min(Math.max(vportPos.y + e.clientY, b.top), b.top + b.height);
  390. var newV = Math.round(255 * (b.top + b.height - height) / b.height);
  391. this.setHsv(null, null, newV);
  392. };
  393. /**
  394. * Handles mousemove events on the document once a drag operation on the
  395. * hue/saturation slider has started.
  396. * @param {goog.math.Rect} b Boundaries of the value slider object at the start
  397. * of the drag operation.
  398. * @param {goog.events.BrowserEvent} e Event object.
  399. * @private
  400. */
  401. goog.ui.HsvPalette.prototype.handleMouseMoveHs_ = function(b, e) {
  402. e.preventDefault();
  403. var vportPos = this.getDomHelper().getDocumentScroll();
  404. var newH =
  405. (Math.min(Math.max(vportPos.x + e.clientX, b.left), b.left + b.width) -
  406. b.left) /
  407. b.width;
  408. var newS =
  409. (-Math.min(Math.max(vportPos.y + e.clientY, b.top), b.top + b.height) +
  410. b.top + b.height) /
  411. b.height;
  412. this.setHsv(newH, newS, null);
  413. };
  414. /**
  415. * Handles mouseup events on the document, which ends a drag operation.
  416. * @param {goog.events.Event} e Event object.
  417. * @protected
  418. */
  419. goog.ui.HsvPalette.prototype.handleMouseUp = function(e) {
  420. goog.events.unlistenByKey(this.mouseMoveListener);
  421. goog.events.unlistenByKey(this.mouseUpListener);
  422. };
  423. /**
  424. * Handles input events on the hex value input field.
  425. * @param {goog.events.Event} e Event object.
  426. * @protected
  427. */
  428. goog.ui.HsvPalette.prototype.handleInput = function(e) {
  429. if (/^#?[0-9a-f]{6}$/i.test(this.inputElement.value)) {
  430. this.setColor(this.inputElement.value);
  431. }
  432. };