navigationBindings.js 48 KB

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