navigationBindings.js 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. /**
  2. * (c) 2009-2017 Highsoft, Black Label
  3. *
  4. * License: www.highcharts.com/license
  5. */
  6. 'use strict';
  7. import H from '../parts/Globals.js';
  8. import chartNavigationMixin from '../mixins/navigation.js';
  9. var doc = H.doc,
  10. addEvent = H.addEvent,
  11. pick = H.pick,
  12. merge = H.merge,
  13. extend = H.extend,
  14. isNumber = H.isNumber,
  15. fireEvent = H.fireEvent,
  16. isArray = H.isArray,
  17. isObject = H.isObject,
  18. objectEach = H.objectEach,
  19. PREFIX = 'highcharts-';
  20. /**
  21. * @private
  22. * @interface bindingsUtils
  23. */
  24. var bindingsUtils = {
  25. /**
  26. * Update size of background (rect) in some annotations: Measure, Simple
  27. * Rect.
  28. *
  29. * @private
  30. * @function bindingsUtils.updateRectSize
  31. *
  32. * @param {global.Event} event
  33. * Normalized browser event
  34. *
  35. * @param {Highcharts.Annotation} annotation
  36. * Annotation to be updated
  37. */
  38. updateRectSize: function (event, annotation) {
  39. var options = annotation.options.typeOptions,
  40. x = this.chart.xAxis[0].toValue(event.chartX),
  41. y = this.chart.yAxis[0].toValue(event.chartY),
  42. width = x - options.point.x,
  43. height = options.point.y - y;
  44. annotation.update({
  45. typeOptions: {
  46. background: {
  47. width: width,
  48. height: height
  49. }
  50. }
  51. });
  52. },
  53. /**
  54. * Get field type according to value
  55. *
  56. * @private
  57. * @function bindingsUtils.getFieldType
  58. *
  59. * @param {*} value
  60. * Atomic type (one of: string, number, boolean)
  61. *
  62. * @return {string}
  63. * Field type (one of: text, number, checkbox)
  64. */
  65. getFieldType: function (value) {
  66. return {
  67. 'string': 'text',
  68. 'number': 'number',
  69. 'boolean': 'checkbox'
  70. }[typeof value];
  71. }
  72. };
  73. H.NavigationBindings = function (chart, options) {
  74. this.chart = chart;
  75. this.options = options;
  76. this.eventsToUnbind = [];
  77. this.container = doc.getElementsByClassName(
  78. this.options.bindingsClassName
  79. );
  80. };
  81. // Define which options from annotations should show up in edit box:
  82. H.NavigationBindings.annotationsEditable = {
  83. // `typeOptions` are always available
  84. // Nested and shared options:
  85. nestedOptions: {
  86. labelOptions: ['style', 'format', 'backgroundColor'],
  87. labels: ['style'],
  88. label: ['style'],
  89. style: ['fontSize', 'color'],
  90. background: ['fill', 'strokeWidth', 'stroke'],
  91. innerBackground: ['fill', 'strokeWidth', 'stroke'],
  92. outerBackground: ['fill', 'strokeWidth', 'stroke'],
  93. shapeOptions: ['fill', 'strokeWidth', 'stroke'],
  94. shapes: ['fill', 'strokeWidth', 'stroke'],
  95. line: ['strokeWidth', 'stroke'],
  96. backgroundColors: [true],
  97. connector: ['fill', 'strokeWidth', 'stroke'],
  98. crosshairX: ['strokeWidth', 'stroke'],
  99. crosshairY: ['strokeWidth', 'stroke']
  100. },
  101. // Simple shapes:
  102. circle: ['shapes'],
  103. verticalLine: [],
  104. label: ['labelOptions'],
  105. // Measure
  106. measure: ['background', 'crosshairY', 'crosshairX'],
  107. // Others:
  108. fibonacci: [],
  109. tunnel: ['background', 'line', 'height'],
  110. pitchfork: ['innerBackground', 'outerBackground'],
  111. rect: ['shapes'],
  112. // Crooked lines, elliots, arrows etc:
  113. crookedLine: []
  114. };
  115. // Define non editable fields per annotation, for example Rectangle inherits
  116. // options from Measure, but crosshairs are not available
  117. H.NavigationBindings.annotationsNonEditable = {
  118. rectangle: ['crosshairX', 'crosshairY', 'label']
  119. };
  120. extend(H.NavigationBindings.prototype, {
  121. // Private properties added by bindings:
  122. // Active (selected) annotation that is editted through popup/forms
  123. // activeAnnotation: Annotation
  124. // Holder for current step, used on mouse move to update bound object
  125. // mouseMoveEvent: function () {}
  126. // Next event in `step` array to be called on chart's click
  127. // nextEvent: function () {}
  128. // Index in the `step` array of the current event
  129. // stepIndex: 0
  130. // Flag to determine if current binding has steps
  131. // steps: true|false
  132. // Bindings holder for all events
  133. // selectedButton: {}
  134. // Holder for user options, returned from `start` event, and passed on to
  135. // `step`'s' and `end`.
  136. // currentUserDetails: {}
  137. /**
  138. * Initi all events conencted to NavigationBindings.
  139. *
  140. * @private
  141. * @function Highcharts.NavigationBindings#initEvents
  142. */
  143. initEvents: function () {
  144. var navigation = this,
  145. chart = navigation.chart,
  146. bindingsContainer = navigation.container,
  147. options = navigation.options;
  148. // Shorthand object for getting events for buttons:
  149. navigation.boundClassNames = {};
  150. objectEach(options.bindings, function (value) {
  151. navigation.boundClassNames[value.className] = value;
  152. });
  153. // Handle multiple containers with the same class names:
  154. [].forEach.call(bindingsContainer, function (subContainer) {
  155. navigation.eventsToUnbind.push(
  156. addEvent(
  157. subContainer,
  158. 'click',
  159. function (event) {
  160. var bindings = navigation.getButtonEvents(
  161. bindingsContainer,
  162. event
  163. );
  164. if (bindings) {
  165. navigation.bindingsButtonClick(
  166. bindings.button,
  167. bindings.events,
  168. event
  169. );
  170. }
  171. }
  172. )
  173. );
  174. });
  175. objectEach(options.events || {}, function (callback, eventName) {
  176. navigation.eventsToUnbind.push(
  177. addEvent(
  178. navigation,
  179. eventName,
  180. callback
  181. )
  182. );
  183. });
  184. navigation.eventsToUnbind.push(
  185. addEvent(chart.container, 'click', function (e) {
  186. if (
  187. !chart.cancelClick &&
  188. chart.isInsidePlot(
  189. e.chartX - chart.plotLeft,
  190. e.chartY - chart.plotTop
  191. )
  192. ) {
  193. navigation.bindingsChartClick(this, e);
  194. }
  195. })
  196. );
  197. navigation.eventsToUnbind.push(
  198. addEvent(chart.container, 'mousemove', function (e) {
  199. navigation.bindingsContainerMouseMove(this, e);
  200. })
  201. );
  202. },
  203. /**
  204. * Common chart.update() delegation, shared between bindings and exporting.
  205. *
  206. * @private
  207. * @function Highcharts.NavigationBindings#initUpdate
  208. */
  209. initUpdate: function () {
  210. var navigation = this;
  211. chartNavigationMixin.addUpdate(
  212. function (options) {
  213. navigation.update(options);
  214. },
  215. this.chart
  216. );
  217. },
  218. /**
  219. * Hook for click on a button, method selcts/unselects buttons,
  220. * then calls `bindings.init` callback.
  221. *
  222. * @private
  223. * @function Highcharts.NavigationBindings#bindingsButtonClick
  224. *
  225. * @param {Highcharts.HTMLDOMElement} [button]
  226. * Clicked button
  227. *
  228. * @param {object} [events]
  229. * Events passed down from bindings (`init`, `start`, `step`, `end`)
  230. *
  231. * @param {global.Event} [clickEvent]
  232. * Browser's click event
  233. */
  234. bindingsButtonClick: function (button, events, clickEvent) {
  235. var navigation = this,
  236. chart = navigation.chart;
  237. if (navigation.selectedButtonElement) {
  238. fireEvent(
  239. navigation,
  240. 'deselectButton',
  241. { button: navigation.selectedButtonElement }
  242. );
  243. if (navigation.nextEvent) {
  244. // Remove in-progress annotations adders:
  245. if (
  246. navigation.currentUserDetails &&
  247. navigation.currentUserDetails.coll === 'annotations'
  248. ) {
  249. chart.removeAnnotation(navigation.currentUserDetails);
  250. }
  251. navigation.mouseMoveEvent = navigation.nextEvent = false;
  252. }
  253. }
  254. navigation.selectedButton = events;
  255. navigation.selectedButtonElement = button;
  256. fireEvent(navigation, 'selectButton', { button: button });
  257. // Call "init" event, for example to open modal window
  258. if (events.init) {
  259. events.init.call(navigation, button, clickEvent);
  260. }
  261. if (events.start || events.steps) {
  262. chart.renderer.boxWrapper.addClass(PREFIX + 'draw-mode');
  263. }
  264. },
  265. /**
  266. * Hook for click on a chart, first click on a chart calls `start` event,
  267. * then on all subsequent clicks iterate over `steps` array.
  268. * When finished, calls `end` event.
  269. *
  270. * @private
  271. * @function Highcharts.NavigationBindings#bindingsChartClick
  272. *
  273. * @param {Highcharts.Chart} chart
  274. * Chart that click was performed on.
  275. *
  276. * @param {global.Event} clickEvent
  277. * Browser's click event.
  278. */
  279. bindingsChartClick: function (chartContainer, clickEvent) {
  280. var navigation = this,
  281. chart = navigation.chart,
  282. selectedButton = navigation.selectedButton,
  283. svgContainer = chart.renderer.boxWrapper;
  284. if (
  285. navigation.activeAnnotation &&
  286. !clickEvent.activeAnnotation &&
  287. // Element could be removed in the child action, e.g. button
  288. clickEvent.target.parentNode &&
  289. // TO DO: Polyfill for IE11?
  290. !clickEvent.target.closest('.' + PREFIX + 'popup')
  291. ) {
  292. fireEvent(navigation, 'closePopup');
  293. navigation.deselectAnnotation();
  294. }
  295. if (!selectedButton || !selectedButton.start) {
  296. return;
  297. }
  298. if (!navigation.nextEvent) {
  299. // Call init method:
  300. navigation.currentUserDetails = selectedButton.start.call(
  301. navigation,
  302. clickEvent
  303. );
  304. // If steps exists (e.g. Annotations), bind them:
  305. if (selectedButton.steps) {
  306. navigation.stepIndex = 0;
  307. navigation.steps = true;
  308. navigation.mouseMoveEvent = navigation.nextEvent =
  309. selectedButton.steps[navigation.stepIndex];
  310. } else {
  311. fireEvent(
  312. navigation,
  313. 'deselectButton',
  314. { button: navigation.selectedButtonElement }
  315. );
  316. svgContainer.removeClass(PREFIX + 'draw-mode');
  317. navigation.steps = false;
  318. navigation.selectedButton = null;
  319. // First click is also the last one:
  320. if (selectedButton.end) {
  321. selectedButton.end.call(
  322. navigation,
  323. clickEvent,
  324. navigation.currentUserDetails
  325. );
  326. }
  327. }
  328. } else {
  329. navigation.nextEvent(
  330. clickEvent,
  331. navigation.currentUserDetails
  332. );
  333. if (navigation.steps) {
  334. navigation.stepIndex++;
  335. if (selectedButton.steps[navigation.stepIndex]) {
  336. // If we have more steps, bind them one by one:
  337. navigation.mouseMoveEvent = navigation.nextEvent =
  338. selectedButton.steps[navigation.stepIndex];
  339. } else {
  340. fireEvent(
  341. navigation,
  342. 'deselectButton',
  343. { button: navigation.selectedButtonElement }
  344. );
  345. svgContainer.removeClass(PREFIX + 'draw-mode');
  346. // That was the last step, call end():
  347. if (selectedButton.end) {
  348. selectedButton.end.call(
  349. navigation,
  350. clickEvent,
  351. navigation.currentUserDetails
  352. );
  353. }
  354. navigation.nextEvent = false;
  355. navigation.mouseMoveEvent = false;
  356. navigation.selectedButton = null;
  357. }
  358. }
  359. }
  360. },
  361. /**
  362. * Hook for mouse move on a chart's container. It calls current step.
  363. *
  364. * @private
  365. * @function Highcharts.NavigationBindings#bindingsContainerMouseMove
  366. *
  367. * @param {Highcharts.HTMLDOMElement} container
  368. * Chart's container.
  369. *
  370. * @param {global.Event} moveEvent
  371. * Browser's move event.
  372. */
  373. bindingsContainerMouseMove: function (container, moveEvent) {
  374. if (this.mouseMoveEvent) {
  375. this.mouseMoveEvent(
  376. moveEvent,
  377. this.currentUserDetails
  378. );
  379. }
  380. },
  381. /**
  382. * Translate fields (e.g. `params.period` or `marker.styles.color`) to
  383. * Highcharts options object (e.g. `{ params: { period } }`).
  384. *
  385. * @private
  386. * @function Highcharts.NavigationBindings#fieldsToOptions
  387. *
  388. * @param {object} fields
  389. * Fields from popup form.
  390. *
  391. * @param {object} config
  392. * Default config to be modified.
  393. *
  394. * @return {object}
  395. * Modified config
  396. */
  397. fieldsToOptions: function (fields, config) {
  398. objectEach(fields, function (value, field) {
  399. var parsedValue = parseFloat(value),
  400. path = field.split('.'),
  401. parent = config,
  402. pathLength = path.length - 1;
  403. // If it's a number (not "forma" options), parse it:
  404. if (
  405. isNumber(parsedValue) &&
  406. !value.match(/px/g) &&
  407. !field.match(/format/g)
  408. ) {
  409. value = parsedValue;
  410. }
  411. // Remove empty strings or values like 0
  412. if (value !== '' && value !== 'undefined') {
  413. path.forEach(function (name, index) {
  414. var nextName = pick(path[index + 1], '');
  415. if (pathLength === index) {
  416. // Last index, put value:
  417. parent[name] = value;
  418. } else if (!parent[name]) {
  419. // Create middle property:
  420. parent[name] = nextName.match(/\d/g) ? [] : {};
  421. parent = parent[name];
  422. } else {
  423. // Jump into next property
  424. parent = parent[name];
  425. }
  426. });
  427. }
  428. });
  429. return config;
  430. },
  431. /**
  432. * Shorthand method to deselect an annotation.
  433. *
  434. * @function Highcharts.NavigationBindings#deselectAnnotation
  435. */
  436. deselectAnnotation: function () {
  437. if (this.activeAnnotation) {
  438. this.activeAnnotation.setControlPointsVisibility(false);
  439. this.activeAnnotation = false;
  440. }
  441. },
  442. /**
  443. * Generates API config for popup in the same format as options for
  444. * Annotation object.
  445. *
  446. * @function Highcharts.NavigationBindings#annotationToFields
  447. *
  448. * @param {Highcharts.Annotation} annotation
  449. * Annotations object
  450. *
  451. * @return {object}
  452. * Annotation options to be displayed in popup box
  453. */
  454. annotationToFields: function (annotation) {
  455. var options = annotation.options,
  456. editables = H.NavigationBindings.annotationsEditable,
  457. nestedEditables = editables.nestedOptions,
  458. getFieldType = this.utils.getFieldType,
  459. type = pick(
  460. options.type,
  461. options.shapes && options.shapes[0] &&
  462. options.shapes[0].type,
  463. options.labels && options.labels[0] &&
  464. options.labels[0].itemType,
  465. 'label'
  466. ),
  467. nonEditables = H.NavigationBindings
  468. .annotationsNonEditable[options.langKey] || [],
  469. visualOptions = {
  470. langKey: options.langKey,
  471. type: type
  472. };
  473. /**
  474. * Nested options traversing. Method goes down to the options and copies
  475. * allowed options (with values) to new object, which is last parameter:
  476. * "parent".
  477. *
  478. * @private
  479. * @function Highcharts.NavigationBindings#annotationToFields.traverse
  480. *
  481. * @param {*} option
  482. * Atomic type or object/array
  483. *
  484. * @param {string} key
  485. * Option name, for example "visible" or "x", "y"
  486. *
  487. * @param {object} allowed
  488. * Editables from H.NavigationBindings.annotationsEditable
  489. *
  490. * @param {object} parent
  491. * Where new options will be assigned
  492. */
  493. function traverse(option, key, parentEditables, parent) {
  494. var nextParent;
  495. if (
  496. parentEditables &&
  497. nonEditables.indexOf(key) === -1 &&
  498. (
  499. (
  500. parentEditables.indexOf &&
  501. parentEditables.indexOf(key)
  502. ) >= 0 ||
  503. parentEditables[key] || // nested array
  504. parentEditables === true // simple array
  505. )
  506. ) {
  507. // Roots:
  508. if (isArray(option)) {
  509. parent[key] = [];
  510. option.forEach(function (arrayOption, i) {
  511. if (!isObject(arrayOption)) {
  512. // Simple arrays, e.g. [String, Number, Boolean]
  513. traverse(
  514. arrayOption,
  515. 0,
  516. nestedEditables[key],
  517. parent[key]
  518. );
  519. } else {
  520. // Advanced arrays, e.g. [Object, Object]
  521. parent[key][i] = {};
  522. objectEach(
  523. arrayOption,
  524. function (nestedOption, nestedKey) {
  525. traverse(
  526. nestedOption,
  527. nestedKey,
  528. nestedEditables[key],
  529. parent[key][i]
  530. );
  531. }
  532. );
  533. }
  534. });
  535. } else if (isObject(option)) {
  536. nextParent = {};
  537. if (isArray(parent)) {
  538. parent.push(nextParent);
  539. nextParent[key] = {};
  540. nextParent = nextParent[key];
  541. } else {
  542. parent[key] = nextParent;
  543. }
  544. objectEach(option, function (nestedOption, nestedKey) {
  545. traverse(
  546. nestedOption,
  547. nestedKey,
  548. key === 0 ? parentEditables : nestedEditables[key],
  549. nextParent
  550. );
  551. });
  552. } else {
  553. // Leaf:
  554. if (key === 'format') {
  555. parent[key] = [
  556. H.format(
  557. option,
  558. annotation.labels[0].points[0]
  559. ).toString(),
  560. 'text'
  561. ];
  562. } else if (isArray(parent)) {
  563. parent.push([option, getFieldType(option)]);
  564. } else {
  565. parent[key] = [option, getFieldType(option)];
  566. }
  567. }
  568. }
  569. }
  570. objectEach(options, function (option, key) {
  571. if (key === 'typeOptions') {
  572. visualOptions[key] = {};
  573. objectEach(options[key], function (typeOption, typeKey) {
  574. traverse(
  575. typeOption,
  576. typeKey,
  577. nestedEditables,
  578. visualOptions[key],
  579. true
  580. );
  581. });
  582. } else {
  583. traverse(option, key, editables[type], visualOptions);
  584. }
  585. });
  586. return visualOptions;
  587. },
  588. /**
  589. * Get all class names for all parents in the element. Iterates until finds
  590. * main container.
  591. *
  592. * @function Highcharts.NavigationBindings#getClickedClassNames
  593. *
  594. * @param {Highcharts.HTMLDOMElement}
  595. * Container that event is bound to.
  596. *
  597. * @param {global.Event} event
  598. * Browser's event.
  599. *
  600. * @return {Array<string>}
  601. * Array of class names with corresponding elements
  602. */
  603. getClickedClassNames: function (container, event) {
  604. var element = event.target,
  605. classNames = [],
  606. elemClassName;
  607. while (element) {
  608. elemClassName = H.attr(element, 'class');
  609. if (elemClassName) {
  610. classNames = classNames.concat(
  611. elemClassName.split(' ').map(
  612. function (name) { // eslint-disable-line no-loop-func
  613. return [
  614. name,
  615. element
  616. ];
  617. }
  618. )
  619. );
  620. }
  621. element = element.parentNode;
  622. if (element === container) {
  623. return classNames;
  624. }
  625. }
  626. return classNames;
  627. },
  628. /**
  629. * Get events bound to a button. It's a custom event delegation to find all
  630. * events connected to the element.
  631. *
  632. * @function Highcharts.NavigationBindings#getButtonEvents
  633. *
  634. * @param {Highcharts.HTMLDOMElement}
  635. * Container that event is bound to.
  636. *
  637. * @param {global.Event} event
  638. * Browser's event.
  639. *
  640. * @return {object}
  641. * Oject with events (init, start, steps, and end)
  642. */
  643. getButtonEvents: function (container, event) {
  644. var navigation = this,
  645. classNames = this.getClickedClassNames(container, event),
  646. bindings;
  647. classNames.forEach(function (className) {
  648. if (navigation.boundClassNames[className[0]] && !bindings) {
  649. bindings = {
  650. events: navigation.boundClassNames[className[0]],
  651. button: className[1]
  652. };
  653. }
  654. });
  655. return bindings;
  656. },
  657. /**
  658. * Bindings are just events, so the whole update process is simply
  659. * removing old events and adding new ones.
  660. *
  661. * @private
  662. * @function Highcharts.NavigationBindings#update
  663. */
  664. update: function (options) {
  665. this.options = merge(true, this.options, options);
  666. this.removeEvents();
  667. this.initEvents();
  668. },
  669. /**
  670. * Remove all events created in the navigation.
  671. *
  672. * @private
  673. * @function Highcharts.NavigationBindings#removeEvents
  674. */
  675. removeEvents: function () {
  676. this.eventsToUnbind.forEach(function (unbinder) {
  677. unbinder();
  678. });
  679. },
  680. destroy: function () {
  681. this.removeEvents();
  682. },
  683. /**
  684. * General utils for bindings
  685. *
  686. * @private
  687. * @name Highcharts.NavigationBindings#utils
  688. * @type {bindingsUtils}
  689. */
  690. utils: bindingsUtils
  691. });
  692. H.Chart.prototype.initNavigationBindings = function () {
  693. var chart = this,
  694. options = chart.options;
  695. if (options && options.navigation && options.navigation.bindings) {
  696. chart.navigationBindings = new H.NavigationBindings(
  697. chart,
  698. options.navigation
  699. );
  700. chart.navigationBindings.initEvents();
  701. chart.navigationBindings.initUpdate();
  702. }
  703. };
  704. addEvent(H.Chart, 'load', function () {
  705. this.initNavigationBindings();
  706. });
  707. addEvent(H.Chart, 'destroy', function () {
  708. if (this.navigationBindings) {
  709. this.navigationBindings.destroy();
  710. }
  711. });
  712. addEvent(H.NavigationBindings, 'deselectButton', function () {
  713. this.selectedButtonElement = null;
  714. });
  715. // Show edit-annotation form:
  716. function selectableAnnotation(annotationType) {
  717. var originalClick = annotationType.prototype.defaultOptions.events &&
  718. annotationType.prototype.defaultOptions.events.click;
  719. function selectAndshowPopup(event) {
  720. var annotation = this,
  721. navigation = annotation.chart.navigationBindings,
  722. prevAnnotation = navigation.activeAnnotation;
  723. if (originalClick) {
  724. originalClick.click.call(annotation, event);
  725. }
  726. if (prevAnnotation !== annotation) {
  727. // Select current:
  728. navigation.deselectAnnotation();
  729. navigation.activeAnnotation = annotation;
  730. annotation.setControlPointsVisibility(true);
  731. fireEvent(
  732. navigation,
  733. 'showPopup',
  734. {
  735. annotation: annotation,
  736. formType: 'annotation-toolbar',
  737. options: navigation.annotationToFields(annotation),
  738. onSubmit: function (data) {
  739. var config = {},
  740. typeOptions;
  741. if (data.actionType === 'remove') {
  742. navigation.activeAnnotation = false;
  743. navigation.chart.removeAnnotation(annotation);
  744. } else {
  745. navigation.fieldsToOptions(data.fields, config);
  746. navigation.deselectAnnotation();
  747. typeOptions = config.typeOptions;
  748. if (annotation.options.type === 'measure') {
  749. // Manually disable crooshars according to
  750. // stroke width of the shape:
  751. typeOptions.crosshairY.enabled =
  752. typeOptions.crosshairY.strokeWidth !== 0;
  753. typeOptions.crosshairX.enabled =
  754. typeOptions.crosshairX.strokeWidth !== 0;
  755. }
  756. annotation.update(config);
  757. }
  758. }
  759. }
  760. );
  761. } else {
  762. // Deselect current:
  763. navigation.deselectAnnotation();
  764. fireEvent(navigation, 'closePopup');
  765. }
  766. // Let bubble event to chart.click:
  767. event.activeAnnotation = true;
  768. }
  769. H.merge(
  770. true,
  771. annotationType.prototype.defaultOptions.events,
  772. {
  773. click: selectAndshowPopup
  774. }
  775. );
  776. }
  777. if (H.Annotation) {
  778. // Basic shapes:
  779. selectableAnnotation(H.Annotation);
  780. // Advanced annotations:
  781. H.objectEach(H.Annotation.types, function (annotationType) {
  782. selectableAnnotation(annotationType);
  783. });
  784. }
  785. H.setOptions({
  786. /**
  787. * @optionparent lang
  788. */
  789. lang: {
  790. /**
  791. * Configure the Popup strings in the chart. Requires the
  792. * `annotations.js` or `annotations-advanced.src.js` module to be
  793. * loaded.
  794. *
  795. * @since 7.0.0
  796. * @type {Object}
  797. * @product highcharts highstock
  798. */
  799. navigation: {
  800. /**
  801. * Translations for all field names used in popup.
  802. *
  803. * @product highcharts highstock
  804. * @type {Object}
  805. */
  806. popup: {
  807. simpleShapes: 'Simple shapes',
  808. lines: 'Lines',
  809. circle: 'Circle',
  810. rectangle: 'Rectangle',
  811. label: 'Label',
  812. shapeOptions: 'Shape options',
  813. typeOptions: 'Details',
  814. fill: 'Fill',
  815. format: 'Text',
  816. strokeWidth: 'Line width',
  817. stroke: 'Line color',
  818. title: 'Title',
  819. name: 'Name',
  820. labelOptions: 'Label options',
  821. labels: 'Labels',
  822. backgroundColor: 'Background color',
  823. backgroundColors: 'Background colors',
  824. borderColor: 'Border color',
  825. borderRadius: 'Border radius',
  826. borderWidth: 'Border width',
  827. style: 'Style',
  828. padding: 'Padding',
  829. fontSize: 'Font size',
  830. color: 'Color',
  831. height: 'Height',
  832. shapes: 'Shape options'
  833. }
  834. }
  835. },
  836. /**
  837. * @optionparent navigation
  838. * @product highcharts highstock
  839. */
  840. navigation: {
  841. /**
  842. * A CSS class name where all bindings will be attached to. Multiple
  843. * charts on the same page should have separate class names to prevent
  844. * duplicating events.
  845. *
  846. * @since 7.0.0
  847. * @type {string}
  848. */
  849. bindingsClassName: 'highcharts-bindings-wrapper',
  850. /**
  851. * Bindings definitions for custom HTML buttons. Each binding implements
  852. * simple event-driven interface:
  853. *
  854. * - `className`: classname used to bind event to
  855. *
  856. * - `init`: initial event, fired on button click
  857. *
  858. * - `start`: fired on first click on a chart
  859. *
  860. * - `steps`: array of sequential events fired one after another on each
  861. * of users clicks
  862. *
  863. * - `end`: last event to be called after last step event
  864. *
  865. * @type {Highcharts.Dictionary<Highcharts.StockToolsBindingsObject>|*}
  866. * @sample stock/stocktools/stocktools-thresholds
  867. * Custom bindings in Highstock
  868. * @since 7.0.0
  869. * @product highcharts highstock
  870. */
  871. bindings: {
  872. /**
  873. * A circle annotation bindings. Includes `start` and one event in
  874. * `steps` array.
  875. *
  876. * @type {Highcharts.StockToolsBindingsObject}
  877. * @default {"className": "highcharts-circle-annotation", "start": function() {}, "steps": [function() {}]}
  878. */
  879. circleAnnotation: {
  880. /** @ignore */
  881. className: 'highcharts-circle-annotation',
  882. /** @ignore */
  883. start: function (e) {
  884. var x = this.chart.xAxis[0].toValue(e.chartX),
  885. y = this.chart.yAxis[0].toValue(e.chartY),
  886. annotation;
  887. annotation = this.chart.addAnnotation({
  888. langKey: 'circle',
  889. shapes: [{
  890. type: 'circle',
  891. point: {
  892. xAxis: 0,
  893. yAxis: 0,
  894. x: x,
  895. y: y
  896. },
  897. r: 5,
  898. controlPoints: [{
  899. positioner: function (target) {
  900. var xy = H.Annotation.MockPoint
  901. .pointToPixels(
  902. target.points[0]
  903. ),
  904. r = target.options.r;
  905. return {
  906. x: xy.x + r * Math.cos(Math.PI / 4) -
  907. this.graphic.width / 2,
  908. y: xy.y + r * Math.sin(Math.PI / 4) -
  909. this.graphic.height / 2
  910. };
  911. },
  912. events: {
  913. // TRANSFORM RADIUS ACCORDING TO Y
  914. // TRANSLATION
  915. drag: function (e, target) {
  916. var annotation = target.annotation,
  917. position = this
  918. .mouseMoveToTranslation(e);
  919. target.setRadius(
  920. Math.max(
  921. target.options.r +
  922. position.y /
  923. Math.sin(Math.PI / 4),
  924. 5
  925. )
  926. );
  927. annotation.options.shapes[0] =
  928. annotation.userOptions.shapes[0] =
  929. target.options;
  930. target.redraw(false);
  931. }
  932. }
  933. }]
  934. }]
  935. });
  936. return annotation;
  937. },
  938. /** @ignore */
  939. steps: [
  940. function (e, annotation) {
  941. var point = annotation.options.shapes[0].point,
  942. x = this.chart.xAxis[0].toPixels(point.x),
  943. y = this.chart.yAxis[0].toPixels(point.y),
  944. distance = Math.max(
  945. Math.sqrt(
  946. Math.pow(x - e.chartX, 2) +
  947. Math.pow(y - e.chartY, 2)
  948. ),
  949. 5
  950. );
  951. annotation.update({
  952. shapes: [{
  953. r: distance
  954. }]
  955. });
  956. }
  957. ]
  958. },
  959. /**
  960. * A rectangle annotation bindings. Includes `start` and one event
  961. * in `steps` array.
  962. *
  963. * @type {Highcharts.StockToolsBindingsObject}
  964. * @default {"className": "highcharts-rectangle-annotation", "start": function() {}, "steps": [function() {}]}
  965. */
  966. rectangleAnnotation: {
  967. /** @ignore */
  968. className: 'highcharts-rectangle-annotation',
  969. /** @ignore */
  970. start: function (e) {
  971. var x = this.chart.xAxis[0].toValue(e.chartX),
  972. y = this.chart.yAxis[0].toValue(e.chartY),
  973. options = {
  974. langKey: 'rectangle',
  975. shapes: [{
  976. type: 'rect',
  977. point: {
  978. x: x,
  979. y: y,
  980. xAxis: 0,
  981. yAxis: 0
  982. },
  983. width: 5,
  984. height: 5,
  985. controlPoints: [{
  986. positioner: function (target) {
  987. var xy = H.Annotation.MockPoint
  988. .pointToPixels(
  989. target.points[0]
  990. );
  991. return {
  992. x: xy.x + target.options.width - 4,
  993. y: xy.y + target.options.height - 4
  994. };
  995. },
  996. events: {
  997. drag: function (e, target) {
  998. var annotation = target.annotation,
  999. xy = this
  1000. .mouseMoveToTranslation(e);
  1001. target.options.width = Math.max(
  1002. target.options.width + xy.x,
  1003. 5
  1004. );
  1005. target.options.height = Math.max(
  1006. target.options.height + xy.y,
  1007. 5
  1008. );
  1009. annotation.options.shapes[0] =
  1010. target.options;
  1011. annotation.userOptions.shapes[0] =
  1012. target.options;
  1013. target.redraw(false);
  1014. }
  1015. }
  1016. }]
  1017. }]
  1018. };
  1019. return this.chart.addAnnotation(options);
  1020. },
  1021. /** @ignore */
  1022. steps: [
  1023. function (e, annotation) {
  1024. var xAxis = this.chart.xAxis[0],
  1025. yAxis = this.chart.yAxis[0],
  1026. point = annotation.options.shapes[0].point,
  1027. x = xAxis.toPixels(point.x),
  1028. y = yAxis.toPixels(point.y),
  1029. width = Math.max(e.chartX - x, 5),
  1030. height = Math.max(e.chartY - y, 5);
  1031. annotation.update({
  1032. shapes: [{
  1033. width: width,
  1034. height: height,
  1035. point: {
  1036. x: point.x,
  1037. y: point.y
  1038. }
  1039. }]
  1040. });
  1041. }
  1042. ]
  1043. },
  1044. /**
  1045. * A label annotation bindings. Includes `start` event only.
  1046. *
  1047. * @type {Highcharts.StockToolsBindingsObject}
  1048. * @default {"className": "highcharts-label-annotation", "start": function() {}, "steps": [function() {}]}
  1049. */
  1050. labelAnnotation: {
  1051. /** @ignore */
  1052. className: 'highcharts-label-annotation',
  1053. /** @ignore */
  1054. start: function (e) {
  1055. var x = this.chart.xAxis[0].toValue(e.chartX),
  1056. y = this.chart.yAxis[0].toValue(e.chartY);
  1057. this.chart.addAnnotation({
  1058. langKey: 'label',
  1059. labelOptions: {
  1060. format: '{y:.2f}'
  1061. },
  1062. labels: [{
  1063. point: {
  1064. x: x,
  1065. y: y,
  1066. xAxis: 0,
  1067. yAxis: 0
  1068. },
  1069. controlPoints: [{
  1070. symbol: 'triangle-down',
  1071. positioner: function (target) {
  1072. if (!target.graphic.placed) {
  1073. return {
  1074. x: 0,
  1075. y: -9e7
  1076. };
  1077. }
  1078. var xy = H.Annotation.MockPoint
  1079. .pointToPixels(
  1080. target.points[0]
  1081. );
  1082. return {
  1083. x: xy.x - this.graphic.width / 2,
  1084. y: xy.y - this.graphic.height / 2
  1085. };
  1086. },
  1087. // TRANSLATE POINT/ANCHOR
  1088. events: {
  1089. drag: function (e, target) {
  1090. var xy = this.mouseMoveToTranslation(e);
  1091. target.translatePoint(xy.x, xy.y);
  1092. target.annotation.labels[0].options =
  1093. target.options;
  1094. target.redraw(false);
  1095. }
  1096. }
  1097. }, {
  1098. symbol: 'square',
  1099. positioner: function (target) {
  1100. if (!target.graphic.placed) {
  1101. return {
  1102. x: 0,
  1103. y: -9e7
  1104. };
  1105. }
  1106. return {
  1107. x: target.graphic.alignAttr.x -
  1108. this.graphic.width / 2,
  1109. y: target.graphic.alignAttr.y -
  1110. this.graphic.height / 2
  1111. };
  1112. },
  1113. // TRANSLATE POSITION WITHOUT CHANGING THE
  1114. // ANCHOR
  1115. events: {
  1116. drag: function (e, target) {
  1117. var xy = this.mouseMoveToTranslation(e);
  1118. target.translate(xy.x, xy.y);
  1119. target.annotation.labels[0].options =
  1120. target.options;
  1121. target.redraw(false);
  1122. }
  1123. }
  1124. }],
  1125. overflow: 'none',
  1126. crop: true
  1127. }]
  1128. });
  1129. }
  1130. }
  1131. },
  1132. /**
  1133. * A `showPopup` event. Fired when selecting for example an annotation.
  1134. *
  1135. * @type {Function}
  1136. * @apioption navigation.events.showPopup
  1137. */
  1138. /**
  1139. * A `hidePopop` event. Fired when Popup should be hidden, for exampole
  1140. * when clicking on an annotation again.
  1141. *
  1142. * @type {Function}
  1143. * @apioption navigation.events.hidePopup
  1144. */
  1145. /**
  1146. * Event fired on a button click.
  1147. *
  1148. * @type {Function}
  1149. * @sample highcharts/annotations/gui/
  1150. * Change icon in a dropddown on event
  1151. * @sample highcharts/annotations/gui-buttons/
  1152. * Change button class on event
  1153. * @apioption navigation.events.selectButton
  1154. */
  1155. /**
  1156. * Event fired when button state should change, for example after
  1157. * adding an annotation.
  1158. *
  1159. * @type {Function}
  1160. * @sample highcharts/annotations/gui/
  1161. * Change icon in a dropddown on event
  1162. * @sample highcharts/annotations/gui-buttons/
  1163. * Change button class on event
  1164. * @apioption navigation.events.deselectButton
  1165. */
  1166. /**
  1167. * Events to communicate between Stock Tools and custom GUI.
  1168. *
  1169. * @since 7.0.0
  1170. * @product highcharts highstock
  1171. * @optionparent navigation.events
  1172. */
  1173. events: {}
  1174. }
  1175. });