diff --git a/app/services/work_packages/set_attributes_service.rb b/app/services/work_packages/set_attributes_service.rb index b0c40bef6df6..51b596476205 100644 --- a/app/services/work_packages/set_attributes_service.rb +++ b/app/services/work_packages/set_attributes_service.rb @@ -69,7 +69,7 @@ def set_custom_values_to_validate(attributes, validate_custom_fields = nil) def set_static_attributes(attributes) assignable_attributes = attributes.select do |key, _| - !CustomField.custom_field_attribute?(key) && work_package.respond_to?(key) + !CustomField.custom_field_attribute?(key) && work_package.respond_to?("#{key}=") end work_package.attributes = assignable_attributes diff --git a/config/locales/en.yml b/config/locales/en.yml index 027f316bc786..4c7afcb9a696 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4130,6 +4130,7 @@ en: notice_successful_delete: "Successful deletion." notice_successful_cancel: "Successful cancellation." notice_successful_update: "Successful update." + notice_successful_move: "Successful move from %{from} to %{to}." notice_unsuccessful_create: "Creation failed." notice_unsuccessful_create_with_reason: "Creation failed: %{reason}" notice_unsuccessful_update: "Update failed." diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ff9f10abdb08..2d7c355b75d5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -92,7 +92,6 @@ "jquery": "^3.7.1", "jquery.caret": "^0.3.1", "jquery.cookie": "^1.4.1", - "jquery.flot": "^0.8.3", "json5": "^2.2.2", "lit-html": "^3.3.2", "lodash": "^4.17.23", @@ -16955,11 +16954,6 @@ "integrity": "sha512-c/hZOOL+8VSw/FkTVH637gS1/6YzMSCROpTZ2qBYwJ7s7sHajU7uBkSSiE5+GXWwrfCCyO+jsYjUQ7Hs2rIxAA==", "license": "MIT" }, - "node_modules/jquery.flot": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz", - "integrity": "sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -36583,11 +36577,6 @@ "resolved": "https://registry.npmjs.org/jquery.cookie/-/jquery.cookie-1.4.1.tgz", "integrity": "sha512-c/hZOOL+8VSw/FkTVH637gS1/6YzMSCROpTZ2qBYwJ7s7sHajU7uBkSSiE5+GXWwrfCCyO+jsYjUQ7Hs2rIxAA==" }, - "jquery.flot": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/jquery.flot/-/jquery.flot-0.8.3.tgz", - "integrity": "sha512-/tEE8J5NjwvStHDaCHkvTJpD7wDS4hE1OEL8xEmhgQfUe0gLUem923PIceNez1mz4yBNx6Hjv7pJcowLNd+nbg==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 791f4b6386db..50478a0686b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -147,7 +147,6 @@ "jquery": "^3.7.1", "jquery.caret": "^0.3.1", "jquery.cookie": "^1.4.1", - "jquery.flot": "^0.8.3", "json5": "^2.2.2", "lit-html": "^3.3.2", "lodash": "^4.17.23", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 028cdc1ed2c2..802e6b3ec6e4 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -206,6 +206,7 @@ import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-pack import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component'; import { MyPageComponent } from './features/my-page/my-page.component'; import { DashboardComponent } from './features/overview/dashboard.component'; +import { BurndownChartComponent } from './features/backlogs/burndown-chart.component'; export function initializeServices(injector:Injector) { return () => { @@ -419,5 +420,6 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-my-page', MyPageComponent, { injector }); registerCustomElement('opce-dashboard', DashboardComponent, { injector }); + registerCustomElement('opce-burndown-chart', BurndownChartComponent, { injector }); } } diff --git a/frontend/src/app/features/backlogs/burndown-chart.component.html b/frontend/src/app/features/backlogs/burndown-chart.component.html new file mode 100644 index 000000000000..fb72c70a9ac1 --- /dev/null +++ b/frontend/src/app/features/backlogs/burndown-chart.component.html @@ -0,0 +1,18 @@ +
+ +
+ +@if (isDevMode) { +
+ +
+ Debug + +
{{maxValue() }}
+
{{lineChartData() | json}}
+
+
+} diff --git a/frontend/src/app/features/backlogs/burndown-chart.component.ts b/frontend/src/app/features/backlogs/burndown-chart.component.ts new file mode 100644 index 000000000000..7989f667c4cf --- /dev/null +++ b/frontend/src/app/features/backlogs/burndown-chart.component.ts @@ -0,0 +1,86 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { JsonPipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; +import { ChartData, ChartOptions } from 'chart.js'; +import { I18nService } from 'core-app/core/i18n/i18n.service'; +import PrimerColorsPlugin from 'core-app/shared/components/work-package-graphs/plugin.primer-colors'; +import { BaseChartDirective, provideCharts, withDefaultRegisterables } from 'ng2-charts'; +import { environment } from '../../../environments/environment'; + +const BURNDOWN_Y_SCALE_MIN = 25; + +@Component({ + selector: 'op-burndown-chart', + templateUrl: './burndown-chart.component.html', + imports: [BaseChartDirective, JsonPipe], + providers: [provideCharts(withDefaultRegisterables(PrimerColorsPlugin))], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BurndownChartComponent { + readonly isDevMode = !environment.production; + readonly i18n = inject(I18nService); + readonly chartData = input.required(); + + readonly lineChartData = computed>(() => { + const data = JSON.parse(this.chartData()) as ChartData<'line'>; + return data; + }); + + readonly maxValue = computed(() => { + return this.lineChartData().datasets + .flatMap((dataset) => dataset.data) + .filter((item):item is number => typeof item === 'number') + .reduce((a, b) => Math.max(a, b), 0); + }); + + readonly lineChartOptions = computed>(() => ({ + scales: { + x: { + title: { + display: true, + text: this.i18n.t('js.burndown.day') + } + }, + y: { + title: { + display: true, + text: this.i18n.t('js.burndown.points') + }, + suggestedMin: 0, + max: this.maxValue() + BURNDOWN_Y_SCALE_MIN + } + }, + plugins: { + legend: { + position: 'top' + } + } + })); +} diff --git a/frontend/src/app/features/plugins/plugin-context.ts b/frontend/src/app/features/plugins/plugin-context.ts index 2dbd58f1d8f0..c51d0a50d798 100644 --- a/frontend/src/app/features/plugins/plugin-context.ts +++ b/frontend/src/app/features/plugins/plugin-context.ts @@ -31,6 +31,7 @@ import { HttpClient } from '@angular/common/http'; import { TimezoneService } from 'core-app/core/datetime/timezone.service'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { HalEventsService } from '../hal/services/hal-events.service'; /** * Plugin context bridge for plugins outside the CLI compiler context * in order to access services and parts of the core application @@ -48,6 +49,7 @@ export class OpenProjectPluginContext { confirmDialog: this.injector.get(ConfirmDialogService), externalQueryConfiguration: this.injector.get(ExternalQueryConfigurationService), externalRelationQueryConfiguration: this.injector.get(ExternalRelationQueryConfigurationService), + halEvents: this.injector.get(HalEventsService), halResource: this.injector.get(HalResourceService), hooks: this.injector.get(HookService), i18n: this.injector.get(I18nService), diff --git a/frontend/src/assets/sass/backlogs/_index.sass b/frontend/src/assets/sass/backlogs/_index.sass index bb1eb5d44a78..0f2b73435f43 100644 --- a/frontend/src/assets/sass/backlogs/_index.sass +++ b/frontend/src/assets/sass/backlogs/_index.sass @@ -33,6 +33,11 @@ * See COPYRIGHT and LICENSE files for more details. */ +// Variables +@import "../../../global_styles/openproject/_variable_defaults.scss" + +@import "../../../global_styles/openproject/_variables.sass" + @import global @import global_print @import jqplot diff --git a/frontend/src/assets/sass/backlogs/_master_backlog.sass b/frontend/src/assets/sass/backlogs/_master_backlog.sass index a6f0a9e3cd13..159b6dd9ae24 100644 --- a/frontend/src/assets/sass/backlogs/_master_backlog.sass +++ b/frontend/src/assets/sass/backlogs/_master_backlog.sass @@ -26,299 +26,116 @@ * See COPYRIGHT and LICENSE files for more details. ++ */ -#rb - #backlogs_container - width: 100% +$op-backlogs-header--points-min-width: 5rem +$op-backlogs-header--points-min-width-narrow: 2rem + +.op-backlogs-header + display: grid + grid-template-columns: 1fr minmax($op-backlogs-header--points-min-width, max-content) auto + grid-template-areas: "collapsible points menu" + align-items: center + +.op-backlogs-header--collapsible + margin-left: calc(var(--stack-padding-normal) / 2) + +.op-backlogs-header--points + margin-left: var(--stack-gap-normal) + font-variant-numeric: tabular-nums + +.op-backlogs-header--menu + margin-left: var(--stack-gap-normal) + +.op-backlogs-collapsible + display: flex + flex-wrap: wrap + align-items: center + column-gap: var(--stack-gap-normal) + row-gap: var(--base-size-4) + flex: 1 + + &--title-line display: flex - flex-wrap: wrap - justify-content: space-between - #owner_backlogs_container - min-width: 420px - order: 2 - width: 49% - flex: 0 0 49% - #sprint_backlogs_container - min-width: 420px - width: 49% - flex: 0 0 49% - min-height: 230px - #owner_backlogs_container .backlog .header > .add_new_story - height: 28px - line-height: 31px - padding: 0 - position: absolute - right: 10px - text-align: right - top: 1px - width: 100px - #backlogs_container .backlog - border: 1px solid var(--borderColor-default) - display: block - margin: 0 0 10px 0 - width: 100% + align-items: center + gap: var(--stack-gap-condensed) + flex: 1 + min-width: fit-content -#rb - #backlogs_container .backlog .header - background-color: var(--bgColor-muted) - height: 30px - position: relative - width: 100% - .backlog .header .backlog-menu - border-right: 1px solid var(--borderColor-default) - cursor: pointer - height: 30px - overflow: visible - position: absolute - top: 0 - right: 0 - width: 30px - .icon-context - position: absolute - top: 7px - left: 12px - // Firefox wrongly positions icon - &:before - padding: 0 - &.open - &+ .items - display: block - .items - display: none - background-color: var(--overlay-bgColor) - border: 1px solid var(--borderColor-default) - position: absolute - top: 30px - right: -2px - list-style: none - margin: 0 - padding: 0 - z-index: 1000 - .item - display: block - width: 160px - height: 2rem - font-size: 0.9rem - text-align: left - text-decoration: none - vertical-align: middle - overflow: hidden - white-space: nowrap - &.hover, &:hover - background-color: #999 - a - display: block - height: 100% - padding: 6px - width: 100% - &.hover a, &:hover a - color: #FFFFFF - text-decoration: none - #backlogs_container - .backlog - .header - .velocity - height: 28px - line-height: 31px - padding: 0 3px 0 9px - position: absolute - right: 25px - text-align: right - top: 0px - width: 32px - .toggler - font-family: "openproject-icon-font" - height: 30px - line-height: 31px - padding: 0 - position: absolute - left: 0 - top: 0 - width: 23px - cursor: pointer - &:before - position: absolute - left: 6px - top: 10px - &.closed:before - position: absolute - left: 6px - top: 10px - &:hover - cursor: pointer - background-color: #D8D8D8 - .sprint - background-color: transparent - cursor: pointer - display: block - height: 29px - width: auto - margin-left: 30px - margin-right: 50px - &.error.icon-bug - background: none - text-align: center - &:before - position: absolute - color: red - .id, .status - display: none + &--description + display: inline + white-space: nowrap - .name - line-height: 2rem - font-weight: var(--base-text-weight-bold) - overflow: hidden - white-space: nowrap - margin-left: 0.5em +.op-backlogs-story + display: grid + grid-template-columns: var(--control-xsmall-size) 1fr minmax($op-backlogs-header--points-min-width, max-content) auto + grid-template-rows: auto auto + grid-template-areas: "drag_handle info_line points menu" ". subject subject subject" + align-items: center + margin-top: calc(-1 * var(--base-size-4)) + margin-bottom: var(--base-size-4) - .start_date, .effective_date - float: right - height: 28px - line-height: 2rem - width: 6.5em - margin-left: 0.5em - .stories - list-style: none - min-height: 2rem - margin: 0 - padding: 0 0 0px 0 - z-index: 500 - overflow-y: auto - overflow-x: hidden - &.closed - display: none +.op-backlogs-story--drag_handle_button + padding: var(--base-size-4) - .error.icon.icon-bug - text-align: left - .stories:not(.prevent_drag) .story - cursor: move - .stories .story - display: block - font-size: 0.9rem - margin: 0 - overflow: hidden - position: relative - width: 100% - &.odd - background-color: var(--bgColor-neutral-muted) - &.even - background-color: var(--body-background) - &.error.icon-bug - background: none - text-align: center - &:before - position: absolute - color: red - pointer-events: none - &.hover, &:hover - background-color: var(--highlight-neutral-bgColor) - &.closed - text-decoration: line-through - .id - float: left - margin-left: 1em - margin-right: 1em - padding: 5px 2px 4px 2px - width: 4em - text-align: right - white-space: nowrap - .type_id .t - float: left - padding: 5px 2px 4px 2px - text-align: right - white-space: nowrap - .subject - overflow: hidden - margin-left: 4em - padding: 5px 2px 4px 2px - white-space: nowrap - min-height: 1em - .status_id - float: right - padding: 5px 2px 4px 2px - margin-left: 1em - width: 68px - .story_points - float: right - padding: 5px 1rem 4px 2px - width: 3.5rem - min-height: 14px - height: 2rem - text-align: center - .type_id .v, .id .v, .status_id .v, .version_id, .higher_item_id - display: none +.op-backlogs-story--points + margin-left: var(--stack-gap-normal) + font-variant-numeric: tabular-nums -.rb_dialog - .burndown_chart - margin-top: 20px - margin-bottom: 20px - margin-left: 20px - #charts - h3 - border: 0px - overflow: hidden - fieldset.burndown_control - padding-left: 10px - border: none - .axislabel - font-weight: var(--base-text-weight-bold) +.op-backlogs-story--menu + margin-left: var(--stack-gap-normal) -/* In-place Sprint Editor */ +.op-backlogs-story--subject + align-self: start // Align to top of second row + word-wrap: break-word + overflow-wrap: break-word -#rb #backlogs_container - .sprint.editing - .editors, > .editor - display: block - label, > * - display: none - + - .velocity, .add_new_story - display: none - .backlog .sprint.editing - .editors - display: flex - align-items: center - flex-direction: row-reverse +.op-backlogs-page + display: block + container-name: backlogsListsContainer + container-type: inline-size + +.op-backlogs-container + display: flex + flex-direction: row + gap: var(--stack-gap-normal) + +.op-backlogs-lists + display: flex + flex-direction: column + gap: var(--stack-gap-normal) + flex: 1 1 100% + overflow: hidden + +// Note: Using hardcoded values because Sass doesn't interpolate variables in +// @container query conditions. +// Note: 655px is between $breakpoint-sm and $breakpoint-md. This was found to +// be a sensible value after initial testing with different viewports. +@container backlogsListsContainer (min-width: 655px) + .op-backlogs-header-form + .FormControl-spacingWrapper + flex-direction: row + column-gap: 0.5rem - .editor - font-size: 0.9rem - line-height: 1.5rem - height: 30px - margin: 0 - padding: 0 + & > :first-child + flex: 1 1 auto + min-width: 33% - &.name - flex-basis: 15em - &.start_date, - &.effective_date - margin-left: 0.5em - flex-basis: 12.5em +@container backlogsListsContainer (max-width: 654px) + .op-backlogs-header + grid-template-columns: 1fr minmax($op-backlogs-header--points-min-width-narrow, max-content) auto - .stories .story.editing - > - *, .editors label + .op-backlogs-collapsible + flex-direction: column + align-items: flex-start + + &--description + [data-collapsed] & display: none - .editors - display: block - select, input - display: inline-block - float: none - margin: 5px 3px 4px 2px - font-size: 0.8rem - // reset the line-height (foundation sets it to "normal" but that does not work here) - line-height: inherit - .type_id.editor - width: 15% - /* sets max-width for IE */ - max-width: 140px - /* for the cool guys */ - .subject.editor - width: 55% - .status_id.editor - width: 15% - float: right - .story_points.editor - float: right - width: 10% -.backlog - font-size: 0.9rem + .op-backlogs-points-label + display: none + + .op-backlogs-story + grid-template-columns: var(--control-xsmall-size) 1fr minmax($op-backlogs-header--points-min-width-narrow, max-content) auto + + .op-backlogs-container + flex-direction: column diff --git a/frontend/src/global_styles/primer/_overrides.sass b/frontend/src/global_styles/primer/_overrides.sass index 5dec0007aaa4..0ca650eb6261 100644 --- a/frontend/src/global_styles/primer/_overrides.sass +++ b/frontend/src/global_styles/primer/_overrides.sass @@ -152,3 +152,17 @@ ul.SegmentedControl, .ActionListContent[aria-disabled="true"] .ActionListItem-label[class^="__hl_"], .ActionListItem-label[class*=" __hl_"] color: var(--control-fgColor-disabled) !important + +.Box-row--focus-gray + &:focus-visible + background-color: var(--bgColor-muted) + +.Box-row--focus-blue + &:focus-visible + background-color: var(--bgColor-accent-muted) + +.Box-row--clickable + cursor: pointer + +.Box-row:is(.Box-row--draggable) + padding-left: 0 diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts index a551ee1768e3..239409703ef3 100644 --- a/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/backlogs.controller.ts @@ -1,24 +1,105 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + import { Controller } from '@hotwired/stimulus'; +import { FrameElement, TurboVisitEvent } from '@hotwired/turbo'; +import { HalEventsService } from 'core-app/features/hal/services/hal-events.service'; +import { filter, Subscription } from 'rxjs'; +import StoryController from './backlogs/story.controller'; + +export default class BacklogsController extends Controller { + static outlets = ['backlogs--story']; + declare backlogsStoryOutlets:StoryController[]; + + static values = { + listUrl: String, + }; + + declare listUrlValue:string; + + private abortController:AbortController|null = null; + private service:HalEventsService|null = null; + private subscription:Subscription|null = null; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async connect() { + this.abortController = new AbortController(); + const { signal } = this.abortController; + + document.addEventListener('turbo:visit', this.updateSelection, { signal }); + + const { services: { halEvents } } = await window.OpenProject.getPluginContext(); + + this.service = halEvents; + this.subscription = this.service.aggregated$('WorkPackage') + .pipe(filter((events) => events.some((event) => event.eventType === 'updated'))) + .subscribe(() => { this.refreshList(); }); + } + + disconnect() { + this.subscription?.unsubscribe(); + this.subscription = null; + this.service = null; + + this.abortController?.abort(); + this.abortController = null; + } + + backlogsStoryOutletConnected(outlet:StoryController) { + const selectedId = this.getSelectedIdFromPathname(window.location.pathname); + if (selectedId !== null && outlet.idValue === selectedId) { + outlet.markAsSelected(); + } + } + + private updateSelection = (event:TurboVisitEvent) => { + const url = new URL(event.detail.url, window.location.origin); + const selectedId = this.getSelectedIdFromPathname(url.pathname); + + this.backlogsStoryOutlets.forEach((story) => { + if (selectedId !== null && story.idValue === selectedId) { + story.markAsSelected(event); + } else { + story.unmarkAsSelected(event); + } + }); + }; + + private getSelectedIdFromPathname(pathname:string):number|null { + const match = /\/details\/(\d+)/.exec(pathname); + return match ? Number(match[1]) : null; + } + + private refreshList() { + this.listElement.src = this.listUrlValue; + } -import 'jquery.flot'; -import 'jquery.flot/excanvas'; - -import 'core-vendor/jquery.jeditable.mini'; -import 'core-vendor/jquery.colorcontrast'; - -import './backlogs/common'; -import './backlogs/master_backlog'; -import './backlogs/backlog'; -import './backlogs/burndown'; -import './backlogs/model'; -import './backlogs/editable_inplace'; -import './backlogs/sprint'; -import './backlogs/work_package'; -import './backlogs/story'; -import './backlogs/task'; -import './backlogs/impediment'; -import './backlogs/taskboard'; -import './backlogs/show_main'; - -export default class BacklogsController extends Controller { + private get listElement() { + return this.element.querySelector('#backlogs_container')!; + } } diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts deleted file mode 100644 index 91cb0937b8c4..000000000000 --- a/frontend/src/stimulus/controllers/dynamic/backlogs/backlog.ts +++ /dev/null @@ -1,182 +0,0 @@ -//-- copyright -// OpenProject is an open source project management software. -// Copyright (C) the OpenProject GmbH -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See COPYRIGHT and LICENSE files for more details. -//++ - -/****************************************** - BACKLOG - A backlog is a visual representation of - a sprint and its stories. It is not a - sprint. Imagine it this way: A sprint is - a start and end date, and a set of - objectives. A backlog is something you - would draw up on the board or a spread- - sheet (or in Redmine Backlogs!) to - visualize the sprint. -******************************************/ - -// @ts-expect-error TS(2304): Cannot find name 'RB'. -RB.Backlog = (function ($) { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - return RB.Object.create({ - - initialize(el:any) { - this.$ = $(el); - this.el = el; - - // Associate this object with the element for later retrieval - this.$.data('this', this); - - // Make the list sortable - this.getList().sortable({ - connectWith: '.stories', - dropOnEmpty: true, - start: this.dragStart, - stop: this.dragStop, - update: this.dragComplete, - receive: this.dragChanged, - remove: this.dragChanged, - containment: $('#backlogs_container'), - cancel: 'input, textarea, button, select, option, .prevent_drag', - scroll: true, - helper(event:any, ui:any) { - const $clone = $(ui).clone(); - $clone.css('position', 'absolute'); - return $clone.get(0); - }, - }); - - // Observe menu items - this.$.find('.add_new_story').click(this.handleNewStoryClick); - - if (this.isSprintBacklog()) { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - RB.Factory.initialize(RB.Sprint, this.getSprint()); - // @ts-expect-error TS(2304): Cannot find name 'RB'. - this.burndown = RB.Factory.initialize(RB.Burndown, this.$.find('.show_burndown_chart')); - this.burndown.setSprintId(this.getSprint().data('this').getID()); - } - - // Initialize each item in the backlog - this.getStories().each(function (this:any, index:any) { - // 'this' refers to an element with class="story" - // @ts-expect-error TS(2304): Cannot find name 'RB'. - RB.Factory.initialize(RB.Story, this); - }); - - if (this.isSprintBacklog()) { - this.refresh(); - } - }, - - dragChanged(e:any, ui:any) { - $(this).parents('.backlog').data('this').refresh(); - }, - - dragComplete(e:any, ui:any) { - const isDropTarget = (ui.sender === null || ui.sender === undefined); - - // jQuery triggers dragComplete of source and target. - // Thus we have to check here. Otherwise, the story - // would be saved twice. - if (isDropTarget) { - ui.item.data('this').saveDragResult(); - } - }, - - dragStart(e:any, ui:any) { - ui.item.addClass('dragging'); - }, - - dragStop(e:any, ui:any) { - ui.item.removeClass('dragging'); - }, - - getSprint() { - return $(this.el).find('.model.sprint').first(); - }, - - getStories() { - return this.getList().children('.story'); - }, - - getList() { - return this.$.children('.stories').first(); - }, - - handleNewStoryClick(e:any) { - const toggler = $(this).parents('.header').find('.toggler'); - if (toggler.hasClass('closed')) { - toggler.click(); - } - e.preventDefault(); - $(this).parents('.backlog').data('this').newStory(); - }, - - // return true if backlog has an element with class="sprint" - isSprintBacklog() { - return $(this.el).find('.sprint').length === 1; - }, - - newStory() { - let story; - let o; - - story = $('#story_template').children().first().clone(); - this.getList().prepend(story); - - // @ts-expect-error TS(2304): Cannot find name 'RB'. - o = RB.Factory.initialize(RB.Story, story[0]); - o.edit(); - - story.find('.editor').first().focus(); - }, - - refresh() { - this.recalcVelocity(); - this.recalcOddity(); - }, - - recalcVelocity() { - let total:any; - - if (!this.isSprintBacklog()) { - return; - } - - total = 0; - this.getStories().each(function (this:any, index:any) { - total += $(this).data('this').getPoints(); - }); - this.$.children('.header').children('.velocity').text(total); - }, - - recalcOddity() { - this.$.find('.story:even').removeClass('odd').addClass('even'); - this.$.find('.story:odd').removeClass('even').addClass('odd'); - }, - }); -}(jQuery)); diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts deleted file mode 100644 index b6942b866767..000000000000 --- a/frontend/src/stimulus/controllers/dynamic/backlogs/burndown.ts +++ /dev/null @@ -1,67 +0,0 @@ -//-- copyright -// OpenProject is an open source project management software. -// Copyright (C) the OpenProject GmbH -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See COPYRIGHT and LICENSE files for more details. -//++ - -// @ts-expect-error TS(2304): Cannot find name 'RB'. -RB.Burndown = (function ($) { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - return RB.Object.create({ - - initialize(el:any) { - this.$ = $(el); - this.el = el; - - // Associate this object with the element for later retrieval - this.$.data('this', this); - - // Observe menu items - this.$.click(this.show); - }, - - setSprintId(sprintId:any) { - this.sprintId = sprintId; - }, - - getSprintId() { - return this.sprintId; - }, - - show(e:any) { - e.preventDefault(); - - if ($('#charts').length === 0) { - $('
').appendTo('body'); - } - // @ts-expect-error TS(2304): Cannot find name 'RB'. - $('#charts').html(`
${RB.i18n.generating_graph}
`); - - // @ts-expect-error TS(2304): Cannot find name 'RB'. - const url = RB.urlFor('show_burndown_chart', { sprint_id: $(this).data('this').sprintId, project_id: RB.constants.project_id }); - window.open(url); - }, - }); -}(jQuery)); diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts deleted file mode 100644 index 9f055c6bcfcf..000000000000 --- a/frontend/src/stimulus/controllers/dynamic/backlogs/master_backlog.ts +++ /dev/null @@ -1,42 +0,0 @@ -//-- copyright -// OpenProject is an open source project management software. -// Copyright (C) the OpenProject GmbH -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See COPYRIGHT and LICENSE files for more details. -//++ - -// Initialize the backlogs after DOM is loaded -jQuery(($) => { - // Initialize each backlog - $('.backlog').each(function (index) { - // 'this' refers to an element with class="backlog" - // @ts-expect-error TS(2304): Cannot find name 'RB'. - RB.Factory.initialize(RB.Backlog, this); - }); - - $('.backlog .toggler').on('click', function () { - $(this).toggleClass('closed icon-arrow-up1 icon-arrow-down1'); - $(this).parents('.backlog').find('ul.stories').toggleClass('closed'); - }); -}); diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts new file mode 100644 index 000000000000..4a6f76bfcb3d --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/backlogs/story.controller.ts @@ -0,0 +1,164 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { Controller } from '@hotwired/stimulus'; +import * as Turbo from '@hotwired/turbo'; + +export default class StoryController extends Controller implements EventListenerObject { + static values = { + id: Number, + splitUrl: String, + fullUrl: String, + }; + + declare idValue:number; + declare splitUrlValue:string; + declare fullUrlValue:string; + + static classes = ['selected']; + declare readonly selectedClass:string; + + private abortController:AbortController|null = null; + private clickTimeout:number|null = null; + + connect():void { + this.abortController = new AbortController(); + const { signal } = this.abortController; + + this.element.addEventListener('click', this, { signal }); + this.element.addEventListener('dblclick', this, { signal }); + this.element.addEventListener('keydown', this, { signal }); + } + + disconnect():void { + this.abortController?.abort(); + this.abortController = null; + + if (this.clickTimeout !== null) { + clearTimeout(this.clickTimeout); + this.clickTimeout = null; + } + } + + markAsSelected(_event?:Event) { + this.element.classList.add(this.selectedClass); + this.element.setAttribute('aria-current', 'true'); + } + + unmarkAsSelected(_event?:Event) { + this.element.classList.remove(this.selectedClass); + this.element.removeAttribute('aria-current'); + } + + handleEvent(event:Event):void { + switch (event.type) { + case 'click': + this.onClick(event as MouseEvent); + break; + case 'dblclick': + this.onDblClick(event as MouseEvent); + break; + case 'keydown': + this.onKeydown(event as KeyboardEvent); + break; + } + } + + private onClick(event:MouseEvent):void { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + + if ( + target.closest('a') || + target.closest('button') || + target.closest('[data-drag-handle]') + ) { + return; + } + + if (this.clickTimeout !== null) return; + + this.clickTimeout = window.setTimeout(() => { + this.clickTimeout = null; + this.openSplitPane(); + }, 250); + } + + private onDblClick(event:MouseEvent):void { + const target = event.target; + if (!(target instanceof HTMLElement)) return; + + if ( + target.closest('a') || + target.closest('button') || + target.closest('[data-drag-handle]') + ) { + return; + } + + if (this.clickTimeout !== null) { + clearTimeout(this.clickTimeout); + this.clickTimeout = null; + } + + this.openFullPane(); + } + + private onKeydown(event:KeyboardEvent):void { + if (event.key !== 'Enter') return; + + const target = event.target; + if (!(target instanceof HTMLElement)) return; + + if ( + target.closest('a') || + target.closest('button') || + target.closest('input') || + target.closest('textarea') || + target.closest('select') || + target.closest("[contenteditable='true']") + ) { + return; + } + + event.preventDefault(); + if (event.shiftKey) { + this.openFullPane(); + } else { + this.openSplitPane(); + } + } + + private openSplitPane():void { + Turbo.visit(this.splitUrlValue, { frame: 'content-bodyRight', action: 'advance' }); + } + + private openFullPane():void { + Turbo.visit(this.fullUrlValue, { frame: '_top' }); + } +} diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts deleted file mode 100644 index a9f945adacf6..000000000000 --- a/frontend/src/stimulus/controllers/dynamic/backlogs/story.ts +++ /dev/null @@ -1,145 +0,0 @@ -//-- copyright -// OpenProject is an open source project management software. -// Copyright (C) the OpenProject GmbH -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See COPYRIGHT and LICENSE files for more details. -//++ - -import { FetchResponse } from '@rails/request.js'; - -/************************************** - STORY -***************************************/ -// @ts-expect-error TS(2304): Cannot find name 'RB'. -RB.Story = (function ($) { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - return RB.Object.create(RB.WorkPackage, RB.EditableInplace, { - initialize(el:any) { - this.$ = $(el); - this.el = el; - - // Associate this object with the element for later retrieval - this.$.data('this', this); - this.$.on('click', '.editable', this.handleClick); - }, - - /** - * Callbacks from model.js - **/ - beforeSave() { - this.refreshStory(); - }, - - afterCreate(data:string, response:FetchResponse) { - this.refreshStory(); - }, - - afterUpdate(data:string, response:FetchResponse) { - this.refreshStory(); - }, - - refreshed() { - this.refreshStory(); - }, - /**/ - - editDialogTitle() { - return `Story #${this.getID()}`; - }, - - editorDisplayed(editor:any) { }, - - getPoints() { - const points = parseInt(this.$.find('.story_points').first().text(), 10); - return isNaN(points) ? 0 : points; - }, - - getType() { - return 'Story'; - }, - - markIfClosed() { - // Do nothing - }, - - newDialogTitle() { - return 'New Story'; - }, - - refreshStory() { - this.recalcVelocity(); - }, - - recalcVelocity() { - this.$.parents('.backlog').first().data('this').refresh(); - }, - - saveDirectives() { - let url; - let prev; - let sprintId; - - let data; - let method; - - prev = this.$.prev(); - sprintId = this.$.parents('.backlog').data('this').isSprintBacklog() - ? this.$.parents('.backlog').data('this').getSprint().data('this') -.getID() - : ''; - - data = `prev=${ - prev.length === 1 ? prev.data('this').getID() : '' - }&version_id=${sprintId}`; - - if (this.$.find('.editor').length > 0) { - data += `&${this.$.find('.editor').serialize()}`; - } - - //TODO: this might be unsave in case the parent of this story is not the - // sprint backlog, then we dont have a sprintId an cannot generate a - // valid url - one option might be to take RB.constants.sprint_id - // hoping it exists - if (this.isNew()) { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - url = RB.urlFor('create_story', { sprint_id: sprintId }); - method = 'post'; - } else { - // @ts-expect-error TS(2304): Cannot find name 'RB'. - url = RB.urlFor('update_story', { id: this.getID(), sprint_id: sprintId }); - method = 'put'; - } - - return { - url, - method, - data, - }; - }, - - beforeSaveDragResult() { - // Do nothing - }, - }); -}(jQuery)); diff --git a/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts b/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts new file mode 100644 index 000000000000..3e3beebea8ed --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/backlogs/taskboard-legacy.controller.ts @@ -0,0 +1,17 @@ +import { Controller } from '@hotwired/stimulus'; + +import 'core-vendor/jquery.jeditable.mini'; +import 'core-vendor/jquery.colorcontrast'; + +import './common'; +import './model'; +import './editable_inplace'; +import './sprint'; +import './work_package'; +import './task'; +import './impediment'; +import './taskboard'; +import './show_main'; + +export default class TaskboardLegacyController extends Controller { +} diff --git a/frontend/src/vendor/jquery.flot/LICENSE b/frontend/src/vendor/jquery.flot/LICENSE deleted file mode 100644 index 07d5b2094d15..000000000000 --- a/frontend/src/vendor/jquery.flot/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2007-2009 IOLA and Ole Laursen - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. diff --git a/frontend/src/vendor/jquery.flot/excanvas.js b/frontend/src/vendor/jquery.flot/excanvas.js deleted file mode 100644 index c40d6f7014d8..000000000000 --- a/frontend/src/vendor/jquery.flot/excanvas.js +++ /dev/null @@ -1,1427 +0,0 @@ -// Copyright 2006 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - - -// Known Issues: -// -// * Patterns only support repeat. -// * Radial gradient are not implemented. The VML version of these look very -// different from the canvas one. -// * Clipping paths are not implemented. -// * Coordsize. The width and height attribute have higher priority than the -// width and height style values which isn't correct. -// * Painting mode isn't implemented. -// * Canvas width/height should is using content-box by default. IE in -// Quirks mode will draw the canvas using border-box. Either change your -// doctype to HTML5 -// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) -// or use Box Sizing Behavior from WebFX -// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) -// * Non uniform scaling does not correctly scale strokes. -// * Filling very large shapes (above 5000 points) is buggy. -// * Optimize. There is always room for speed improvements. - -// Only add this code if we do not already have a canvas implementation -if (!document.createElement('canvas').getContext) { - -(function() { - - // alias some functions to make (compiled) code shorter - var m = Math; - var mr = m.round; - var ms = m.sin; - var mc = m.cos; - var abs = m.abs; - var sqrt = m.sqrt; - - // this is used for sub pixel precision - var Z = 10; - var Z2 = Z / 2; - - /** - * This funtion is assigned to the elements as element.getContext(). - * @this {HTMLElement} - * @return {CanvasRenderingContext2D_} - */ - function getContext() { - return this.context_ || - (this.context_ = new CanvasRenderingContext2D_(this)); - } - - var slice = Array.prototype.slice; - - /** - * Binds a function to an object. The returned function will always use the - * passed in {@code obj} as {@code this}. - * - * Example: - * - * g = bind(f, obj, a, b) - * g(c, d) // will do f.call(obj, a, b, c, d) - * - * @param {Function} f The function to bind the object to - * @param {Object} obj The object that should act as this when the function - * is called - * @param {*} var_args Rest arguments that will be used as the initial - * arguments when the function is called - * @return {Function} A new function that has bound this - */ - function bind(f, obj, var_args) { - var a = slice.call(arguments, 2); - return function() { - return f.apply(obj, a.concat(slice.call(arguments))); - }; - } - - function encodeHtmlAttribute(s) { - return String(s).replace(/&/g, '&').replace(/"/g, '"'); - } - - function addNamespacesAndStylesheet(doc) { - // create xmlns - if (!doc.namespaces['g_vml_']) { - doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml', - '#default#VML'); - - } - if (!doc.namespaces['g_o_']) { - doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office', - '#default#VML'); - } - - // Setup default CSS. Only add one style sheet per document - if (!doc.styleSheets['ex_canvas_']) { - var ss = doc.createStyleSheet(); - ss.owningElement.id = 'ex_canvas_'; - ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + - // default size is 300x150 in Gecko and Opera - 'text-align:left;width:300px;height:150px}'; - } - } - - // Add namespaces and stylesheet at startup. - addNamespacesAndStylesheet(document); - - var G_vmlCanvasManager_ = { - init: function(opt_doc) { - if (/MSIE/.test(navigator.userAgent) && !window.opera) { - var doc = opt_doc || document; - // Create a dummy element so that IE will allow canvas elements to be - // recognized. - doc.createElement('canvas'); - doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); - } - }, - - init_: function(doc) { - // find all canvas elements - var els = doc.getElementsByTagName('canvas'); - for (var i = 0; i < els.length; i++) { - this.initElement(els[i]); - } - }, - - /** - * Public initializes a canvas element so that it can be used as canvas - * element from now on. This is called automatically before the page is - * loaded but if you are creating elements using createElement you need to - * make sure this is called on the element. - * @param {HTMLElement} el The canvas element to initialize. - * @return {HTMLElement} the element that was created. - */ - initElement: function(el) { - if (!el.getContext) { - el.getContext = getContext; - - // Add namespaces and stylesheet to document of the element. - addNamespacesAndStylesheet(el.ownerDocument); - - // Remove fallback content. There is no way to hide text nodes so we - // just remove all childNodes. We could hide all elements and remove - // text nodes but who really cares about the fallback content. - el.innerHTML = ''; - - // do not use inline function because that will leak memory - el.attachEvent('onpropertychange', onPropertyChange); - el.attachEvent('onresize', onResize); - - var attrs = el.attributes; - if (attrs.width && attrs.width.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setWidth_(attrs.width.nodeValue); - el.style.width = attrs.width.nodeValue + 'px'; - } else { - el.width = el.clientWidth; - } - if (attrs.height && attrs.height.specified) { - // TODO: use runtimeStyle and coordsize - // el.getContext().setHeight_(attrs.height.nodeValue); - el.style.height = attrs.height.nodeValue + 'px'; - } else { - el.height = el.clientHeight; - } - //el.getContext().setCoordsize_() - } - return el; - } - }; - - function onPropertyChange(e) { - var el = e.srcElement; - - switch (e.propertyName) { - case 'width': - el.getContext().clearRect(); - el.style.width = el.attributes.width.nodeValue + 'px'; - // In IE8 this does not trigger onresize. - el.firstChild.style.width = el.clientWidth + 'px'; - break; - case 'height': - el.getContext().clearRect(); - el.style.height = el.attributes.height.nodeValue + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - break; - } - } - - function onResize(e) { - var el = e.srcElement; - if (el.firstChild) { - el.firstChild.style.width = el.clientWidth + 'px'; - el.firstChild.style.height = el.clientHeight + 'px'; - } - } - - G_vmlCanvasManager_.init(); - - // precompute "00" to "FF" - var decToHex = []; - for (var i = 0; i < 16; i++) { - for (var j = 0; j < 16; j++) { - decToHex[i * 16 + j] = i.toString(16) + j.toString(16); - } - } - - function createMatrixIdentity() { - return [ - [1, 0, 0], - [0, 1, 0], - [0, 0, 1] - ]; - } - - function matrixMultiply(m1, m2) { - var result = createMatrixIdentity(); - - for (var x = 0; x < 3; x++) { - for (var y = 0; y < 3; y++) { - var sum = 0; - - for (var z = 0; z < 3; z++) { - sum += m1[x][z] * m2[z][y]; - } - - result[x][y] = sum; - } - } - return result; - } - - function copyState(o1, o2) { - o2.fillStyle = o1.fillStyle; - o2.lineCap = o1.lineCap; - o2.lineJoin = o1.lineJoin; - o2.lineWidth = o1.lineWidth; - o2.miterLimit = o1.miterLimit; - o2.shadowBlur = o1.shadowBlur; - o2.shadowColor = o1.shadowColor; - o2.shadowOffsetX = o1.shadowOffsetX; - o2.shadowOffsetY = o1.shadowOffsetY; - o2.strokeStyle = o1.strokeStyle; - o2.globalAlpha = o1.globalAlpha; - o2.font = o1.font; - o2.textAlign = o1.textAlign; - o2.textBaseline = o1.textBaseline; - o2.arcScaleX_ = o1.arcScaleX_; - o2.arcScaleY_ = o1.arcScaleY_; - o2.lineScale_ = o1.lineScale_; - } - - var colorData = { - aliceblue: '#F0F8FF', - antiquewhite: '#FAEBD7', - aquamarine: '#7FFFD4', - azure: '#F0FFFF', - beige: '#F5F5DC', - bisque: '#FFE4C4', - black: '#000000', - blanchedalmond: '#FFEBCD', - blueviolet: '#8A2BE2', - brown: '#A52A2A', - burlywood: '#DEB887', - cadetblue: '#5F9EA0', - chartreuse: '#7FFF00', - chocolate: '#D2691E', - coral: '#FF7F50', - cornflowerblue: '#6495ED', - cornsilk: '#FFF8DC', - crimson: '#DC143C', - cyan: '#00FFFF', - darkblue: '#00008B', - darkcyan: '#008B8B', - darkgoldenrod: '#B8860B', - darkgray: '#A9A9A9', - darkgreen: '#006400', - darkgrey: '#A9A9A9', - darkkhaki: '#BDB76B', - darkmagenta: '#8B008B', - darkolivegreen: '#556B2F', - darkorange: '#FF8C00', - darkorchid: '#9932CC', - darkred: '#8B0000', - darksalmon: '#E9967A', - darkseagreen: '#8FBC8F', - darkslateblue: '#483D8B', - darkslategray: '#2F4F4F', - darkslategrey: '#2F4F4F', - darkturquoise: '#00CED1', - darkviolet: '#9400D3', - deeppink: '#FF1493', - deepskyblue: '#00BFFF', - dimgray: '#696969', - dimgrey: '#696969', - dodgerblue: '#1E90FF', - firebrick: '#B22222', - floralwhite: '#FFFAF0', - forestgreen: '#228B22', - gainsboro: '#DCDCDC', - ghostwhite: '#F8F8FF', - gold: '#FFD700', - goldenrod: '#DAA520', - grey: '#808080', - greenyellow: '#ADFF2F', - honeydew: '#F0FFF0', - hotpink: '#FF69B4', - indianred: '#CD5C5C', - indigo: '#4B0082', - ivory: '#FFFFF0', - khaki: '#F0E68C', - lavender: '#E6E6FA', - lavenderblush: '#FFF0F5', - lawngreen: '#7CFC00', - lemonchiffon: '#FFFACD', - lightblue: '#ADD8E6', - lightcoral: '#F08080', - lightcyan: '#E0FFFF', - lightgoldenrodyellow: '#FAFAD2', - lightgreen: '#90EE90', - lightgrey: '#D3D3D3', - lightpink: '#FFB6C1', - lightsalmon: '#FFA07A', - lightseagreen: '#20B2AA', - lightskyblue: '#87CEFA', - lightslategray: '#778899', - lightslategrey: '#778899', - lightsteelblue: '#B0C4DE', - lightyellow: '#FFFFE0', - limegreen: '#32CD32', - linen: '#FAF0E6', - magenta: '#FF00FF', - mediumaquamarine: '#66CDAA', - mediumblue: '#0000CD', - mediumorchid: '#BA55D3', - mediumpurple: '#9370DB', - mediumseagreen: '#3CB371', - mediumslateblue: '#7B68EE', - mediumspringgreen: '#00FA9A', - mediumturquoise: '#48D1CC', - mediumvioletred: '#C71585', - midnightblue: '#191970', - mintcream: '#F5FFFA', - mistyrose: '#FFE4E1', - moccasin: '#FFE4B5', - navajowhite: '#FFDEAD', - oldlace: '#FDF5E6', - olivedrab: '#6B8E23', - orange: '#FFA500', - orangered: '#FF4500', - orchid: '#DA70D6', - palegoldenrod: '#EEE8AA', - palegreen: '#98FB98', - paleturquoise: '#AFEEEE', - palevioletred: '#DB7093', - papayawhip: '#FFEFD5', - peachpuff: '#FFDAB9', - peru: '#CD853F', - pink: '#FFC0CB', - plum: '#DDA0DD', - powderblue: '#B0E0E6', - rosybrown: '#BC8F8F', - royalblue: '#4169E1', - saddlebrown: '#8B4513', - salmon: '#FA8072', - sandybrown: '#F4A460', - seagreen: '#2E8B57', - seashell: '#FFF5EE', - sienna: '#A0522D', - skyblue: '#87CEEB', - slateblue: '#6A5ACD', - slategray: '#708090', - slategrey: '#708090', - snow: '#FFFAFA', - springgreen: '#00FF7F', - steelblue: '#4682B4', - tan: '#D2B48C', - thistle: '#D8BFD8', - tomato: '#FF6347', - turquoise: '#40E0D0', - violet: '#EE82EE', - wheat: '#F5DEB3', - whitesmoke: '#F5F5F5', - yellowgreen: '#9ACD32' - }; - - - function getRgbHslContent(styleString) { - var start = styleString.indexOf('(', 3); - var end = styleString.indexOf(')', start + 1); - var parts = styleString.substring(start + 1, end).split(','); - // add alpha if needed - if (parts.length == 4 && styleString.substr(3, 1) == 'a') { - alpha = Number(parts[3]); - } else { - parts[3] = 1; - } - return parts; - } - - function percent(s) { - return parseFloat(s) / 100; - } - - function clamp(v, min, max) { - return Math.min(max, Math.max(min, v)); - } - - function hslToRgb(parts){ - var r, g, b; - h = parseFloat(parts[0]) / 360 % 360; - if (h < 0) - h++; - s = clamp(percent(parts[1]), 0, 1); - l = clamp(percent(parts[2]), 0, 1); - if (s == 0) { - r = g = b = l; // achromatic - } else { - var q = l < 0.5 ? l * (1 + s) : l + s - l * s; - var p = 2 * l - q; - r = hueToRgb(p, q, h + 1 / 3); - g = hueToRgb(p, q, h); - b = hueToRgb(p, q, h - 1 / 3); - } - - return '#' + decToHex[Math.floor(r * 255)] + - decToHex[Math.floor(g * 255)] + - decToHex[Math.floor(b * 255)]; - } - - function hueToRgb(m1, m2, h) { - if (h < 0) - h++; - if (h > 1) - h--; - - if (6 * h < 1) - return m1 + (m2 - m1) * 6 * h; - else if (2 * h < 1) - return m2; - else if (3 * h < 2) - return m1 + (m2 - m1) * (2 / 3 - h) * 6; - else - return m1; - } - - function processStyle(styleString) { - var str, alpha = 1; - - styleString = String(styleString); - if (styleString.charAt(0) == '#') { - str = styleString; - } else if (/^rgb/.test(styleString)) { - var parts = getRgbHslContent(styleString); - var str = '#', n; - for (var i = 0; i < 3; i++) { - if (parts[i].indexOf('%') != -1) { - n = Math.floor(percent(parts[i]) * 255); - } else { - n = Number(parts[i]); - } - str += decToHex[clamp(n, 0, 255)]; - } - alpha = parts[3]; - } else if (/^hsl/.test(styleString)) { - var parts = getRgbHslContent(styleString); - str = hslToRgb(parts); - alpha = parts[3]; - } else { - str = colorData[styleString] || styleString; - } - return {color: str, alpha: alpha}; - } - - var DEFAULT_STYLE = { - style: 'normal', - variant: 'normal', - weight: 'normal', - size: 10, - family: 'sans-serif' - }; - - // Internal text style cache - var fontStyleCache = {}; - - function processFontStyle(styleString) { - if (fontStyleCache[styleString]) { - return fontStyleCache[styleString]; - } - - var el = document.createElement('div'); - var style = el.style; - try { - style.font = styleString; - } catch (ex) { - // Ignore failures to set to invalid font. - } - - return fontStyleCache[styleString] = { - style: style.fontStyle || DEFAULT_STYLE.style, - variant: style.fontVariant || DEFAULT_STYLE.variant, - weight: style.fontWeight || DEFAULT_STYLE.weight, - size: style.fontSize || DEFAULT_STYLE.size, - family: style.fontFamily || DEFAULT_STYLE.family - }; - } - - function getComputedStyle(style, element) { - var computedStyle = {}; - - for (var p in style) { - computedStyle[p] = style[p]; - } - - // Compute the size - var canvasFontSize = parseFloat(element.currentStyle.fontSize), - fontSize = parseFloat(style.size); - - if (typeof style.size == 'number') { - computedStyle.size = style.size; - } else if (style.size.indexOf('px') != -1) { - computedStyle.size = fontSize; - } else if (style.size.indexOf('em') != -1) { - computedStyle.size = canvasFontSize * fontSize; - } else if(style.size.indexOf('%') != -1) { - computedStyle.size = (canvasFontSize / 100) * fontSize; - } else if (style.size.indexOf('pt') != -1) { - computedStyle.size = fontSize / .75; - } else { - computedStyle.size = canvasFontSize; - } - - // Different scaling between normal text and VML text. This was found using - // trial and error to get the same size as non VML text. - computedStyle.size *= 0.981; - - return computedStyle; - } - - function buildStyle(style) { - return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + - style.size + 'px ' + style.family; - } - - function processLineCap(lineCap) { - switch (lineCap) { - case 'butt': - return 'flat'; - case 'round': - return 'round'; - case 'square': - default: - return 'square'; - } - } - - /** - * This class implements CanvasRenderingContext2D interface as described by - * the WHATWG. - * @param {HTMLElement} surfaceElement The element that the 2D context should - * be associated with - */ - function CanvasRenderingContext2D_(surfaceElement) { - this.m_ = createMatrixIdentity(); - - this.mStack_ = []; - this.aStack_ = []; - this.currentPath_ = []; - - // Canvas context properties - this.strokeStyle = '#000'; - this.fillStyle = '#000'; - - this.lineWidth = 1; - this.lineJoin = 'miter'; - this.lineCap = 'butt'; - this.miterLimit = Z * 1; - this.globalAlpha = 1; - this.font = '10px sans-serif'; - this.textAlign = 'left'; - this.textBaseline = 'alphabetic'; - this.canvas = surfaceElement; - - var el = surfaceElement.ownerDocument.createElement('div'); - el.style.width = surfaceElement.clientWidth + 'px'; - el.style.height = surfaceElement.clientHeight + 'px'; - el.style.overflow = 'hidden'; - el.style.position = 'absolute'; - surfaceElement.appendChild(el); - - this.element_ = el; - this.arcScaleX_ = 1; - this.arcScaleY_ = 1; - this.lineScale_ = 1; - } - - var contextPrototype = CanvasRenderingContext2D_.prototype; - contextPrototype.clearRect = function() { - if (this.textMeasureEl_) { - this.textMeasureEl_.removeNode(true); - this.textMeasureEl_ = null; - } - this.element_.innerHTML = ''; - }; - - contextPrototype.beginPath = function() { - // TODO: Branch current matrix so that save/restore has no effect - // as per safari docs. - this.currentPath_ = []; - }; - - contextPrototype.moveTo = function(aX, aY) { - var p = this.getCoords_(aX, aY); - this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.lineTo = function(aX, aY) { - var p = this.getCoords_(aX, aY); - this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); - - this.currentX_ = p.x; - this.currentY_ = p.y; - }; - - contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, - aCP2x, aCP2y, - aX, aY) { - var p = this.getCoords_(aX, aY); - var cp1 = this.getCoords_(aCP1x, aCP1y); - var cp2 = this.getCoords_(aCP2x, aCP2y); - bezierCurveTo(this, cp1, cp2, p); - }; - - // Helper function that takes the already fixed cordinates. - function bezierCurveTo(self, cp1, cp2, p) { - self.currentPath_.push({ - type: 'bezierCurveTo', - cp1x: cp1.x, - cp1y: cp1.y, - cp2x: cp2.x, - cp2y: cp2.y, - x: p.x, - y: p.y - }); - self.currentX_ = p.x; - self.currentY_ = p.y; - } - - contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { - // the following is lifted almost directly from - // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes - - var cp = this.getCoords_(aCPx, aCPy); - var p = this.getCoords_(aX, aY); - - var cp1 = { - x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), - y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) - }; - var cp2 = { - x: cp1.x + (p.x - this.currentX_) / 3.0, - y: cp1.y + (p.y - this.currentY_) / 3.0 - }; - - bezierCurveTo(this, cp1, cp2, p); - }; - - contextPrototype.arc = function(aX, aY, aRadius, - aStartAngle, aEndAngle, aClockwise) { - aRadius *= Z; - var arcType = aClockwise ? 'at' : 'wa'; - - var xStart = aX + mc(aStartAngle) * aRadius - Z2; - var yStart = aY + ms(aStartAngle) * aRadius - Z2; - - var xEnd = aX + mc(aEndAngle) * aRadius - Z2; - var yEnd = aY + ms(aEndAngle) * aRadius - Z2; - - // IE won't render arches drawn counter clockwise if xStart == xEnd. - if (xStart == xEnd && !aClockwise) { - xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something - // that can be represented in binary - } - - var p = this.getCoords_(aX, aY); - var pStart = this.getCoords_(xStart, yStart); - var pEnd = this.getCoords_(xEnd, yEnd); - - this.currentPath_.push({type: arcType, - x: p.x, - y: p.y, - radius: aRadius, - xStart: pStart.x, - yStart: pStart.y, - xEnd: pEnd.x, - yEnd: pEnd.y}); - - }; - - contextPrototype.rect = function(aX, aY, aWidth, aHeight) { - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - }; - - contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.stroke(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { - var oldPath = this.currentPath_; - this.beginPath(); - - this.moveTo(aX, aY); - this.lineTo(aX + aWidth, aY); - this.lineTo(aX + aWidth, aY + aHeight); - this.lineTo(aX, aY + aHeight); - this.closePath(); - this.fill(); - - this.currentPath_ = oldPath; - }; - - contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { - var gradient = new CanvasGradient_('gradient'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - return gradient; - }; - - contextPrototype.createRadialGradient = function(aX0, aY0, aR0, - aX1, aY1, aR1) { - var gradient = new CanvasGradient_('gradientradial'); - gradient.x0_ = aX0; - gradient.y0_ = aY0; - gradient.r0_ = aR0; - gradient.x1_ = aX1; - gradient.y1_ = aY1; - gradient.r1_ = aR1; - return gradient; - }; - - contextPrototype.drawImage = function(image, var_args) { - var dx, dy, dw, dh, sx, sy, sw, sh; - - // to find the original width we overide the width and height - var oldRuntimeWidth = image.runtimeStyle.width; - var oldRuntimeHeight = image.runtimeStyle.height; - image.runtimeStyle.width = 'auto'; - image.runtimeStyle.height = 'auto'; - - // get the original size - var w = image.width; - var h = image.height; - - // and remove overides - image.runtimeStyle.width = oldRuntimeWidth; - image.runtimeStyle.height = oldRuntimeHeight; - - if (arguments.length == 3) { - dx = arguments[1]; - dy = arguments[2]; - sx = sy = 0; - sw = dw = w; - sh = dh = h; - } else if (arguments.length == 5) { - dx = arguments[1]; - dy = arguments[2]; - dw = arguments[3]; - dh = arguments[4]; - sx = sy = 0; - sw = w; - sh = h; - } else if (arguments.length == 9) { - sx = arguments[1]; - sy = arguments[2]; - sw = arguments[3]; - sh = arguments[4]; - dx = arguments[5]; - dy = arguments[6]; - dw = arguments[7]; - dh = arguments[8]; - } else { - throw Error('Invalid number of arguments'); - } - - var d = this.getCoords_(dx, dy); - - var w2 = sw / 2; - var h2 = sh / 2; - - var vmlStr = []; - - var W = 10; - var H = 10; - - // For some reason that I've now forgotten, using divs didn't work - vmlStr.push(' ' , - '', - ''); - - this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join('')); - }; - - contextPrototype.stroke = function(aFill) { - var W = 10; - var H = 10; - // Divide the shape into chunks if it's too long because IE has a limit - // somewhere for how long a VML shape can be. This simple division does - // not work with fills, only strokes, unfortunately. - var chunkSize = 5000; - - var min = {x: null, y: null}; - var max = {x: null, y: null}; - - for (var j = 0; j < this.currentPath_.length; j += chunkSize) { - var lineStr = []; - var lineOpen = false; - - lineStr.push(''); - - if (!aFill) { - appendStroke(this, lineStr); - } else { - appendFill(this, lineStr, min, max); - } - - lineStr.push(''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - } - }; - - function appendStroke(ctx, lineStr) { - var a = processStyle(ctx.strokeStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - var lineWidth = ctx.lineScale_ * ctx.lineWidth; - - // VML cannot correctly render a line if the width is less than 1px. - // In that case, we dilute the color to make the line look thinner. - if (lineWidth < 1) { - opacity *= lineWidth; - } - - lineStr.push( - '' - ); - } - - function appendFill(ctx, lineStr, min, max) { - var fillStyle = ctx.fillStyle; - var arcScaleX = ctx.arcScaleX_; - var arcScaleY = ctx.arcScaleY_; - var width = max.x - min.x; - var height = max.y - min.y; - if (fillStyle instanceof CanvasGradient_) { - // TODO: Gradients transformed with the transformation matrix. - var angle = 0; - var focus = {x: 0, y: 0}; - - // additional offset - var shift = 0; - // scale factor for offset - var expansion = 1; - - if (fillStyle.type_ == 'gradient') { - var x0 = fillStyle.x0_ / arcScaleX; - var y0 = fillStyle.y0_ / arcScaleY; - var x1 = fillStyle.x1_ / arcScaleX; - var y1 = fillStyle.y1_ / arcScaleY; - var p0 = ctx.getCoords_(x0, y0); - var p1 = ctx.getCoords_(x1, y1); - var dx = p1.x - p0.x; - var dy = p1.y - p0.y; - angle = Math.atan2(dx, dy) * 180 / Math.PI; - - // The angle should be a non-negative number. - if (angle < 0) { - angle += 360; - } - - // Very small angles produce an unexpected result because they are - // converted to a scientific notation string. - if (angle < 1e-6) { - angle = 0; - } - } else { - var p0 = ctx.getCoords_(fillStyle.x0_, fillStyle.y0_); - focus = { - x: (p0.x - min.x) / width, - y: (p0.y - min.y) / height - }; - - width /= arcScaleX * Z; - height /= arcScaleY * Z; - var dimension = m.max(width, height); - shift = 2 * fillStyle.r0_ / dimension; - expansion = 2 * fillStyle.r1_ / dimension - shift; - } - - // We need to sort the color stops in ascending order by offset, - // otherwise IE won't interpret it correctly. - var stops = fillStyle.colors_; - stops.sort(function(cs1, cs2) { - return cs1.offset - cs2.offset; - }); - - var length = stops.length; - var color1 = stops[0].color; - var color2 = stops[length - 1].color; - var opacity1 = stops[0].alpha * ctx.globalAlpha; - var opacity2 = stops[length - 1].alpha * ctx.globalAlpha; - - var colors = []; - for (var i = 0; i < length; i++) { - var stop = stops[i]; - colors.push(stop.offset * expansion + shift + ' ' + stop.color); - } - - // When colors attribute is used, the meanings of opacity and o:opacity2 - // are reversed. - lineStr.push(''); - } else if (fillStyle instanceof CanvasPattern_) { - if (width && height) { - var deltaLeft = -min.x; - var deltaTop = -min.y; - lineStr.push(''); - } - } else { - var a = processStyle(ctx.fillStyle); - var color = a.color; - var opacity = a.alpha * ctx.globalAlpha; - lineStr.push(''); - } - } - - contextPrototype.fill = function() { - this.stroke(true); - }; - - contextPrototype.closePath = function() { - this.currentPath_.push({type: 'close'}); - }; - - /** - * @private - */ - contextPrototype.getCoords_ = function(aX, aY) { - var m = this.m_; - return { - x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, - y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 - }; - }; - - contextPrototype.save = function() { - var o = {}; - copyState(this, o); - this.aStack_.push(o); - this.mStack_.push(this.m_); - this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); - }; - - contextPrototype.restore = function() { - if (this.aStack_.length) { - copyState(this.aStack_.pop(), this); - this.m_ = this.mStack_.pop(); - } - }; - - function matrixIsFinite(m) { - return isFinite(m[0][0]) && isFinite(m[0][1]) && - isFinite(m[1][0]) && isFinite(m[1][1]) && - isFinite(m[2][0]) && isFinite(m[2][1]); - } - - function setM(ctx, m, updateLineScale) { - if (!matrixIsFinite(m)) { - return; - } - ctx.m_ = m; - - if (updateLineScale) { - // Get the line scale. - // Determinant of this.m_ means how much the area is enlarged by the - // transformation. So its square root can be used as a scale factor - // for width. - var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; - ctx.lineScale_ = sqrt(abs(det)); - } - } - - contextPrototype.translate = function(aX, aY) { - var m1 = [ - [1, 0, 0], - [0, 1, 0], - [aX, aY, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.rotate = function(aRot) { - var c = mc(aRot); - var s = ms(aRot); - - var m1 = [ - [c, s, 0], - [-s, c, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), false); - }; - - contextPrototype.scale = function(aX, aY) { - this.arcScaleX_ *= aX; - this.arcScaleY_ *= aY; - var m1 = [ - [aX, 0, 0], - [0, aY, 0], - [0, 0, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { - var m1 = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, matrixMultiply(m1, this.m_), true); - }; - - contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { - var m = [ - [m11, m12, 0], - [m21, m22, 0], - [dx, dy, 1] - ]; - - setM(this, m, true); - }; - - /** - * The text drawing function. - * The maxWidth argument isn't taken in account, since no browser supports - * it yet. - */ - contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) { - var m = this.m_, - delta = 1000, - left = 0, - right = delta, - offset = {x: 0, y: 0}, - lineStr = []; - - var fontStyle = getComputedStyle(processFontStyle(this.font), - this.element_); - - var fontStyleString = buildStyle(fontStyle); - - var elementStyle = this.element_.currentStyle; - var textAlign = this.textAlign.toLowerCase(); - switch (textAlign) { - case 'left': - case 'center': - case 'right': - break; - case 'end': - textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left'; - break; - case 'start': - textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left'; - break; - default: - textAlign = 'left'; - } - - // 1.75 is an arbitrary number, as there is no info about the text baseline - switch (this.textBaseline) { - case 'hanging': - case 'top': - offset.y = fontStyle.size / 1.75; - break; - case 'middle': - break; - default: - case null: - case 'alphabetic': - case 'ideographic': - case 'bottom': - offset.y = -fontStyle.size / 2.25; - break; - } - - switch(textAlign) { - case 'right': - left = delta; - right = 0.05; - break; - case 'center': - left = right = delta / 2; - break; - } - - var d = this.getCoords_(x + offset.x, y + offset.y); - - lineStr.push(''); - - if (stroke) { - appendStroke(this, lineStr); - } else { - // TODO: Fix the min and max params. - appendFill(this, lineStr, {x: -left, y: 0}, - {x: right, y: fontStyle.size}); - } - - var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + - m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0'; - - var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z); - - lineStr.push('', - '', - ''); - - this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); - }; - - contextPrototype.fillText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, false); - }; - - contextPrototype.strokeText = function(text, x, y, maxWidth) { - this.drawText_(text, x, y, maxWidth, true); - }; - - contextPrototype.measureText = function(text) { - if (!this.textMeasureEl_) { - var s = ''; - this.element_.insertAdjacentHTML('beforeEnd', s); - this.textMeasureEl_ = this.element_.lastChild; - } - var doc = this.element_.ownerDocument; - this.textMeasureEl_.innerHTML = ''; - this.textMeasureEl_.style.font = this.font; - // Don't use innerHTML or innerText because they allow markup/whitespace. - this.textMeasureEl_.appendChild(doc.createTextNode(text)); - return {width: this.textMeasureEl_.offsetWidth}; - }; - - /******** STUBS ********/ - contextPrototype.clip = function() { - // TODO: Implement - }; - - contextPrototype.arcTo = function() { - // TODO: Implement - }; - - contextPrototype.createPattern = function(image, repetition) { - return new CanvasPattern_(image, repetition); - }; - - // Gradient / Pattern Stubs - function CanvasGradient_(aType) { - this.type_ = aType; - this.x0_ = 0; - this.y0_ = 0; - this.r0_ = 0; - this.x1_ = 0; - this.y1_ = 0; - this.r1_ = 0; - this.colors_ = []; - } - - CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { - aColor = processStyle(aColor); - this.colors_.push({offset: aOffset, - color: aColor.color, - alpha: aColor.alpha}); - }; - - function CanvasPattern_(image, repetition) { - assertImageIsValid(image); - switch (repetition) { - case 'repeat': - case null: - case '': - this.repetition_ = 'repeat'; - break - case 'repeat-x': - case 'repeat-y': - case 'no-repeat': - this.repetition_ = repetition; - break; - default: - throwException('SYNTAX_ERR'); - } - - this.src_ = image.src; - this.width_ = image.width; - this.height_ = image.height; - } - - function throwException(s) { - throw new DOMException_(s); - } - - function assertImageIsValid(img) { - if (!img || img.nodeType != 1 || img.tagName != 'IMG') { - throwException('TYPE_MISMATCH_ERR'); - } - if (img.readyState != 'complete') { - throwException('INVALID_STATE_ERR'); - } - } - - function DOMException_(s) { - this.code = this[s]; - this.message = s +': DOM Exception ' + this.code; - } - var p = DOMException_.prototype = new Error; - p.INDEX_SIZE_ERR = 1; - p.DOMSTRING_SIZE_ERR = 2; - p.HIERARCHY_REQUEST_ERR = 3; - p.WRONG_DOCUMENT_ERR = 4; - p.INVALID_CHARACTER_ERR = 5; - p.NO_DATA_ALLOWED_ERR = 6; - p.NO_MODIFICATION_ALLOWED_ERR = 7; - p.NOT_FOUND_ERR = 8; - p.NOT_SUPPORTED_ERR = 9; - p.INUSE_ATTRIBUTE_ERR = 10; - p.INVALID_STATE_ERR = 11; - p.SYNTAX_ERR = 12; - p.INVALID_MODIFICATION_ERR = 13; - p.NAMESPACE_ERR = 14; - p.INVALID_ACCESS_ERR = 15; - p.VALIDATION_ERR = 16; - p.TYPE_MISMATCH_ERR = 17; - - // set up externs - G_vmlCanvasManager = G_vmlCanvasManager_; - CanvasRenderingContext2D = CanvasRenderingContext2D_; - CanvasGradient = CanvasGradient_; - CanvasPattern = CanvasPattern_; - DOMException = DOMException_; -})(); - -} // if diff --git a/frontend/src/vendor/jquery.flot/excanvas.min.js b/frontend/src/vendor/jquery.flot/excanvas.min.js deleted file mode 100644 index 12c74f7bea84..000000000000 --- a/frontend/src/vendor/jquery.flot/excanvas.min.js +++ /dev/null @@ -1 +0,0 @@ -if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&").replace(/"/g,""")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AHAL.x){AL.x=Z.x}if(AG.y==null||Z.yAL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('','','');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()}; \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js b/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js deleted file mode 100644 index d3524d786f0a..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.js +++ /dev/null @@ -1,179 +0,0 @@ -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ - -(function($) { - $.color = {}; - - // construct color object with some convenient chainable helpers - $.color.make = function (r, g, b, a) { - var o = {}; - o.r = r || 0; - o.g = g || 0; - o.b = b || 0; - o.a = a != null ? a : 1; - - o.add = function (c, d) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] += d; - return o.normalize(); - }; - - o.scale = function (c, f) { - for (var i = 0; i < c.length; ++i) - o[c.charAt(i)] *= f; - return o.normalize(); - }; - - o.toString = function () { - if (o.a >= 1.0) { - return "rgb("+[o.r, o.g, o.b].join(",")+")"; - } else { - return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; - } - }; - - o.normalize = function () { - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - o.r = clamp(0, parseInt(o.r), 255); - o.g = clamp(0, parseInt(o.g), 255); - o.b = clamp(0, parseInt(o.b), 255); - o.a = clamp(0, o.a, 1); - return o; - }; - - o.clone = function () { - return $.color.make(o.r, o.b, o.g, o.a); - }; - - return o.normalize(); - } - - // extract CSS color property from element, going up in the DOM - // if it's "transparent" - $.color.extract = function (elem, css) { - var c; - do { - c = elem.css(css).toLowerCase(); - // keep going until we find an element that has color, or - // we hit the body - if (c != '' && c != 'transparent') - break; - elem = elem.parent(); - } while (!$.nodeName(elem.get(0), "body")); - - // catch Safari's way of signalling transparent - if (c == "rgba(0, 0, 0, 0)") - c = "transparent"; - - return $.color.parse(c); - } - - // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), - // returns color object, if parsing failed, you get black (0, 0, - // 0) out - $.color.parse = function (str) { - var res, m = $.color.make; - - // Look for rgb(num,num,num) - if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); - - // Look for rgba(num,num,num,num) - if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); - - // Look for rgb(num%,num%,num%) - if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); - - // Look for rgba(num%,num%,num%,num) - if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) - return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); - - // Look for #a0b1c2 - if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) - return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); - - // Look for #fff - if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) - return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); - - // Otherwise, we're most likely dealing with a named color - var name = $.trim(str).toLowerCase(); - if (name == "transparent") - return m(255, 255, 255, 0); - else { - // default to black - res = lookupColors[name] || [0, 0, 0]; - return m(res[0], res[1], res[2]); - } - } - - var lookupColors = { - aqua:[0,255,255], - azure:[240,255,255], - beige:[245,245,220], - black:[0,0,0], - blue:[0,0,255], - brown:[165,42,42], - cyan:[0,255,255], - darkblue:[0,0,139], - darkcyan:[0,139,139], - darkgrey:[169,169,169], - darkgreen:[0,100,0], - darkkhaki:[189,183,107], - darkmagenta:[139,0,139], - darkolivegreen:[85,107,47], - darkorange:[255,140,0], - darkorchid:[153,50,204], - darkred:[139,0,0], - darksalmon:[233,150,122], - darkviolet:[148,0,211], - fuchsia:[255,0,255], - gold:[255,215,0], - green:[0,128,0], - indigo:[75,0,130], - khaki:[240,230,140], - lightblue:[173,216,230], - lightcyan:[224,255,255], - lightgreen:[144,238,144], - lightgrey:[211,211,211], - lightpink:[255,182,193], - lightyellow:[255,255,224], - lime:[0,255,0], - magenta:[255,0,255], - maroon:[128,0,0], - navy:[0,0,128], - olive:[128,128,0], - orange:[255,165,0], - pink:[255,192,203], - purple:[128,0,128], - violet:[128,0,128], - red:[255,0,0], - silver:[192,192,192], - white:[255,255,255], - yellow:[255,255,0] - }; -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js b/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js deleted file mode 100644 index 7f44c57b560c..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.colorhelpers.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){b.color={};b.color.make=function(f,e,c,d){var h={};h.r=f||0;h.g=e||0;h.b=c||0;h.a=d!=null?d:1;h.add=function(k,j){for(var g=0;g=1){return"rgb("+[h.r,h.g,h.b].join(",")+")"}else{return"rgba("+[h.r,h.g,h.b,h.a].join(",")+")"}};h.normalize=function(){function g(j,k,i){return ki?i:k)}h.r=g(0,parseInt(h.r),255);h.g=g(0,parseInt(h.g),255);h.b=g(0,parseInt(h.b),255);h.a=g(0,h.a,1);return h};h.clone=function(){return b.color.make(h.r,h.b,h.g,h.a)};return h.normalize()};b.color.extract=function(e,d){var f;do{f=e.css(d).toLowerCase();if(f!=""&&f!="transparent"){break}e=e.parent()}while(!b.nodeName(e.get(0),"body"));if(f=="rgba(0, 0, 0, 0)"){f="transparent"}return b.color.parse(f)};b.color.parse=function(f){var e,c=b.color.make;if(e=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10))}if(e=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseInt(e[1],10),parseInt(e[2],10),parseInt(e[3],10),parseFloat(e[4]))}if(e=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55)}if(e=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(f)){return c(parseFloat(e[1])*2.55,parseFloat(e[2])*2.55,parseFloat(e[3])*2.55,parseFloat(e[4]))}if(e=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(f)){return c(parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16))}if(e=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(f)){return c(parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16))}var d=b.trim(f).toLowerCase();if(d=="transparent"){return c(255,255,255,0)}else{e=a[d]||[0,0,0];return c(e[0],e[1],e[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js b/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js deleted file mode 100644 index 1d433f0074d1..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.js +++ /dev/null @@ -1,167 +0,0 @@ -/* -Flot plugin for showing crosshairs, thin lines, when the mouse hovers -over the plot. - - crosshair: { - mode: null or "x" or "y" or "xy" - color: color - lineWidth: number - } - -Set the mode to one of "x", "y" or "xy". The "x" mode enables a -vertical crosshair that lets you trace the values on the x axis, "y" -enables a horizontal crosshair and "xy" enables them both. "color" is -the color of the crosshair (default is "rgba(170, 0, 0, 0.80)"), -"lineWidth" is the width of the drawn lines (default is 1). - -The plugin also adds four public methods: - - - setCrosshair(pos) - - Set the position of the crosshair. Note that this is cleared if - the user moves the mouse. "pos" is in coordinates of the plot and - should be on the form { x: xpos, y: ypos } (you can use x2/x3/... - if you're using multiple axes), which is coincidentally the same - format as what you get from a "plothover" event. If "pos" is null, - the crosshair is cleared. - - - clearCrosshair() - - Clear the crosshair. - - - lockCrosshair(pos) - - Cause the crosshair to lock to the current location, no longer - updating if the user moves the mouse. Optionally supply a position - (passed on to setCrosshair()) to move it to. - - Example usage: - var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; - $("#graph").bind("plothover", function (evt, position, item) { - if (item) { - // Lock the crosshair to the data point being hovered - myFlot.lockCrosshair({ x: item.datapoint[0], y: item.datapoint[1] }); - } - else { - // Return normal crosshair operation - myFlot.unlockCrosshair(); - } - }); - - - unlockCrosshair() - - Free the crosshair to move again after locking it. -*/ - -(function ($) { - var options = { - crosshair: { - mode: null, // one of null, "x", "y" or "xy", - color: "rgba(170, 0, 0, 0.80)", - lineWidth: 1 - } - }; - - function init(plot) { - // position of crosshair in pixels - var crosshair = { x: -1, y: -1, locked: false }; - - plot.setCrosshair = function setCrosshair(pos) { - if (!pos) - crosshair.x = -1; - else { - var o = plot.p2c(pos); - crosshair.x = Math.max(0, Math.min(o.left, plot.width())); - crosshair.y = Math.max(0, Math.min(o.top, plot.height())); - } - - plot.triggerRedrawOverlay(); - }; - - plot.clearCrosshair = plot.setCrosshair; // passes null for pos - - plot.lockCrosshair = function lockCrosshair(pos) { - if (pos) - plot.setCrosshair(pos); - crosshair.locked = true; - } - - plot.unlockCrosshair = function unlockCrosshair() { - crosshair.locked = false; - } - - function onMouseOut(e) { - if (crosshair.locked) - return; - - if (crosshair.x != -1) { - crosshair.x = -1; - plot.triggerRedrawOverlay(); - } - } - - function onMouseMove(e) { - if (crosshair.locked) - return; - - if (plot.getSelection && plot.getSelection()) { - crosshair.x = -1; // hide the crosshair while selecting - return; - } - - var offset = plot.offset(); - crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); - crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); - plot.triggerRedrawOverlay(); - } - - plot.hooks.bindEvents.push(function (plot, eventHolder) { - if (!plot.getOptions().crosshair.mode) - return; - - eventHolder.mouseout(onMouseOut); - eventHolder.mousemove(onMouseMove); - }); - - plot.hooks.drawOverlay.push(function (plot, ctx) { - var c = plot.getOptions().crosshair; - if (!c.mode) - return; - - var plotOffset = plot.getPlotOffset(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - if (crosshair.x != -1) { - ctx.strokeStyle = c.color; - ctx.lineWidth = c.lineWidth; - ctx.lineJoin = "round"; - - ctx.beginPath(); - if (c.mode.indexOf("x") != -1) { - ctx.moveTo(crosshair.x, 0); - ctx.lineTo(crosshair.x, plot.height()); - } - if (c.mode.indexOf("y") != -1) { - ctx.moveTo(0, crosshair.y); - ctx.lineTo(plot.width(), crosshair.y); - } - ctx.stroke(); - } - ctx.restore(); - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mouseout", onMouseOut); - eventHolder.unbind("mousemove", onMouseMove); - }); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'crosshair', - version: '1.0' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js deleted file mode 100644 index ccaf240366ac..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.crosshair.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){var a={crosshair:{mode:null,color:"rgba(170, 0, 0, 0.80)",lineWidth:1}};function c(h){var j={x:-1,y:-1,locked:false};h.setCrosshair=function e(l){if(!l){j.x=-1}else{var k=h.p2c(l);j.x=Math.max(0,Math.min(k.left,h.width()));j.y=Math.max(0,Math.min(k.top,h.height()))}h.triggerRedrawOverlay()};h.clearCrosshair=h.setCrosshair;h.lockCrosshair=function f(k){if(k){h.setCrosshair(k)}j.locked=true};h.unlockCrosshair=function g(){j.locked=false};function d(k){if(j.locked){return}if(j.x!=-1){j.x=-1;h.triggerRedrawOverlay()}}function i(k){if(j.locked){return}if(h.getSelection&&h.getSelection()){j.x=-1;return}var l=h.offset();j.x=Math.max(0,Math.min(k.pageX-l.left,h.width()));j.y=Math.max(0,Math.min(k.pageY-l.top,h.height()));h.triggerRedrawOverlay()}h.hooks.bindEvents.push(function(l,k){if(!l.getOptions().crosshair.mode){return}k.mouseout(d);k.mousemove(i)});h.hooks.drawOverlay.push(function(m,k){var n=m.getOptions().crosshair;if(!n.mode){return}var l=m.getPlotOffset();k.save();k.translate(l.left,l.top);if(j.x!=-1){k.strokeStyle=n.color;k.lineWidth=n.lineWidth;k.lineJoin="round";k.beginPath();if(n.mode.indexOf("x")!=-1){k.moveTo(j.x,0);k.lineTo(j.x,m.height())}if(n.mode.indexOf("y")!=-1){k.moveTo(0,j.y);k.lineTo(m.width(),j.y)}k.stroke()}k.restore()});h.hooks.shutdown.push(function(l,k){k.unbind("mouseout",d);k.unbind("mousemove",i)})}b.plot.plugins.push({init:c,options:a,name:"crosshair",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js b/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js deleted file mode 100644 index 69700e79ce66..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.js +++ /dev/null @@ -1,183 +0,0 @@ -/* -Flot plugin for computing bottoms for filled line and bar charts. - -The case: you've got two series that you want to fill the area -between. In Flot terms, you need to use one as the fill bottom of the -other. You can specify the bottom of each data point as the third -coordinate manually, or you can use this plugin to compute it for you. - -In order to name the other series, you need to give it an id, like this - - var dataset = [ - { data: [ ... ], id: "foo" } , // use default bottom - { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom - ]; - - $.plot($("#placeholder"), dataset, { line: { show: true, fill: true }}); - -As a convenience, if the id given is a number that doesn't appear as -an id in the series, it is interpreted as the index in the array -instead (so fillBetween: 0 can also mean the first series). - -Internally, the plugin modifies the datapoints in each series. For -line series, extra data points might be inserted through -interpolation. Note that at points where the bottom line is not -defined (due to a null point or start/end of line), the current line -will show a gap too. The algorithm comes from the jquery.flot.stack.js -plugin, possibly some code could be shared. -*/ - -(function ($) { - var options = { - series: { fillBetween: null } // or number - }; - - function init(plot) { - function findBottomSeries(s, allseries) { - var i; - for (i = 0; i < allseries.length; ++i) { - if (allseries[i].id == s.fillBetween) - return allseries[i]; - } - - if (typeof s.fillBetween == "number") { - i = s.fillBetween; - - if (i < 0 || i >= allseries.length) - return null; - - return allseries[i]; - } - - return null; - } - - function computeFillBottoms(plot, s, datapoints) { - if (s.fillBetween == null) - return; - - var other = findBottomSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - withbottom = ps > 2 && datapoints.format[2].y, - withsteps = withlines && s.lines.steps, - fromgap = true, - i = 0, j = 0, l; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i]; - py = points[i + 1]; - qx = otherpoints[j]; - qy = otherpoints[j + 1]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - //newpoints[l + 1] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px); - newpoints.push(qx); - newpoints.push(intery) - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + 1] - qy) * (px - qx) / (otherpoints[j - otherps] - qx); - - //newpoints[l + 1] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] = bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(computeFillBottoms); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'fillbetween', - version: '1.0' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js deleted file mode 100644 index 47f3dfb6de04..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.fillbetween.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){var a={series:{fillBetween:null}};function c(f){function d(j,h){var g;for(g=0;g=h.length){return null}return h[g]}return null}function e(B,u,g){if(u.fillBetween==null){return}var p=d(u,B.getData());if(!p){return}var y=g.pointsize,E=g.points,h=p.datapoints.pointsize,x=p.datapoints.points,r=[],w,v,k,G,F,q,t=u.lines.show,o=y>2&&g.format[2].y,n=t&&u.lines.steps,D=true,C=0,A=0,z;while(true){if(C>=E.length){break}z=r.length;if(E[C]==null){for(m=0;m=x.length){if(!t){for(m=0;mG){if(t&&C>0&&E[C-y]!=null){k=v+(E[C-y+1]-v)*(G-w)/(E[C-y]-w);r.push(G);r.push(k);for(m=2;m0&&x[A-h]!=null){q=F+(x[A-h+1]-F)*(w-G)/(x[A-h]-G)}C+=y}}D=false;if(z!=r.length&&o){r[z+2]=q}}}}if(n&&z!=r.length&&z>0&&r[z]!=null&&r[z]!=r[z-y]&&r[z+1]!=r[z-y+1]){for(m=0;m').load(handler).error(handler).attr('src', url); - }); - } - - function drawSeries(plot, ctx, series) { - var plotOffset = plot.getPlotOffset(); - - if (!series.images || !series.images.show) - return; - - var points = series.datapoints.points, - ps = series.datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var img = points[i], - x1 = points[i + 1], y1 = points[i + 2], - x2 = points[i + 3], y2 = points[i + 4], - xaxis = series.xaxis, yaxis = series.yaxis, - tmp; - - // actually we should check img.complete, but it - // appears to be a somewhat unreliable indicator in - // IE6 (false even after load event) - if (!img || img.width <= 0 || img.height <= 0) - continue; - - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - // if the anchor is at the center of the pixel, expand the - // image by 1/2 pixel in each direction - if (series.images.anchor == "center") { - tmp = 0.5 * (x2-x1) / (img.width - 1); - x1 -= tmp; - x2 += tmp; - tmp = 0.5 * (y2-y1) / (img.height - 1); - y1 -= tmp; - y2 += tmp; - } - - // clip - if (x1 == x2 || y1 == y2 || - x1 >= xaxis.max || x2 <= xaxis.min || - y1 >= yaxis.max || y2 <= yaxis.min) - continue; - - var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; - if (x1 < xaxis.min) { - sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); - x1 = xaxis.min; - } - - if (x2 > xaxis.max) { - sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); - x2 = xaxis.max; - } - - if (y1 < yaxis.min) { - sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); - y1 = yaxis.min; - } - - if (y2 > yaxis.max) { - sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); - y2 = yaxis.max; - } - - x1 = xaxis.p2c(x1); - x2 = xaxis.p2c(x2); - y1 = yaxis.p2c(y1); - y2 = yaxis.p2c(y2); - - // the transformation may have swapped us - if (x1 > x2) { - tmp = x2; - x2 = x1; - x1 = tmp; - } - if (y1 > y2) { - tmp = y2; - y2 = y1; - y1 = tmp; - } - - tmp = ctx.globalAlpha; - ctx.globalAlpha *= series.images.alpha; - ctx.drawImage(img, - sx1, sy1, sx2 - sx1, sy2 - sy1, - x1 + plotOffset.left, y1 + plotOffset.top, - x2 - x1, y2 - y1); - ctx.globalAlpha = tmp; - } - } - - function processRawData(plot, series, data, datapoints) { - if (!series.images.show) - return; - - // format is Image, x1, y1, x2, y2 (opposite corners) - datapoints.format = [ - { required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true }, - { x: true, number: true, required: true }, - { y: true, number: true, required: true } - ]; - } - - function init(plot) { - plot.hooks.processRawData.push(processRawData); - plot.hooks.drawSeries.push(drawSeries); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'image', - version: '1.1' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js deleted file mode 100644 index 9480c1e7a319..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.image.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(c){var a={series:{images:{show:false,alpha:1,anchor:"corner"}}};c.plot.image={};c.plot.image.loadDataImages=function(g,f,k){var j=[],h=[];var i=f.series.images.show;c.each(g,function(l,m){if(!(i||m.images.show)){return}if(m.data){m=m.data}c.each(m,function(n,o){if(typeof o[0]=="string"){j.push(o[0]);h.push(o)}})});c.plot.image.load(j,function(l){c.each(h,function(n,o){var m=o[0];if(l[m]){o[0]=l[m]}});k()})};c.plot.image.load=function(h,i){var g=h.length,f={};if(g==0){i({})}c.each(h,function(k,j){var l=function(){--g;f[j]=this;if(g==0){i(f)}};c("").load(l).error(l).attr("src",j)})};function d(q,o,l){var m=q.getPlotOffset();if(!l.images||!l.images.show){return}var r=l.datapoints.points,n=l.datapoints.pointsize;for(var t=0;tv){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}if(l.images.anchor=="center"){x=0.5*(v-w)/(y.width-1);w-=x;v+=x;x=0.5*(f-g)/(y.height-1);g-=x;f+=x}if(w==v||g==f||w>=h.max||v<=h.min||g>=u.max||f<=u.min){continue}var k=0,s=0,j=y.width,p=y.height;if(wh.max){j+=(j-k)*(h.max-v)/(v-w);v=h.max}if(gu.max){s+=(s-p)*(u.max-f)/(f-g);f=u.max}w=h.p2c(w);v=h.p2c(v);g=u.p2c(g);f=u.p2c(f);if(w>v){x=v;v=w;w=x}if(g>f){x=f;f=g;g=x}x=o.globalAlpha;o.globalAlpha*=l.images.alpha;o.drawImage(y,k,s,j-k,p-s,w+m.left,g+m.top,v-w,f-g);o.globalAlpha=x}}function b(i,f,g,h){if(!f.images.show){return}h.format=[{required:true},{x:true,number:true,required:true},{y:true,number:true,required:true},{x:true,number:true,required:true},{y:true,number:true,required:true}]}function e(f){f.hooks.processRawData.push(b);f.hooks.drawSeries.push(d)}c.plot.plugins.push({init:e,options:a,name:"image",version:"1.1"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.js b/frontend/src/vendor/jquery.flot/jquery.flot.js deleted file mode 100644 index 28abf7f5c8a2..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.js +++ /dev/null @@ -1,2599 +0,0 @@ -/*! Javascript plotting library for jQuery, v. 0.7. - * - * Released under the MIT license by IOLA, December 2007. - * - */ - -// first an inline dependency, jquery.colorhelpers.js, we inline it here -// for convenience - -/* Plugin for jQuery for working with colors. - * - * Version 1.1. - * - * Inspiration from jQuery color animation plugin by John Resig. - * - * Released under the MIT license by Ole Laursen, October 2009. - * - * Examples: - * - * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() - * var c = $.color.extract($("#mydiv"), 'background-color'); - * console.log(c.r, c.g, c.b, c.a); - * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" - * - * Note that .scale() and .add() return the same modified object - * instead of making a new one. - * - * V. 1.1: Fix error handling so e.g. parsing an empty string does - * produce a color rather than just crashing. - */ -(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); - -// the actual Flot code -(function($) { - function Plot(placeholder, data_, options_, plugins) { - // data is on the form: - // [ series1, series2 ... ] - // where series is either just the data as [ [x1, y1], [x2, y2], ... ] - // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } - - var series = [], - options = { - // the color theme used for graphs - colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], - legend: { - show: true, - noColumns: 1, // number of colums in legend table - labelFormatter: null, // fn: string -> string - labelBoxBorderColor: "#ccc", // border color for the little label boxes - container: null, // container (as jQuery object) to put legend in, null means default on top of graph - position: "ne", // position of default legend container within plot - margin: 5, // distance from grid edge to default legend container within plot - backgroundColor: null, // null means auto-detect - backgroundOpacity: 0.85 // set to 0 to avoid background - }, - xaxis: { - show: null, // null = auto-detect, true = always, false = never - position: "bottom", // or "top" - mode: null, // null or "time" - color: null, // base color, labels, ticks - tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" - transform: null, // null or f: number -> number to transform axis - inverseTransform: null, // if transform is set, this should be the inverse function - min: null, // min. value to show, null means set automatically - max: null, // max. value to show, null means set automatically - autoscaleMargin: null, // margin in % to add if auto-setting min/max - ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks - tickFormatter: null, // fn: number -> string - labelWidth: null, // size of tick labels in pixels - labelHeight: null, - reserveSpace: null, // whether to reserve space even if axis isn't shown - tickLength: null, // size in pixels of ticks, or "full" for whole line - alignTicksWithAxis: null, // axis number or null for no sync - - // mode specific options - tickDecimals: null, // no. of decimals, null means auto - tickSize: null, // number or [number, "unit"] - minTickSize: null, // number or [number, "unit"] - monthNames: null, // list of names of months - timeformat: null, // format string to use - twelveHourClock: false // 12 or 24 time in time mode - }, - yaxis: { - autoscaleMargin: 0.02, - position: "left" // or "right" - }, - xaxes: [], - yaxes: [], - series: { - points: { - show: false, - radius: 3, - lineWidth: 2, // in pixels - fill: true, - fillColor: "#ffffff", - symbol: "circle" // or callback - }, - lines: { - // we don't put in show: false so we can see - // whether lines were actively disabled - lineWidth: 2, // in pixels - fill: false, - fillColor: null, - steps: false - }, - bars: { - show: false, - lineWidth: 2, // in pixels - barWidth: 1, // in units of the x axis - fill: true, - fillColor: null, - align: "left", // or "center" - horizontal: false - }, - shadowSize: 3 - }, - grid: { - show: true, - aboveData: false, - color: "#545454", // primary color used for outline and labels - backgroundColor: null, // null for transparent, else color - borderColor: null, // set if different from the grid color - tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" - labelMargin: 5, // in pixels - axisMargin: 8, // in pixels - borderWidth: 2, // in pixels - minBorderMargin: null, // in pixels, null means taken from points radius - markings: null, // array of ranges or fn: axes -> array of ranges - markingsColor: "#f4f4f4", - markingsLineWidth: 2, - // interactive stuff - clickable: false, - hoverable: false, - autoHighlight: true, // highlight in case mouse is near - mouseActiveRadius: 10 // how far the mouse can be away to activate an item - }, - hooks: {} - }, - canvas = null, // the canvas for the plot itself - overlay = null, // canvas for interactive stuff on top of plot - eventHolder = null, // jQuery object that events should be bound to - ctx = null, octx = null, - xaxes = [], yaxes = [], - plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, - plotWidth = 0, plotHeight = 0, - hooks = { - processOptions: [], - processRawData: [], - processDatapoints: [], - drawSeries: [], - draw: [], - bindEvents: [], - drawOverlay: [], - shutdown: [] - }, - plot = this; - - // public functions - plot.setData = setData; - plot.setupGrid = setupGrid; - plot.draw = draw; - plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; - plot.getPlotOffset = function() { return plotOffset; }; - plot.width = function () { return plotWidth; }; - plot.height = function () { return plotHeight; }; - plot.offset = function () { - var o = eventHolder.offset(); - o.left += plotOffset.left; - o.top += plotOffset.top; - return o; - }; - plot.getData = function () { return series; }; - plot.getAxes = function () { - var res = {}, i; - $.each(xaxes.concat(yaxes), function (_, axis) { - if (axis) - res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; - }); - return res; - }; - plot.getXAxes = function () { return xaxes; }; - plot.getYAxes = function () { return yaxes; }; - plot.c2p = canvasToAxisCoords; - plot.p2c = axisToCanvasCoords; - plot.getOptions = function () { return options; }; - plot.highlight = highlight; - plot.unhighlight = unhighlight; - plot.triggerRedrawOverlay = triggerRedrawOverlay; - plot.pointOffset = function(point) { - return { - left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left), - top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top) - }; - }; - plot.shutdown = shutdown; - plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(canvas); - resizeCanvas(overlay); - }; - - // public attributes - plot.hooks = hooks; - - // initialize - initPlugins(plot); - parseOptions(options_); - setupCanvases(); - setData(data_); - setupGrid(); - draw(); - bindEvents(); - - - function executeHooks(hook, args) { - args = [plot].concat(args); - for (var i = 0; i < hook.length; ++i) - hook[i].apply(this, args); - } - - function initPlugins() { - for (var i = 0; i < plugins.length; ++i) { - var p = plugins[i]; - p.init(plot); - if (p.options) - $.extend(true, options, p.options); - } - } - - function parseOptions(opts) { - var i; - - $.extend(true, options, opts); - - if (options.xaxis.color == null) - options.xaxis.color = options.grid.color; - if (options.yaxis.color == null) - options.yaxis.color = options.grid.color; - - if (options.xaxis.tickColor == null) // backwards-compatibility - options.xaxis.tickColor = options.grid.tickColor; - if (options.yaxis.tickColor == null) // backwards-compatibility - options.yaxis.tickColor = options.grid.tickColor; - - if (options.grid.borderColor == null) - options.grid.borderColor = options.grid.color; - if (options.grid.tickColor == null) - options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - - // fill in defaults in axes, copy at least always the - // first as the rest of the code assumes it'll be there - for (i = 0; i < Math.max(1, options.xaxes.length); ++i) - options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]); - for (i = 0; i < Math.max(1, options.yaxes.length); ++i) - options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]); - - // backwards compatibility, to be removed in future - if (options.xaxis.noTicks && options.xaxis.ticks == null) - options.xaxis.ticks = options.xaxis.noTicks; - if (options.yaxis.noTicks && options.yaxis.ticks == null) - options.yaxis.ticks = options.yaxis.noTicks; - if (options.x2axis) { - options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); - options.xaxes[1].position = "top"; - } - if (options.y2axis) { - options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); - options.yaxes[1].position = "right"; - } - if (options.grid.coloredAreas) - options.grid.markings = options.grid.coloredAreas; - if (options.grid.coloredAreasColor) - options.grid.markingsColor = options.grid.coloredAreasColor; - if (options.lines) - $.extend(true, options.series.lines, options.lines); - if (options.points) - $.extend(true, options.series.points, options.points); - if (options.bars) - $.extend(true, options.series.bars, options.bars); - if (options.shadowSize != null) - options.series.shadowSize = options.shadowSize; - - // save options on axes for future reference - for (i = 0; i < options.xaxes.length; ++i) - getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; - for (i = 0; i < options.yaxes.length; ++i) - getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; - - // add hooks from options - for (var n in hooks) - if (options.hooks[n] && options.hooks[n].length) - hooks[n] = hooks[n].concat(options.hooks[n]); - - executeHooks(hooks.processOptions, [options]); - } - - function setData(d) { - series = parseData(d); - fillInSeriesOptions(); - processData(); - } - - function parseData(d) { - var res = []; - for (var i = 0; i < d.length; ++i) { - var s = $.extend(true, {}, options.series); - - if (d[i].data != null) { - s.data = d[i].data; // move the data instead of deep-copy - delete d[i].data; - - $.extend(true, s, d[i]); - - d[i].data = s.data; - } - else - s.data = d[i]; - res.push(s); - } - - return res; - } - - function axisNumber(obj, coord) { - var a = obj[coord + "axis"]; - if (typeof a == "object") // if we got a real axis, extract number - a = a.n; - if (typeof a != "number") - a = 1; // default to first axis - return a; - } - - function allAxes() { - // return flat array without annoying null entries - return $.grep(xaxes.concat(yaxes), function (a) { return a; }); - } - - function canvasToAxisCoords(pos) { - // return an object with x/y corresponding to all used axes - var res = {}, i, axis; - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) - res["x" + axis.n] = axis.c2p(pos.left); - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) - res["y" + axis.n] = axis.c2p(pos.top); - } - - if (res.x1 !== undefined) - res.x = res.x1; - if (res.y1 !== undefined) - res.y = res.y1; - - return res; - } - - function axisToCanvasCoords(pos) { - // get canvas coords from the first pair of x/y found in pos - var res = {}, i, axis, key; - - for (i = 0; i < xaxes.length; ++i) { - axis = xaxes[i]; - if (axis && axis.used) { - key = "x" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "x"; - - if (pos[key] != null) { - res.left = axis.p2c(pos[key]); - break; - } - } - } - - for (i = 0; i < yaxes.length; ++i) { - axis = yaxes[i]; - if (axis && axis.used) { - key = "y" + axis.n; - if (pos[key] == null && axis.n == 1) - key = "y"; - - if (pos[key] != null) { - res.top = axis.p2c(pos[key]); - break; - } - } - } - - return res; - } - - function getOrCreateAxis(axes, number) { - if (!axes[number - 1]) - axes[number - 1] = { - n: number, // save the number for future reference - direction: axes == xaxes ? "x" : "y", - options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) - }; - - return axes[number - 1]; - } - - function fillInSeriesOptions() { - var i; - - // collect what we already got of colors - var neededColors = series.length, - usedColors = [], - assignedColors = []; - for (i = 0; i < series.length; ++i) { - var sc = series[i].color; - if (sc != null) { - --neededColors; - if (typeof sc == "number") - assignedColors.push(sc); - else - usedColors.push($.color.parse(series[i].color)); - } - } - - // we might need to generate more colors if higher indices - // are assigned - for (i = 0; i < assignedColors.length; ++i) { - neededColors = Math.max(neededColors, assignedColors[i] + 1); - } - - // produce colors as needed - var colors = [], variation = 0; - i = 0; - while (colors.length < neededColors) { - var c; - if (options.colors.length == i) // check degenerate case - c = $.color.make(100, 100, 100); - else - c = $.color.parse(options.colors[i]); - - // vary color if needed - var sign = variation % 2 == 1 ? -1 : 1; - c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2) - - // FIXME: if we're getting to close to something else, - // we should probably skip this one - colors.push(c); - - ++i; - if (i >= options.colors.length) { - i = 0; - ++variation; - } - } - - // fill in the options - var colori = 0, s; - for (i = 0; i < series.length; ++i) { - s = series[i]; - - // assign colors - if (s.color == null) { - s.color = colors[colori].toString(); - ++colori; - } - else if (typeof s.color == "number") - s.color = colors[s.color].toString(); - - // turn on lines automatically in case nothing is set - if (s.lines.show == null) { - var v, show = true; - for (v in s) - if (s[v] && s[v].show) { - show = false; - break; - } - if (show) - s.lines.show = true; - } - - // setup axes - s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); - s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); - } - } - - function processData() { - var topSentry = Number.POSITIVE_INFINITY, - bottomSentry = Number.NEGATIVE_INFINITY, - fakeInfinity = Number.MAX_VALUE, - i, j, k, m, length, - s, points, ps, x, y, axis, val, f, p; - - function updateAxis(axis, min, max) { - if (min < axis.datamin && min != -fakeInfinity) - axis.datamin = min; - if (max > axis.datamax && max != fakeInfinity) - axis.datamax = max; - } - - $.each(allAxes(), function (_, axis) { - // init axis - axis.datamin = topSentry; - axis.datamax = bottomSentry; - axis.used = false; - }); - - for (i = 0; i < series.length; ++i) { - s = series[i]; - s.datapoints = { points: [] }; - - executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); - } - - // first pass: clean and copy data - for (i = 0; i < series.length; ++i) { - s = series[i]; - - var data = s.data, format = s.datapoints.format; - - if (!format) { - format = []; - // find out how to copy - format.push({ x: true, number: true, required: true }); - format.push({ y: true, number: true, required: true }); - - if (s.bars.show || (s.lines.show && s.lines.fill)) { - format.push({ y: true, number: true, required: false, defaultValue: 0 }); - if (s.bars.horizontal) { - delete format[format.length - 1].y; - format[format.length - 1].x = true; - } - } - - s.datapoints.format = format; - } - - if (s.datapoints.pointsize != null) - continue; // already filled in - - s.datapoints.pointsize = format.length; - - ps = s.datapoints.pointsize; - points = s.datapoints.points; - - let insertSteps = s.lines.show && s.lines.steps; - s.xaxis.used = s.yaxis.used = true; - - for (j = k = 0; j < data.length; ++j, k += ps) { - p = data[j]; - - var nullify = p == null; - if (!nullify) { - for (m = 0; m < ps; ++m) { - val = p[m]; - f = format[m]; - - if (f) { - if (f.number && val != null) { - val = +val; // convert to number - if (isNaN(val)) - val = null; - else if (val == Infinity) - val = fakeInfinity; - else if (val == -Infinity) - val = -fakeInfinity; - } - - if (val == null) { - if (f.required) - nullify = true; - - if (f.defaultValue != null) - val = f.defaultValue; - } - } - - points[k + m] = val; - } - } - - if (nullify) { - for (m = 0; m < ps; ++m) { - val = points[k + m]; - if (val != null) { - f = format[m]; - // extract min/max info - if (f.x) - updateAxis(s.xaxis, val, val); - if (f.y) - updateAxis(s.yaxis, val, val); - } - points[k + m] = null; - } - } - else { - // a little bit of line specific stuff that - // perhaps shouldn't be here, but lacking - // better means... - if (insertSteps && k > 0 - && points[k - ps] != null - && points[k - ps] != points[k] - && points[k - ps + 1] != points[k + 1]) { - // copy the point to make room for a middle point - for (m = 0; m < ps; ++m) - points[k + ps + m] = points[k + m]; - - // middle point has same y - points[k + 1] = points[k - ps + 1]; - - // we've added a point, better reflect that - k += ps; - } - } - } - } - - // give the hooks a chance to run - for (i = 0; i < series.length; ++i) { - s = series[i]; - - executeHooks(hooks.processDatapoints, [ s, s.datapoints]); - } - - // second pass: find datamax/datamin for auto-scaling - for (i = 0; i < series.length; ++i) { - s = series[i]; - points = s.datapoints.points, - ps = s.datapoints.pointsize; - - var xmin = topSentry, ymin = topSentry, - xmax = bottomSentry, ymax = bottomSentry; - - for (j = 0; j < points.length; j += ps) { - if (points[j] == null) - continue; - - for (m = 0; m < ps; ++m) { - val = points[j + m]; - f = format[m]; - if (!f || val == fakeInfinity || val == -fakeInfinity) - continue; - - if (f.x) { - if (val < xmin) - xmin = val; - if (val > xmax) - xmax = val; - } - if (f.y) { - if (val < ymin) - ymin = val; - if (val > ymax) - ymax = val; - } - } - } - - if (s.bars.show) { - // make sure we got room for the bar on the dancing floor - var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; - if (s.bars.horizontal) { - ymin += delta; - ymax += delta + s.bars.barWidth; - } - else { - xmin += delta; - xmax += delta + s.bars.barWidth; - } - } - - updateAxis(s.xaxis, xmin, xmax); - updateAxis(s.yaxis, ymin, ymax); - } - - $.each(allAxes(), function (_, axis) { - if (axis.datamin == topSentry) - axis.datamin = null; - if (axis.datamax == bottomSentry) - axis.datamax = null; - }); - } - - function makeCanvas(skipPositioning, cls) { - var c = document.createElement('canvas'); - c.className = cls; - c.width = canvasWidth; - c.height = canvasHeight; - - if (!skipPositioning) - $(c).css({ position: 'absolute', left: 0, top: 0 }); - - $(c).appendTo(placeholder); - - if (!c.getContext) // excanvas hack - c = window.G_vmlCanvasManager.initElement(c); - - // used for resetting in case we get replotted - c.getContext("2d").save(); - - return c; - } - - function getCanvasDimensions() { - canvasWidth = placeholder.width(); - canvasHeight = placeholder.height(); - - if (canvasWidth <= 0 || canvasHeight <= 0) - throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight); - } - - function resizeCanvas(c) { - // resizing should reset the state (excanvas seems to be - // buggy though) - if (c.width != canvasWidth) - c.width = canvasWidth; - - if (c.height != canvasHeight) - c.height = canvasHeight; - - // so try to get back to the initial state (even if it's - // gone now, this should be safe according to the spec) - var cctx = c.getContext("2d"); - cctx.restore(); - - // and save again - cctx.save(); - } - - function setupCanvases() { - var reused, - existingCanvas = placeholder.children("canvas.base"), - existingOverlay = placeholder.children("canvas.overlay"); - - if (existingCanvas.length == 0 || existingOverlay == 0) { - // init everything - - placeholder.html(""); // make sure placeholder is clear - - placeholder.css({ padding: 0 }); // padding messes up the positioning - - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay - - getCanvasDimensions(); - - canvas = makeCanvas(true, "base"); - overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features - - reused = false; - } - else { - // reuse existing elements - - canvas = existingCanvas.get(0); - overlay = existingOverlay.get(0); - - reused = true; - } - - ctx = canvas.getContext("2d"); - octx = overlay.getContext("2d"); - - // we include the canvas in the event holder too, because IE 7 - // sometimes has trouble with the stacking order - eventHolder = $([overlay, canvas]); - - if (reused) { - // run shutdown in the old plot object - placeholder.data("plot").shutdown(); - - // reset reused canvases - plot.resize(); - - // make sure overlay pixels are cleared (canvas is cleared when we redraw) - octx.clearRect(0, 0, canvasWidth, canvasHeight); - - // then whack any remaining obvious garbage left - eventHolder.unbind(); - placeholder.children().not([canvas, overlay]).remove(); - } - - // save in case we get replotted - placeholder.data("plot", plot); - } - - function bindEvents() { - // bind events - if (options.grid.hoverable) { - eventHolder.mousemove(onMouseMove); - eventHolder.mouseleave(onMouseLeave); - } - - if (options.grid.clickable) - eventHolder.click(onClick); - - executeHooks(hooks.bindEvents, [eventHolder]); - } - - function shutdown() { - if (redrawTimeout) - clearTimeout(redrawTimeout); - - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mouseleave", onMouseLeave); - eventHolder.unbind("click", onClick); - - executeHooks(hooks.shutdown, [eventHolder]); - } - - function setTransformationHelpers(axis) { - // set helper functions on the axis, assumes plot area - // has been computed already - - function identity(x) { return x; } - - var s, m, t = axis.options.transform || identity, - it = axis.options.inverseTransform; - - // precompute how much the axis is scaling a point - // in canvas space - if (axis.direction == "x") { - s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); - m = Math.min(t(axis.max), t(axis.min)); - } - else { - s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); - s = -s; - m = Math.max(t(axis.max), t(axis.min)); - } - - // data point to canvas coordinate - if (t == identity) // slight optimization - axis.p2c = function (p) { return (p - m) * s; }; - else - axis.p2c = function (p) { return (t(p) - m) * s; }; - // canvas coordinate to data point - if (!it) - axis.c2p = function (c) { return m + c / s; }; - else - axis.c2p = function (c) { return it(m + c / s); }; - } - - function measureTickLabels(axis) { - var opts = axis.options, i, ticks = axis.ticks || [], labels = [], - l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; - - function makeDummyDiv(labels, width) { - return $('
' + - '
' - + labels.join("") + '
') - .appendTo(placeholder); - } - - if (axis.direction == "x") { - // to avoid measuring the widths of the labels (it's slow), we - // construct fixed-size boxes and put the labels inside - // them, we don't need the exact figures and the - // fixed-size box content is easy to center - if (w == null) - w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1)); - - // measure x label heights - if (h == null) { - labels = []; - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - // stick them all in the same div and measure - // collective height - labels.push('
'); - dummyDiv = makeDummyDiv(labels, "width:10000px;"); - h = dummyDiv.height(); - dummyDiv.remove(); - } - } - } - else if (w == null || h == null) { - // calculate y label dimensions - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - dummyDiv = makeDummyDiv(labels, ""); - if (w == null) - w = dummyDiv.children().width(); - if (h == null) - h = dummyDiv.find("div.tickLabel").height(); - dummyDiv.remove(); - } - } - - if (w == null) - w = 0; - if (h == null) - h = 0; - - axis.labelWidth = w; - axis.labelHeight = h; - } - - function allocateAxisBoxFirstPhase(axis) { - // find the bounding box of the axis by looking at label - // widths/heights and ticks, make room by diminishing the - // plotOffset - - var lw = axis.labelWidth, - lh = axis.labelHeight, - pos = axis.options.position, - tickLength = axis.options.tickLength, - axismargin = options.grid.axisMargin, - padding = options.grid.labelMargin, - all = axis.direction == "x" ? xaxes : yaxes, - index; - - // determine axis margin - var samePosition = $.grep(all, function (a) { - return a && a.options.position == pos && a.reserveSpace; - }); - if ($.inArray(axis, samePosition) == samePosition.length - 1) - axismargin = 0; // outermost - - // determine tick length - if we're innermost, we can use "full" - if (tickLength == null) - tickLength = "full"; - - var sameDirection = $.grep(all, function (a) { - return a && a.reserveSpace; - }); - - var innermost = $.inArray(axis, sameDirection) == 0; - if (!innermost && tickLength == "full") - tickLength = 5; - - if (!isNaN(+tickLength)) - padding += +tickLength; - - // compute box - if (axis.direction == "x") { - lh += padding; - - if (pos == "bottom") { - plotOffset.bottom += lh + axismargin; - axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; - } - else { - axis.box = { top: plotOffset.top + axismargin, height: lh }; - plotOffset.top += lh + axismargin; - } - } - else { - lw += padding; - - if (pos == "left") { - axis.box = { left: plotOffset.left + axismargin, width: lw }; - plotOffset.left += lw + axismargin; - } - else { - plotOffset.right += lw + axismargin; - axis.box = { left: canvasWidth - plotOffset.right, width: lw }; - } - } - - // save for future reference - axis.position = pos; - axis.tickLength = tickLength; - axis.box.padding = padding; - axis.innermost = innermost; - } - - function allocateAxisBoxSecondPhase(axis) { - // set remaining bounding box coordinates - if (axis.direction == "x") { - axis.box.left = plotOffset.left; - axis.box.width = plotWidth; - } - else { - axis.box.top = plotOffset.top; - axis.box.height = plotHeight; - } - } - - function setupGrid() { - var i, axes = allAxes(); - - // first calculate the plot and axis box dimensions - - $.each(axes, function (_, axis) { - axis.show = axis.options.show; - if (axis.show == null) - axis.show = axis.used; // by default an axis is visible if it's got data - - axis.reserveSpace = axis.show || axis.options.reserveSpace; - - setRange(axis); - }); - - let allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); - - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; - if (options.grid.show) { - $.each(allocatedAxes, function (_, axis) { - // make the ticks - setupTickGeneration(axis); - setTicks(axis); - snapRangeToTicks(axis, axis.ticks); - - // find labelWidth/Height for axis - measureTickLabels(axis); - }); - - // with all dimensions in house, we can compute the - // axis boxes, start from the outside (reverse order) - for (i = allocatedAxes.length - 1; i >= 0; --i) - allocateAxisBoxFirstPhase(allocatedAxes[i]); - - // make sure we've got enough space for things that - // might stick out - var minMargin = options.grid.minBorderMargin; - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2); - } - - for (var a in plotOffset) { - plotOffset[a] += options.grid.borderWidth; - plotOffset[a] = Math.max(minMargin, plotOffset[a]); - } - } - - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - - // now we got the proper plotWidth/Height, we can compute the scaling - $.each(axes, function (_, axis) { - setTransformationHelpers(axis); - }); - - if (options.grid.show) { - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - - insertAxisLabels(); - } - - insertLegend(); - } - - function setRange(axis) { - var opts = axis.options, - min = +(opts.min != null ? opts.min : axis.datamin), - max = +(opts.max != null ? opts.max : axis.datamax), - delta = max - min; - - if (delta == 0.0) { - // degenerate case - var widen = max == 0 ? 1 : 0.01; - - if (opts.min == null) - min -= widen; - // always widen max if we couldn't widen min to ensure we - // don't fall into min == max which doesn't work - if (opts.max == null || opts.min != null) - max += widen; - } - else { - // consider autoscaling - var margin = opts.autoscaleMargin; - if (margin != null) { - if (opts.min == null) { - min -= delta * margin; - // make sure we don't go below zero if all values - // are positive - if (min < 0 && axis.datamin != null && axis.datamin >= 0) - min = 0; - } - if (opts.max == null) { - max += delta * margin; - if (max > 0 && axis.datamax != null && axis.datamax <= 0) - max = 0; - } - } - } - axis.min = min; - axis.max = max; - } - - function setupTickGeneration(axis) { - var opts = axis.options; - - // estimate number of ticks - var noTicks; - if (typeof opts.ticks == "number" && opts.ticks > 0) - noTicks = opts.ticks; - else - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight); - - var delta = (axis.max - axis.min) / noTicks, - size, generator, unit, formatter, i, magn, norm; - - if (opts.mode == "time") { - // pretty handling of time - - // map of app. size of time units in milliseconds - var timeUnitSize = { - "second": 1000, - "minute": 60 * 1000, - "hour": 60 * 60 * 1000, - "day": 24 * 60 * 60 * 1000, - "month": 30 * 24 * 60 * 60 * 1000, - "year": 365.2425 * 24 * 60 * 60 * 1000 - }; - - - // the allowed tick sizes, after 1 year we use - // an integer algorithm - var spec = [ - [1, "second"], [2, "second"], [5, "second"], [10, "second"], - [30, "second"], - [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], - [30, "minute"], - [1, "hour"], [2, "hour"], [4, "hour"], - [8, "hour"], [12, "hour"], - [1, "day"], [2, "day"], [3, "day"], - [0.25, "month"], [0.5, "month"], [1, "month"], - [2, "month"], [3, "month"], [6, "month"], - [1, "year"] - ]; - - var minSize = 0; - if (opts.minTickSize != null) { - if (typeof opts.tickSize == "number") - minSize = opts.tickSize; - else - minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; - } - - for (var i = 0; i < spec.length - 1; ++i) - if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] - + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 - && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) - break; - size = spec[i][0]; - unit = spec[i][1]; - - // special-case the possibility of several years - if (unit == "year") { - magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); - norm = (delta / timeUnitSize.year) / magn; - if (norm < 1.5) - size = 1; - else if (norm < 3) - size = 2; - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - } - - axis.tickSize = opts.tickSize || [size, unit]; - - generator = function(axis) { - var ticks = [], - tickSize = axis.tickSize[0], unit = axis.tickSize[1], - d = new Date(axis.min); - - var step = tickSize * timeUnitSize[unit]; - - if (unit == "second") - d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); - if (unit == "minute") - d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); - if (unit == "hour") - d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); - if (unit == "month") - d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); - if (unit == "year") - d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); - - // reset smaller components - d.setUTCMilliseconds(0); - if (step >= timeUnitSize.minute) - d.setUTCSeconds(0); - if (step >= timeUnitSize.hour) - d.setUTCMinutes(0); - if (step >= timeUnitSize.day) - d.setUTCHours(0); - if (step >= timeUnitSize.day * 4) - d.setUTCDate(1); - if (step >= timeUnitSize.year) - d.setUTCMonth(0); - - - var carry = 0, v = Number.NaN, prev; - do { - prev = v; - v = d.getTime(); - ticks.push(v); - if (unit == "month") { - if (tickSize < 1) { - // a bit complicated - we'll divide the month - // up but we need to take care of fractions - // so we don't end up in the middle of a day - d.setUTCDate(1); - var start = d.getTime(); - d.setUTCMonth(d.getUTCMonth() + 1); - var end = d.getTime(); - d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); - carry = d.getUTCHours(); - d.setUTCHours(0); - } - else - d.setUTCMonth(d.getUTCMonth() + tickSize); - } - else if (unit == "year") { - d.setUTCFullYear(d.getUTCFullYear() + tickSize); - } - else - d.setTime(v + step); - } while (v < axis.max && v != prev); - - return ticks; - }; - - formatter = function (v, axis) { - var d = new Date(v); - - // first check global format - if (opts.timeformat != null) - return $.plot.formatDate(d, opts.timeformat, opts.monthNames); - - var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; - var span = axis.max - axis.min; - var suffix = (opts.twelveHourClock) ? " %p" : ""; - - if (t < timeUnitSize.minute) - fmt = "%h:%M:%S" + suffix; - else if (t < timeUnitSize.day) { - if (span < 2 * timeUnitSize.day) - fmt = "%h:%M" + suffix; - else - fmt = "%b %d %h:%M" + suffix; - } - else if (t < timeUnitSize.month) - fmt = "%b %d"; - else if (t < timeUnitSize.year) { - if (span < timeUnitSize.year) - fmt = "%b"; - else - fmt = "%b %y"; - } - else - fmt = "%y"; - - return $.plot.formatDate(d, fmt, opts.monthNames); - }; - } - else { - // pretty rounding of base-10 numbers - var maxDec = opts.tickDecimals; - var dec = -Math.floor(Math.log(delta) / Math.LN10); - if (maxDec != null && dec > maxDec) - dec = maxDec; - - magn = Math.pow(10, -dec); - norm = delta / magn; // norm is between 1.0 and 10.0 - - if (norm < 1.5) - size = 1; - else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } - else if (norm < 7.5) - size = 5; - else - size = 10; - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) - size = opts.minTickSize; - - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - - generator = function (axis) { - var ticks = []; - - // spew out all possible ticks - var start = floorInBase(axis.min, axis.tickSize), - i = 0, v = Number.NaN, prev; - do { - prev = v; - v = start + i * axis.tickSize; - ticks.push(v); - ++i; - } while (v < axis.max && v != prev); - return ticks; - }; - - formatter = function (v, axis) { - return v.toFixed(axis.tickDecimals); - }; - } - - if (opts.alignTicksWithAxis != null) { - var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; - if (otherAxis && otherAxis.used && otherAxis != axis) { - // consider snapping min/max to outermost nice ticks - var niceTicks = generator(axis); - if (niceTicks.length > 0) { - if (opts.min == null) - axis.min = Math.min(axis.min, niceTicks[0]); - if (opts.max == null && niceTicks.length > 1) - axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); - } - - generator = function (axis) { - // copy ticks, scaled to this axis - var ticks = [], v, i; - for (i = 0; i < otherAxis.ticks.length; ++i) { - v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); - v = axis.min + v * (axis.max - axis.min); - ticks.push(v); - } - return ticks; - }; - - // we might need an extra decimal since forced - // ticks don't necessarily fit naturally - if (axis.mode != "time" && opts.tickDecimals == null) { - var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1), - ts = generator(axis); - - // only proceed if the tick interval rounded - // with an extra decimal doesn't give us a - // zero at end - if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) - axis.tickDecimals = extraDec; - } - } - } - - axis.tickGenerator = generator; - if ($.isFunction(opts.tickFormatter)) - axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; - else - axis.tickFormatter = formatter; - } - - function setTicks(axis) { - var oticks = axis.options.ticks, ticks = []; - if (oticks == null || (typeof oticks == "number" && oticks > 0)) - ticks = axis.tickGenerator(axis); - else if (oticks) { - if ($.isFunction(oticks)) - // generate the ticks - ticks = oticks({ min: axis.min, max: axis.max }); - else - ticks = oticks; - } - - // clean up/labelify the supplied ticks, copy them over - var i, v; - axis.ticks = []; - for (i = 0; i < ticks.length; ++i) { - var label = null; - var t = ticks[i]; - if (typeof t == "object") { - v = +t[0]; - if (t.length > 1) - label = t[1]; - } - else - v = +t; - if (label == null) - label = axis.tickFormatter(v, axis); - if (!isNaN(v)) - axis.ticks.push({ v: v, label: label }); - } - } - - function snapRangeToTicks(axis, ticks) { - if (axis.options.autoscaleMargin && ticks.length > 0) { - // snap to ticks - if (axis.options.min == null) - axis.min = Math.min(axis.min, ticks[0].v); - if (axis.options.max == null && ticks.length > 1) - axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); - } - } - - function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - - var grid = options.grid; - - // draw background, if any - if (grid.show && grid.backgroundColor) - drawBackground(); - - if (grid.show && !grid.aboveData) - drawGrid(); - - for (var i = 0; i < series.length; ++i) { - executeHooks(hooks.drawSeries, [ctx, series[i]]); - drawSeries(series[i]); - } - - executeHooks(hooks.draw, [ctx]); - - if (grid.show && grid.aboveData) - drawGrid(); - } - - function extractRange(ranges, coord) { - var axis, from, to, key, axes = allAxes(); - - for (i = 0; i < axes.length; ++i) { - axis = axes[i]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? xaxes[0] : yaxes[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function drawBackground() { - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); - ctx.fillRect(0, 0, plotWidth, plotHeight); - ctx.restore(); - } - - function drawGrid() { - var i; - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // draw markings - var markings = options.grid.markings; - if (markings) { - if ($.isFunction(markings)) { - var axes = plot.getAxes(); - // xmin etc. is backwards compatibility, to be - // removed in the future - axes.xmin = axes.xaxis.min; - axes.xmax = axes.xaxis.max; - axes.ymin = axes.yaxis.min; - axes.ymax = axes.yaxis.max; - - markings = markings(axes); - } - - for (i = 0; i < markings.length; ++i) { - var m = markings[i], - xrange = extractRange(m, "x"), - yrange = extractRange(m, "y"); - - // fill in missing - if (xrange.from == null) - xrange.from = xrange.axis.min; - if (xrange.to == null) - xrange.to = xrange.axis.max; - if (yrange.from == null) - yrange.from = yrange.axis.min; - if (yrange.to == null) - yrange.to = yrange.axis.max; - - // clip - if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || - yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) - continue; - - xrange.from = Math.max(xrange.from, xrange.axis.min); - xrange.to = Math.min(xrange.to, xrange.axis.max); - yrange.from = Math.max(yrange.from, yrange.axis.min); - yrange.to = Math.min(yrange.to, yrange.axis.max); - - if (xrange.from == xrange.to && yrange.from == yrange.to) - continue; - - // then draw - xrange.from = xrange.axis.p2c(xrange.from); - xrange.to = xrange.axis.p2c(xrange.to); - yrange.from = yrange.axis.p2c(yrange.from); - yrange.to = yrange.axis.p2c(yrange.to); - - if (xrange.from == xrange.to || yrange.from == yrange.to) { - // draw line - ctx.beginPath(); - ctx.strokeStyle = m.color || options.grid.markingsColor; - ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; - ctx.moveTo(xrange.from, yrange.from); - ctx.lineTo(xrange.to, yrange.to); - ctx.stroke(); - } - else { - // fill area - ctx.fillStyle = m.color || options.grid.markingsColor; - ctx.fillRect(xrange.from, yrange.to, - xrange.to - xrange.from, - yrange.from - yrange.to); - } - } - } - - // draw the ticks - var axes = allAxes(), bw = options.grid.borderWidth; - - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box, - t = axis.tickLength, x, y, xoff, yoff; - if (!axis.show || axis.ticks.length == 0) - continue - - ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString(); - ctx.lineWidth = 1; - - // find the edges - if (axis.direction == "x") { - x = 0; - if (t == "full") - y = (axis.position == "top" ? 0 : plotHeight); - else - y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); - } - else { - y = 0; - if (t == "full") - x = (axis.position == "left" ? 0 : plotWidth); - else - x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); - } - - // draw tick bar - if (!axis.innermost) { - ctx.beginPath(); - xoff = yoff = 0; - if (axis.direction == "x") - xoff = plotWidth; - else - yoff = plotHeight; - - if (ctx.lineWidth == 1) { - x = Math.floor(x) + 0.5; - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - ctx.stroke(); - } - - // draw ticks - ctx.beginPath(); - for (i = 0; i < axis.ticks.length; ++i) { - var v = axis.ticks[i].v; - - xoff = yoff = 0; - - if (v < axis.min || v > axis.max - // skip those lying on the axes if we got a border - || (t == "full" && bw > 0 - && (v == axis.min || v == axis.max))) - continue; - - if (axis.direction == "x") { - x = axis.p2c(v); - yoff = t == "full" ? -plotHeight : t; - - if (axis.position == "top") - yoff = -yoff; - } - else { - y = axis.p2c(v); - xoff = t == "full" ? -plotWidth : t; - - if (axis.position == "left") - xoff = -xoff; - } - - if (ctx.lineWidth == 1) { - if (axis.direction == "x") - x = Math.floor(x) + 0.5; - else - y = Math.floor(y) + 0.5; - } - - ctx.moveTo(x, y); - ctx.lineTo(x + xoff, y + yoff); - } - - ctx.stroke(); - } - - - // draw border - if (bw) { - ctx.lineWidth = bw; - ctx.strokeStyle = options.grid.borderColor; - ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); - } - - ctx.restore(); - } - - function insertAxisLabels() { - placeholder.find(".tickLabels").remove(); - - var html = ['
']; - - var axes = allAxes(); - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box; - if (!axis.show) - continue; - //debug: html.push('
') - html.push('
'); - for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; - if (!tick.label || tick.v < axis.min || tick.v > axis.max) - continue; - - var pos = {}, align; - - if (axis.direction == "x") { - align = "center"; - pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2); - if (axis.position == "bottom") - pos.top = box.top + box.padding; - else - pos.bottom = canvasHeight - (box.top + box.height - box.padding); - } - else { - pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2); - if (axis.position == "left") { - pos.right = canvasWidth - (box.left + box.width - box.padding) - align = "right"; - } - else { - pos.left = box.left + box.padding; - align = "left"; - } - } - - pos.width = axis.labelWidth; - - var style = ["position:absolute", "text-align:" + align ]; - for (var a in pos) - style.push(a + ":" + pos[a] + "px") - - html.push('
' + tick.label + '
'); - } - html.push('
'); - } - - html.push('
'); - - placeholder.append(html.join("")); - } - - function drawSeries(series) { - if (series.lines.show) - drawSeriesLines(series); - if (series.bars.show) - drawSeriesBars(series); - if (series.points.show) - drawSeriesPoints(series); - } - - function drawSeriesLines(series) { - function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - prevx = null, prevy = null; - - ctx.beginPath(); - for (var i = ps; i < points.length; i += ps) { - var x1 = points[i - ps], y1 = points[i - ps + 1], - x2 = points[i], y2 = points[i + 1]; - - if (x1 == null || x2 == null) - continue; - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min) { - if (y2 < axisy.min) - continue; // line segment is outside - // compute new intersection point - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min) { - if (y1 < axisy.min) - continue; - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max) { - if (y2 > axisy.max) - continue; - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max) { - if (y1 > axisy.max) - continue; - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (x1 != prevx || y1 != prevy) - ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); - - prevx = x2; - prevy = y2; - ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); - } - ctx.stroke(); - } - - function plotLineArea(datapoints, axisx, axisy) { - var points = datapoints.points, - ps = datapoints.pointsize, - bottom = Math.min(Math.max(0, axisy.min), axisy.max), - i = 0, top, areaOpen = false, - ypos = 1, segmentStart = 0, segmentEnd = 0; - - // we process each segment in two turns, first forward - // direction to sketch out top, then once we hit the - // end we go backwards to sketch the bottom - while (true) { - if (ps > 0 && i > points.length + ps) - break; - - i += ps; // ps is negative if going backwards - - var x1 = points[i - ps], - y1 = points[i - ps + ypos], - x2 = points[i], y2 = points[i + ypos]; - - if (areaOpen) { - if (ps > 0 && x1 != null && x2 == null) { - // at turning point - segmentEnd = i; - ps = -ps; - ypos = 2; - continue; - } - - if (ps < 0 && i == segmentStart + ps) { - // done with the reverse sweep - ctx.fill(); - areaOpen = false; - ps = -ps; - ypos = 1; - i = segmentStart = segmentEnd + ps; - continue; - } - } - - if (x1 == null || x2 == null) - continue; - - // clip x values - - // clip with xmin - if (x1 <= x2 && x1 < axisx.min) { - if (x2 < axisx.min) - continue; - y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.min; - } - else if (x2 <= x1 && x2 < axisx.min) { - if (x1 < axisx.min) - continue; - y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.min; - } - - // clip with xmax - if (x1 >= x2 && x1 > axisx.max) { - if (x2 > axisx.max) - continue; - y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = axisx.max; - } - else if (x2 >= x1 && x2 > axisx.max) { - if (x1 > axisx.max) - continue; - y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = axisx.max; - } - - if (!areaOpen) { - // open area - ctx.beginPath(); - ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); - areaOpen = true; - } - - // now first check the case where both is outside - if (y1 >= axisy.max && y2 >= axisy.max) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); - continue; - } - else if (y1 <= axisy.min && y2 <= axisy.min) { - ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); - continue; - } - - // else it's a bit more complicated, there might - // be a flat maxed out rectangle first, then a - // triangular cutout or reverse; to find these - // keep track of the current x values - var x1old = x1, x2old = x2; - - // clip the y values, without shortcutting, we - // go through all cases in turn - - // clip with ymin - if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { - x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.min; - } - else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { - x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.min; - } - - // clip with ymax - if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { - x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = axisy.max; - } - else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { - x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = axisy.max; - } - - // if the x value was changed we got a rectangle - // to fill - if (x1 != x1old) { - ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); - // it goes to (x1, y1), but we fill that below - } - - // fill triangular section, this sometimes result - // in redundant points if (x1, y1) hasn't changed - // from previous line to, but we just ignore that - ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - - // fill the other rectangle if it's there - if (x2 != x2old) { - ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); - ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); - } - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - ctx.lineJoin = "round"; - - var lw = series.lines.lineWidth, - sw = series.shadowSize; - // FIXME: consider another form of shadow when filling is turned on - if (lw > 0 && sw > 0) { - // draw shadow as a thick and thin line with transparency - ctx.lineWidth = sw; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - // position shadow at angle from the mid of line - var angle = Math.PI/18; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); - ctx.lineWidth = sw/2; - plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); - if (fillStyle) { - ctx.fillStyle = fillStyle; - plotLineArea(series.datapoints, series.xaxis, series.yaxis); - } - - if (lw > 0) - plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); - ctx.restore(); - } - - function drawSeriesPoints(series) { - function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - var x = points[i], y = points[i + 1]; - if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - continue; - - ctx.beginPath(); - x = axisx.p2c(x); - y = axisy.p2c(y) + offset; - if (symbol == "circle") - ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); - else - symbol(ctx, x, y, radius, shadow); - ctx.closePath(); - - if (fillStyle) { - ctx.fillStyle = fillStyle; - ctx.fill(); - } - ctx.stroke(); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var lw = series.points.lineWidth, - sw = series.shadowSize, - radius = series.points.radius, - symbol = series.points.symbol; - if (lw > 0 && sw > 0) { - // draw shadow in two steps - var w = sw / 2; - ctx.lineWidth = w; - ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPoints(series.datapoints, radius, null, w + w/2, true, - series.xaxis, series.yaxis, symbol); - - ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPoints(series.datapoints, radius, null, w/2, true, - series.xaxis, series.yaxis, symbol); - } - - ctx.lineWidth = lw; - ctx.strokeStyle = series.color; - plotPoints(series.datapoints, radius, - getFillStyle(series.points, series.color), 0, false, - series.xaxis, series.yaxis, symbol); - ctx.restore(); - } - - function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { - var left, right, bottom, top, - drawLeft, drawRight, drawTop, drawBottom, - tmp; - - // in horizontal mode, we start the bar from the left - // instead of from the bottom so it appears to be - // horizontal rather than vertical - if (horizontal) { - drawBottom = drawRight = drawTop = true; - drawLeft = false; - left = b; - right = x; - top = y + barLeft; - bottom = y + barRight; - - // account for negative bars - if (right < left) { - tmp = right; - right = left; - left = tmp; - drawLeft = true; - drawRight = false; - } - } - else { - drawLeft = drawRight = drawTop = true; - drawBottom = false; - left = x + barLeft; - right = x + barRight; - bottom = b; - top = y; - - // account for negative bars - if (top < bottom) { - tmp = top; - top = bottom; - bottom = tmp; - drawBottom = true; - drawTop = false; - } - } - - // clip - if (right < axisx.min || left > axisx.max || - top < axisy.min || bottom > axisy.max) - return; - - if (left < axisx.min) { - left = axisx.min; - drawLeft = false; - } - - if (right > axisx.max) { - right = axisx.max; - drawRight = false; - } - - if (bottom < axisy.min) { - bottom = axisy.min; - drawBottom = false; - } - - if (top > axisy.max) { - top = axisy.max; - drawTop = false; - } - - left = axisx.p2c(left); - bottom = axisy.p2c(bottom); - right = axisx.p2c(right); - top = axisy.p2c(top); - - // fill the bar - if (fillStyleCallback) { - c.beginPath(); - c.moveTo(left, bottom); - c.lineTo(left, top); - c.lineTo(right, top); - c.lineTo(right, bottom); - c.fillStyle = fillStyleCallback(bottom, top); - c.fill(); - } - - // draw outline - if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { - c.beginPath(); - - // FIXME: inline moveTo is buggy with excanvas - c.moveTo(left, bottom + offset); - if (drawLeft) - c.lineTo(left, top + offset); - else - c.moveTo(left, top + offset); - if (drawTop) - c.lineTo(right, top + offset); - else - c.moveTo(right, top + offset); - if (drawRight) - c.lineTo(right, bottom + offset); - else - c.moveTo(right, bottom + offset); - if (drawBottom) - c.lineTo(left, bottom + offset); - else - c.moveTo(left, bottom + offset); - c.stroke(); - } - } - - function drawSeriesBars(series) { - function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { - var points = datapoints.points, ps = datapoints.pointsize; - - for (var i = 0; i < points.length; i += ps) { - if (points[i] == null) - continue; - drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); - } - } - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - // FIXME: figure out a way to add shadows (for instance along the right edge) - ctx.lineWidth = series.bars.lineWidth; - ctx.strokeStyle = series.color; - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; - plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); - ctx.restore(); - } - - function getFillStyle(filloptions, seriesColor, bottom, top) { - var fill = filloptions.fill; - if (!fill) - return null; - - if (filloptions.fillColor) - return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); - - var c = $.color.parse(seriesColor); - c.a = typeof fill == "number" ? fill : 0.4; - c.normalize(); - return c.toString(); - } - - function insertLegend() { - placeholder.find(".legend").remove(); - - if (!options.legend.show) - return; - - var fragments = [], rowStarted = false, - lf = options.legend.labelFormatter, s, label; - for (var i = 0; i < series.length; ++i) { - s = series[i]; - label = s.label; - if (!label) - continue; - - if (i % options.legend.noColumns == 0) { - if (rowStarted) - fragments.push(''); - fragments.push(''); - rowStarted = true; - } - - if (lf) - label = lf(label, s); - - fragments.push( - '
' + - '' + label + ''); - } - if (rowStarted) - fragments.push(''); - - if (fragments.length == 0) - return; - - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - $(options.legend.container).html(table); - else { - var pos = "", - p = options.legend.position, - m = options.legend.margin; - if (m[0] == null) - m = [m, m]; - if (p.charAt(0) == "n") - pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - c = options.grid.backgroundColor; - if (c && typeof c == "string") - c = $.color.parse(c); - else - c = $.color.extract(legend, 'background-color'); - c.a = 1; - c = c.toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - } - } - } - - - // interactive features - - var highlights = [], - redrawTimeout = null; - - // returns the data item the mouse is over, or null if none is found - function findNearbyItem(mouseX, mouseY, seriesFilter) { - var maxDistance = options.grid.mouseActiveRadius, - smallestDistance = maxDistance * maxDistance + 1, - item = null, foundPoint = false, i, j; - - for (i = series.length - 1; i >= 0; --i) { - if (!seriesFilter(series[i])) - continue; - - var s = series[i], - axisx = s.xaxis, - axisy = s.yaxis, - points = s.datapoints.points, - ps = s.datapoints.pointsize, - mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster - my = axisy.c2p(mouseY), - maxx = maxDistance / axisx.scale, - maxy = maxDistance / axisy.scale; - - // with inverse transforms, we can't use the maxx/maxy - // optimization, sadly - if (axisx.options.inverseTransform) - maxx = Number.MAX_VALUE; - if (axisy.options.inverseTransform) - maxy = Number.MAX_VALUE; - - if (s.lines.show || s.points.show) { - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1]; - if (x == null) - continue; - - // For points and lines, the cursor must be within a - // certain distance to the data point - if (x - mx > maxx || x - mx < -maxx || - y - my > maxy || y - my < -maxy) - continue; - - // We have to calculate distances in pixels, not in - // data units, because the scales of the axes may be different - var dx = Math.abs(axisx.p2c(x) - mouseX), - dy = Math.abs(axisy.p2c(y) - mouseY), - dist = dx * dx + dy * dy; // we save the sqrt - - // use <= to ensure last point takes precedence - // (last generally means on top of) - if (dist < smallestDistance) { - smallestDistance = dist; - item = [i, j / ps]; - } - } - } - - if (s.bars.show && !item) { // no other point can be nearby - var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, - barRight = barLeft + s.bars.barWidth; - - for (j = 0; j < points.length; j += ps) { - var x = points[j], y = points[j + 1], b = points[j + 2]; - if (x == null) - continue; - - // for a bar graph, the cursor must be inside the bar - if (series[i].bars.horizontal ? - (mx <= Math.max(b, x) && mx >= Math.min(b, x) && - my >= y + barLeft && my <= y + barRight) : - (mx >= x + barLeft && mx <= x + barRight && - my >= Math.min(b, y) && my <= Math.max(b, y))) - item = [i, j / ps]; - } - } - } - - if (item) { - i = item[0]; - j = item[1]; - ps = series[i].datapoints.pointsize; - - return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), - dataIndex: j, - series: series[i], - seriesIndex: i }; - } - - return null; - } - - function onMouseMove(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return s["hoverable"] != false; }); - } - - function onMouseLeave(e) { - if (options.grid.hoverable) - triggerClickHoverEvent("plothover", e, - function (s) { return false; }); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e, - function (s) { return s["clickable"] != false; }); - } - - // trigger click or hover event (they send the same parameters - // so we share their code) - function triggerClickHoverEvent(eventname, event, seriesFilter) { - var offset = eventHolder.offset(), - canvasX = event.pageX - offset.left - plotOffset.left, - canvasY = event.pageY - offset.top - plotOffset.top, - pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); - - pos.pageX = event.pageX; - pos.pageY = event.pageY; - - var item = findNearbyItem(canvasX, canvasY, seriesFilter); - - if (item) { - // fill in mouse pos for any listeners out there - item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); - item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); - } - - if (options.grid.autoHighlight) { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && - !(item && h.series == item.series && - h.point[0] == item.datapoint[0] && - h.point[1] == item.datapoint[1])) - unhighlight(h.series, h.point); - } - - if (item) - highlight(item.series, item.datapoint, eventname); - } - - placeholder.trigger(eventname, [ pos, item ]); - } - - function triggerRedrawOverlay() { - if (!redrawTimeout) - redrawTimeout = setTimeout(drawOverlay, 30); - } - - function drawOverlay() { - redrawTimeout = null; - - // draw highlights - octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); - octx.translate(plotOffset.left, plotOffset.top); - - var i, hi; - for (i = 0; i < highlights.length; ++i) { - hi = highlights[i]; - - if (hi.series.bars.show) - drawBarHighlight(hi.series, hi.point); - else - drawPointHighlight(hi.series, hi.point); - } - octx.restore(); - - executeHooks(hooks.drawOverlay, [octx]); - } - - function highlight(s, point, auto) { - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") { - var ps = s.datapoints.pointsize; - point = s.datapoints.points.slice(ps * point, ps * (point + 1)); - } - - var i = indexOfHighlight(s, point); - if (i == -1) { - highlights.push({ series: s, point: point, auto: auto }); - - triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s, point) { - if (s == null && point == null) { - highlights = []; - triggerRedrawOverlay(); - } - - if (typeof s == "number") - s = series[s]; - - if (typeof point == "number") - point = s.data[point]; - - var i = indexOfHighlight(s, point); - if (i != -1) { - highlights.splice(i, 1); - - triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s, p) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s && h.point[0] == p[0] - && h.point[1] == p[1]) - return i; - } - return -1; - } - - function drawPointHighlight(series, point) { - var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis; - - if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) - return; - - var pointRadius = series.points.radius + series.points.lineWidth / 2; - octx.lineWidth = pointRadius; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var radius = 1.5 * pointRadius, - x = axisx.p2c(x), - y = axisy.p2c(y); - - octx.beginPath(); - if (series.points.symbol == "circle") - octx.arc(x, y, radius, 0, 2 * Math.PI, false); - else - series.points.symbol(octx, x, y, radius, false); - octx.closePath(); - octx.stroke(); - } - - function drawBarHighlight(series, point) { - octx.lineWidth = series.bars.lineWidth; - octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); - var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; - drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, - 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); - } - - function getColorOrGradient(spec, bottom, top, defaultColor) { - if (typeof spec == "string") - return spec; - else { - // assume this is a gradient spec; IE currently only - // supports a simple vertical gradient properly, so that's - // what we support too - var gradient = ctx.createLinearGradient(0, top, 0, bottom); - - for (var i = 0, l = spec.colors.length; i < l; ++i) { - var c = spec.colors[i]; - if (typeof c != "string") { - var co = $.color.parse(defaultColor); - if (c.brightness != null) - co = co.scale('rgb', c.brightness) - if (c.opacity != null) - co.a *= c.opacity; - c = co.toString(); - } - gradient.addColorStop(i / (l - 1), c); - } - - return gradient; - } - } - } - - $.plot = function(placeholder, data, options) { - //var t0 = new Date(); - var plot = new Plot($(placeholder), data, options, $.plot.plugins); - //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); - return plot; - }; - - $.plot.version = "0.7"; - - $.plot.plugins = []; - - // returns a string with the date d formatted according to fmt - $.plot.formatDate = function(d, fmt, monthNames) { - var leftPad = function(n) { - n = "" + n; - return n.length == 1 ? "0" + n : n; - }; - - var r = []; - var escape = false, padNext = false; - var hours = d.getUTCHours(); - var isAM = hours < 12; - if (monthNames == null) - monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - - if (fmt.search(/%p|%P/) != -1) { - if (hours > 12) { - hours = hours - 12; - } else if (hours == 0) { - hours = 12; - } - } - for (var i = 0; i < fmt.length; ++i) { - var c = fmt.charAt(i); - - if (escape) { - switch (c) { - case 'h': c = "" + hours; break; - case 'H': c = leftPad(hours); break; - case 'M': c = leftPad(d.getUTCMinutes()); break; - case 'S': c = leftPad(d.getUTCSeconds()); break; - case 'd': c = "" + d.getUTCDate(); break; - case 'm': c = "" + (d.getUTCMonth() + 1); break; - case 'y': c = "" + d.getUTCFullYear(); break; - case 'b': c = "" + monthNames[d.getUTCMonth()]; break; - case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; - case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; - case '0': c = ""; padNext = true; break; - } - if (c && padNext) { - c = leftPad(c); - padNext = false; - } - r.push(c); - if (!padNext) - escape = false; - } - else { - if (c == "%") - escape = true; - else - r.push(c); - } - } - return r.join(""); - }; - - // round to nearby lower multiple of base - function floorInBase(n, base) { - return base * Math.floor(n / base); - } - -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.min.js deleted file mode 100644 index 4467fc5d8cd3..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/* Javascript plotting library for jQuery, v. 0.7. - * - * Released under the MIT license by IOLA, December 2007. - * - */ -(function(b){b.color={};b.color.make=function(d,e,g,f){var c={};c.r=d||0;c.g=e||0;c.b=g||0;c.a=f!=null?f:1;c.add=function(h,j){for(var k=0;k=1){return"rgb("+[c.r,c.g,c.b].join(",")+")"}else{return"rgba("+[c.r,c.g,c.b,c.a].join(",")+")"}};c.normalize=function(){function h(k,j,l){return jl?l:j)}c.r=h(0,parseInt(c.r),255);c.g=h(0,parseInt(c.g),255);c.b=h(0,parseInt(c.b),255);c.a=h(0,c.a,1);return c};c.clone=function(){return b.color.make(c.r,c.b,c.g,c.a)};return c.normalize()};b.color.extract=function(d,e){var c;do{c=d.css(e).toLowerCase();if(c!=""&&c!="transparent"){break}d=d.parent()}while(!b.nodeName(d.get(0),"body"));if(c=="rgba(0, 0, 0, 0)"){c="transparent"}return b.color.parse(c)};b.color.parse=function(c){var d,f=b.color.make;if(d=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10))}if(d=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10),parseFloat(d[4]))}if(d=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55)}if(d=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55,parseFloat(d[4]))}if(d=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c)){return f(parseInt(d[1],16),parseInt(d[2],16),parseInt(d[3],16))}if(d=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c)){return f(parseInt(d[1]+d[1],16),parseInt(d[2]+d[2],16),parseInt(d[3]+d[3],16))}var e=b.trim(c).toLowerCase();if(e=="transparent"){return f(255,255,255,0)}else{d=a[e]||[0,0,0];return f(d[0],d[1],d[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function(c){function b(av,ai,J,af){var Q=[],O={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{show:null,position:"bottom",mode:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},az=null,ad=null,y=null,H=null,A=null,p=[],aw=[],q={left:0,right:0,top:0,bottom:0},G=0,I=0,h=0,w=0,ak={processOptions:[],processRawData:[],processDatapoints:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},aq=this;aq.setData=aj;aq.setupGrid=t;aq.draw=W;aq.getPlaceholder=function(){return av};aq.getCanvas=function(){return az};aq.getPlotOffset=function(){return q};aq.width=function(){return h};aq.height=function(){return w};aq.offset=function(){var aB=y.offset();aB.left+=q.left;aB.top+=q.top;return aB};aq.getData=function(){return Q};aq.getAxes=function(){var aC={},aB;c.each(p.concat(aw),function(aD,aE){if(aE){aC[aE.direction+(aE.n!=1?aE.n:"")+"axis"]=aE}});return aC};aq.getXAxes=function(){return p};aq.getYAxes=function(){return aw};aq.c2p=C;aq.p2c=ar;aq.getOptions=function(){return O};aq.highlight=x;aq.unhighlight=T;aq.triggerRedrawOverlay=f;aq.pointOffset=function(aB){return{left:parseInt(p[aA(aB,"x")-1].p2c(+aB.x)+q.left),top:parseInt(aw[aA(aB,"y")-1].p2c(+aB.y)+q.top)}};aq.shutdown=ag;aq.resize=function(){B();g(az);g(ad)};aq.hooks=ak;F(aq);Z(J);X();aj(ai);t();W();ah();function an(aD,aB){aB=[aq].concat(aB);for(var aC=0;aC=O.colors.length){aG=0;++aF}}var aH=0,aN;for(aG=0;aGa3.datamax&&a1!=aB){a3.datamax=a1}}c.each(m(),function(a1,a2){a2.datamin=aO;a2.datamax=aI;a2.used=false});for(aU=0;aU0&&aT[aR-aP]!=null&&aT[aR-aP]!=aT[aR]&&aT[aR-aP+1]!=aT[aR+1]){for(aN=0;aNaM){aM=a0}}if(aX.y){if(a0aV){aV=a0}}}}if(aJ.bars.show){var aY=aJ.bars.align=="left"?0:-aJ.bars.barWidth/2;if(aJ.bars.horizontal){aQ+=aY;aV+=aY+aJ.bars.barWidth}else{aK+=aY;aM+=aY+aJ.bars.barWidth}}aF(aJ.xaxis,aK,aM);aF(aJ.yaxis,aQ,aV)}c.each(m(),function(a1,a2){if(a2.datamin==aO){a2.datamin=null}if(a2.datamax==aI){a2.datamax=null}})}function j(aB,aC){var aD=document.createElement("canvas");aD.className=aC;aD.width=G;aD.height=I;if(!aB){c(aD).css({position:"absolute",left:0,top:0})}c(aD).appendTo(av);if(!aD.getContext){aD=window.G_vmlCanvasManager.initElement(aD)}aD.getContext("2d").save();return aD}function B(){G=av.width();I=av.height();if(G<=0||I<=0){throw"Invalid dimensions for plot, width = "+G+", height = "+I}}function g(aC){if(aC.width!=G){aC.width=G}if(aC.height!=I){aC.height=I}var aB=aC.getContext("2d");aB.restore();aB.save()}function X(){var aC,aB=av.children("canvas.base"),aD=av.children("canvas.overlay");if(aB.length==0||aD==0){av.html("");av.css({padding:0});if(av.css("position")=="static"){av.css("position","relative")}B();az=j(true,"base");ad=j(false,"overlay");aC=false}else{az=aB.get(0);ad=aD.get(0);aC=true}H=az.getContext("2d");A=ad.getContext("2d");y=c([ad,az]);if(aC){av.data("plot").shutdown();aq.resize();A.clearRect(0,0,G,I);y.unbind();av.children().not([az,ad]).remove()}av.data("plot",aq)}function ah(){if(O.grid.hoverable){y.mousemove(aa);y.mouseleave(l)}if(O.grid.clickable){y.click(R)}an(ak.bindEvents,[y])}function ag(){if(M){clearTimeout(M)}y.unbind("mousemove",aa);y.unbind("mouseleave",l);y.unbind("click",R);an(ak.shutdown,[y])}function r(aG){function aC(aH){return aH}var aF,aB,aD=aG.options.transform||aC,aE=aG.options.inverseTransform;if(aG.direction=="x"){aF=aG.scale=h/Math.abs(aD(aG.max)-aD(aG.min));aB=Math.min(aD(aG.max),aD(aG.min))}else{aF=aG.scale=w/Math.abs(aD(aG.max)-aD(aG.min));aF=-aF;aB=Math.max(aD(aG.max),aD(aG.min))}if(aD==aC){aG.p2c=function(aH){return(aH-aB)*aF}}else{aG.p2c=function(aH){return(aD(aH)-aB)*aF}}if(!aE){aG.c2p=function(aH){return aB+aH/aF}}else{aG.c2p=function(aH){return aE(aB+aH/aF)}}}function L(aD){var aB=aD.options,aF,aJ=aD.ticks||[],aI=[],aE,aK=aB.labelWidth,aG=aB.labelHeight,aC;function aH(aM,aL){return c('
'+aM.join("")+"
").appendTo(av)}if(aD.direction=="x"){if(aK==null){aK=Math.floor(G/(aJ.length>0?aJ.length:1))}if(aG==null){aI=[];for(aF=0;aF'+aE+"")}}if(aI.length>0){aI.push('
');aC=aH(aI,"width:10000px;");aG=aC.height();aC.remove()}}}else{if(aK==null||aG==null){for(aF=0;aF'+aE+"")}}if(aI.length>0){aC=aH(aI,"");if(aK==null){aK=aC.children().width()}if(aG==null){aG=aC.find("div.tickLabel").height()}aC.remove()}}}if(aK==null){aK=0}if(aG==null){aG=0}aD.labelWidth=aK;aD.labelHeight=aG}function au(aD){var aC=aD.labelWidth,aL=aD.labelHeight,aH=aD.options.position,aF=aD.options.tickLength,aG=O.grid.axisMargin,aJ=O.grid.labelMargin,aK=aD.direction=="x"?p:aw,aE;var aB=c.grep(aK,function(aN){return aN&&aN.options.position==aH&&aN.reserveSpace});if(c.inArray(aD,aB)==aB.length-1){aG=0}if(aF==null){aF="full"}var aI=c.grep(aK,function(aN){return aN&&aN.reserveSpace});var aM=c.inArray(aD,aI)==0;if(!aM&&aF=="full"){aF=5}if(!isNaN(+aF)){aJ+=+aF}if(aD.direction=="x"){aL+=aJ;if(aH=="bottom"){q.bottom+=aL+aG;aD.box={top:I-q.bottom,height:aL}}else{aD.box={top:q.top+aG,height:aL};q.top+=aL+aG}}else{aC+=aJ;if(aH=="left"){aD.box={left:q.left+aG,width:aC};q.left+=aC+aG}else{q.right+=aC+aG;aD.box={left:G-q.right,width:aC}}}aD.position=aH;aD.tickLength=aF;aD.box.padding=aJ;aD.innermost=aM}function U(aB){if(aB.direction=="x"){aB.box.left=q.left;aB.box.width=h}else{aB.box.top=q.top;aB.box.height=w}}function t(){var aC,aE=m();c.each(aE,function(aF,aG){aG.show=aG.options.show;if(aG.show==null){aG.show=aG.used}aG.reserveSpace=aG.show||aG.options.reserveSpace;n(aG)});allocatedAxes=c.grep(aE,function(aF){return aF.reserveSpace});q.left=q.right=q.top=q.bottom=0;if(O.grid.show){c.each(allocatedAxes,function(aF,aG){S(aG);P(aG);ap(aG,aG.ticks);L(aG)});for(aC=allocatedAxes.length-1;aC>=0;--aC){au(allocatedAxes[aC])}var aD=O.grid.minBorderMargin;if(aD==null){aD=0;for(aC=0;aC=0){aD=0}}if(aF.max==null){aB+=aH*aG;if(aB>0&&aE.datamax!=null&&aE.datamax<=0){aB=0}}}}aE.min=aD;aE.max=aB}function S(aG){var aM=aG.options;var aH;if(typeof aM.ticks=="number"&&aM.ticks>0){aH=aM.ticks}else{aH=0.3*Math.sqrt(aG.direction=="x"?G:I)}var aT=(aG.max-aG.min)/aH,aO,aB,aN,aR,aS,aQ,aI;if(aM.mode=="time"){var aJ={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var aK=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var aC=0;if(aM.minTickSize!=null){if(typeof aM.tickSize=="number"){aC=aM.tickSize}else{aC=aM.minTickSize[0]*aJ[aM.minTickSize[1]]}}for(var aS=0;aS=aC){break}}aO=aK[aS][0];aN=aK[aS][1];if(aN=="year"){aQ=Math.pow(10,Math.floor(Math.log(aT/aJ.year)/Math.LN10));aI=(aT/aJ.year)/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ}aG.tickSize=aM.tickSize||[aO,aN];aB=function(aX){var a2=[],a0=aX.tickSize[0],a3=aX.tickSize[1],a1=new Date(aX.min);var aW=a0*aJ[a3];if(a3=="second"){a1.setUTCSeconds(a(a1.getUTCSeconds(),a0))}if(a3=="minute"){a1.setUTCMinutes(a(a1.getUTCMinutes(),a0))}if(a3=="hour"){a1.setUTCHours(a(a1.getUTCHours(),a0))}if(a3=="month"){a1.setUTCMonth(a(a1.getUTCMonth(),a0))}if(a3=="year"){a1.setUTCFullYear(a(a1.getUTCFullYear(),a0))}a1.setUTCMilliseconds(0);if(aW>=aJ.minute){a1.setUTCSeconds(0)}if(aW>=aJ.hour){a1.setUTCMinutes(0)}if(aW>=aJ.day){a1.setUTCHours(0)}if(aW>=aJ.day*4){a1.setUTCDate(1)}if(aW>=aJ.year){a1.setUTCMonth(0)}var a5=0,a4=Number.NaN,aY;do{aY=a4;a4=a1.getTime();a2.push(a4);if(a3=="month"){if(a0<1){a1.setUTCDate(1);var aV=a1.getTime();a1.setUTCMonth(a1.getUTCMonth()+1);var aZ=a1.getTime();a1.setTime(a4+a5*aJ.hour+(aZ-aV)*a0);a5=a1.getUTCHours();a1.setUTCHours(0)}else{a1.setUTCMonth(a1.getUTCMonth()+a0)}}else{if(a3=="year"){a1.setUTCFullYear(a1.getUTCFullYear()+a0)}else{a1.setTime(a4+aW)}}}while(a4aU){aP=aU}aQ=Math.pow(10,-aP);aI=aT/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2;if(aI>2.25&&(aU==null||aP+1<=aU)){aO=2.5;++aP}}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ;if(aM.minTickSize!=null&&aO0){if(aM.min==null){aG.min=Math.min(aG.min,aL[0])}if(aM.max==null&&aL.length>1){aG.max=Math.max(aG.max,aL[aL.length-1])}}aB=function(aX){var aY=[],aV,aW;for(aW=0;aW1&&/\..*0$/.test((aD[1]-aD[0]).toFixed(aE)))){aG.tickDecimals=aE}}}}aG.tickGenerator=aB;if(c.isFunction(aM.tickFormatter)){aG.tickFormatter=function(aV,aW){return""+aM.tickFormatter(aV,aW)}}else{aG.tickFormatter=aR}}function P(aF){var aH=aF.options.ticks,aG=[];if(aH==null||(typeof aH=="number"&&aH>0)){aG=aF.tickGenerator(aF)}else{if(aH){if(c.isFunction(aH)){aG=aH({min:aF.min,max:aF.max})}else{aG=aH}}}var aE,aB;aF.ticks=[];for(aE=0;aE1){aC=aD[1]}}else{aB=+aD}if(aC==null){aC=aF.tickFormatter(aB,aF)}if(!isNaN(aB)){aF.ticks.push({v:aB,label:aC})}}}function ap(aB,aC){if(aB.options.autoscaleMargin&&aC.length>0){if(aB.options.min==null){aB.min=Math.min(aB.min,aC[0].v)}if(aB.options.max==null&&aC.length>1){aB.max=Math.max(aB.max,aC[aC.length-1].v)}}}function W(){H.clearRect(0,0,G,I);var aC=O.grid;if(aC.show&&aC.backgroundColor){N()}if(aC.show&&!aC.aboveData){ac()}for(var aB=0;aBaG){var aC=aH;aH=aG;aG=aC}return{from:aH,to:aG,axis:aE}}function N(){H.save();H.translate(q.left,q.top);H.fillStyle=am(O.grid.backgroundColor,w,0,"rgba(255, 255, 255, 0)");H.fillRect(0,0,h,w);H.restore()}function ac(){var aF;H.save();H.translate(q.left,q.top);var aH=O.grid.markings;if(aH){if(c.isFunction(aH)){var aK=aq.getAxes();aK.xmin=aK.xaxis.min;aK.xmax=aK.xaxis.max;aK.ymin=aK.yaxis.min;aK.ymax=aK.yaxis.max;aH=aH(aK)}for(aF=0;aFaC.axis.max||aI.toaI.axis.max){continue}aC.from=Math.max(aC.from,aC.axis.min);aC.to=Math.min(aC.to,aC.axis.max);aI.from=Math.max(aI.from,aI.axis.min);aI.to=Math.min(aI.to,aI.axis.max);if(aC.from==aC.to&&aI.from==aI.to){continue}aC.from=aC.axis.p2c(aC.from);aC.to=aC.axis.p2c(aC.to);aI.from=aI.axis.p2c(aI.from);aI.to=aI.axis.p2c(aI.to);if(aC.from==aC.to||aI.from==aI.to){H.beginPath();H.strokeStyle=aD.color||O.grid.markingsColor;H.lineWidth=aD.lineWidth||O.grid.markingsLineWidth;H.moveTo(aC.from,aI.from);H.lineTo(aC.to,aI.to);H.stroke()}else{H.fillStyle=aD.color||O.grid.markingsColor;H.fillRect(aC.from,aI.to,aC.to-aC.from,aI.from-aI.to)}}}var aK=m(),aM=O.grid.borderWidth;for(var aE=0;aEaB.max||(aQ=="full"&&aM>0&&(aO==aB.min||aO==aB.max))){continue}if(aB.direction=="x"){aN=aB.p2c(aO);aJ=aQ=="full"?-w:aQ;if(aB.position=="top"){aJ=-aJ}}else{aL=aB.p2c(aO);aP=aQ=="full"?-h:aQ;if(aB.position=="left"){aP=-aP}}if(H.lineWidth==1){if(aB.direction=="x"){aN=Math.floor(aN)+0.5}else{aL=Math.floor(aL)+0.5}}H.moveTo(aN,aL);H.lineTo(aN+aP,aL+aJ)}H.stroke()}if(aM){H.lineWidth=aM;H.strokeStyle=O.grid.borderColor;H.strokeRect(-aM/2,-aM/2,h+aM,w+aM)}H.restore()}function k(){av.find(".tickLabels").remove();var aG=['
'];var aJ=m();for(var aD=0;aD');for(var aE=0;aEaC.max){continue}var aK={},aI;if(aC.direction=="x"){aI="center";aK.left=Math.round(q.left+aC.p2c(aH.v)-aC.labelWidth/2);if(aC.position=="bottom"){aK.top=aF.top+aF.padding}else{aK.bottom=I-(aF.top+aF.height-aF.padding)}}else{aK.top=Math.round(q.top+aC.p2c(aH.v)-aC.labelHeight/2);if(aC.position=="left"){aK.right=G-(aF.left+aF.width-aF.padding);aI="right"}else{aK.left=aF.left+aF.padding;aI="left"}}aK.width=aC.labelWidth;var aB=["position:absolute","text-align:"+aI];for(var aL in aK){aB.push(aL+":"+aK[aL]+"px")}aG.push('
'+aH.label+"
")}aG.push("
")}aG.push("");av.append(aG.join(""))}function d(aB){if(aB.lines.show){at(aB)}if(aB.bars.show){e(aB)}if(aB.points.show){ao(aB)}}function at(aE){function aD(aP,aQ,aI,aU,aT){var aV=aP.points,aJ=aP.pointsize,aN=null,aM=null;H.beginPath();for(var aO=aJ;aO=aR&&aS>aT.max){if(aR>aT.max){continue}aL=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aS=aT.max}else{if(aR>=aS&&aR>aT.max){if(aS>aT.max){continue}aK=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aR=aT.max}}if(aL<=aK&&aL=aK&&aL>aU.max){if(aK>aU.max){continue}aS=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aL=aU.max}else{if(aK>=aL&&aK>aU.max){if(aL>aU.max){continue}aR=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aK=aU.max}}if(aL!=aN||aS!=aM){H.moveTo(aU.p2c(aL)+aQ,aT.p2c(aS)+aI)}aN=aK;aM=aR;H.lineTo(aU.p2c(aK)+aQ,aT.p2c(aR)+aI)}H.stroke()}function aF(aI,aQ,aP){var aW=aI.points,aV=aI.pointsize,aN=Math.min(Math.max(0,aP.min),aP.max),aX=0,aU,aT=false,aM=1,aL=0,aR=0;while(true){if(aV>0&&aX>aW.length+aV){break}aX+=aV;var aZ=aW[aX-aV],aK=aW[aX-aV+aM],aY=aW[aX],aJ=aW[aX+aM];if(aT){if(aV>0&&aZ!=null&&aY==null){aR=aX;aV=-aV;aM=2;continue}if(aV<0&&aX==aL+aV){H.fill();aT=false;aV=-aV;aM=1;aX=aL=aR+aV;continue}}if(aZ==null||aY==null){continue}if(aZ<=aY&&aZ=aY&&aZ>aQ.max){if(aY>aQ.max){continue}aK=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aZ=aQ.max}else{if(aY>=aZ&&aY>aQ.max){if(aZ>aQ.max){continue}aJ=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aY=aQ.max}}if(!aT){H.beginPath();H.moveTo(aQ.p2c(aZ),aP.p2c(aN));aT=true}if(aK>=aP.max&&aJ>=aP.max){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.max));H.lineTo(aQ.p2c(aY),aP.p2c(aP.max));continue}else{if(aK<=aP.min&&aJ<=aP.min){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.min));H.lineTo(aQ.p2c(aY),aP.p2c(aP.min));continue}}var aO=aZ,aS=aY;if(aK<=aJ&&aK=aP.min){aZ=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.min}else{if(aJ<=aK&&aJ=aP.min){aY=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.min}}if(aK>=aJ&&aK>aP.max&&aJ<=aP.max){aZ=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.max}else{if(aJ>=aK&&aJ>aP.max&&aK<=aP.max){aY=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.max}}if(aZ!=aO){H.lineTo(aQ.p2c(aO),aP.p2c(aK))}H.lineTo(aQ.p2c(aZ),aP.p2c(aK));H.lineTo(aQ.p2c(aY),aP.p2c(aJ));if(aY!=aS){H.lineTo(aQ.p2c(aY),aP.p2c(aJ));H.lineTo(aQ.p2c(aS),aP.p2c(aJ))}}}H.save();H.translate(q.left,q.top);H.lineJoin="round";var aG=aE.lines.lineWidth,aB=aE.shadowSize;if(aG>0&&aB>0){H.lineWidth=aB;H.strokeStyle="rgba(0,0,0,0.1)";var aH=Math.PI/18;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/2),Math.cos(aH)*(aG/2+aB/2),aE.xaxis,aE.yaxis);H.lineWidth=aB/2;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/4),Math.cos(aH)*(aG/2+aB/4),aE.xaxis,aE.yaxis)}H.lineWidth=aG;H.strokeStyle=aE.color;var aC=ae(aE.lines,aE.color,0,w);if(aC){H.fillStyle=aC;aF(aE.datapoints,aE.xaxis,aE.yaxis)}if(aG>0){aD(aE.datapoints,0,0,aE.xaxis,aE.yaxis)}H.restore()}function ao(aE){function aH(aN,aM,aU,aK,aS,aT,aQ,aJ){var aR=aN.points,aI=aN.pointsize;for(var aL=0;aLaT.max||aOaQ.max){continue}H.beginPath();aP=aT.p2c(aP);aO=aQ.p2c(aO)+aK;if(aJ=="circle"){H.arc(aP,aO,aM,0,aS?Math.PI:Math.PI*2,false)}else{aJ(H,aP,aO,aM,aS)}H.closePath();if(aU){H.fillStyle=aU;H.fill()}H.stroke()}}H.save();H.translate(q.left,q.top);var aG=aE.points.lineWidth,aC=aE.shadowSize,aB=aE.points.radius,aF=aE.points.symbol;if(aG>0&&aC>0){var aD=aC/2;H.lineWidth=aD;H.strokeStyle="rgba(0,0,0,0.1)";aH(aE.datapoints,aB,null,aD+aD/2,true,aE.xaxis,aE.yaxis,aF);H.strokeStyle="rgba(0,0,0,0.2)";aH(aE.datapoints,aB,null,aD/2,true,aE.xaxis,aE.yaxis,aF)}H.lineWidth=aG;H.strokeStyle=aE.color;aH(aE.datapoints,aB,ae(aE.points,aE.color),0,false,aE.xaxis,aE.yaxis,aF);H.restore()}function E(aN,aM,aV,aI,aQ,aF,aD,aL,aK,aU,aR,aC){var aE,aT,aJ,aP,aG,aB,aO,aH,aS;if(aR){aH=aB=aO=true;aG=false;aE=aV;aT=aN;aP=aM+aI;aJ=aM+aQ;if(aTaL.max||aPaK.max){return}if(aEaL.max){aT=aL.max;aB=false}if(aJaK.max){aP=aK.max;aO=false}aE=aL.p2c(aE);aJ=aK.p2c(aJ);aT=aL.p2c(aT);aP=aK.p2c(aP);if(aD){aU.beginPath();aU.moveTo(aE,aJ);aU.lineTo(aE,aP);aU.lineTo(aT,aP);aU.lineTo(aT,aJ);aU.fillStyle=aD(aJ,aP);aU.fill()}if(aC>0&&(aG||aB||aO||aH)){aU.beginPath();aU.moveTo(aE,aJ+aF);if(aG){aU.lineTo(aE,aP+aF)}else{aU.moveTo(aE,aP+aF)}if(aO){aU.lineTo(aT,aP+aF)}else{aU.moveTo(aT,aP+aF)}if(aB){aU.lineTo(aT,aJ+aF)}else{aU.moveTo(aT,aJ+aF)}if(aH){aU.lineTo(aE,aJ+aF)}else{aU.moveTo(aE,aJ+aF)}aU.stroke()}}function e(aD){function aC(aJ,aI,aL,aG,aK,aN,aM){var aO=aJ.points,aF=aJ.pointsize;for(var aH=0;aH")}aH.push("");aF=true}if(aN){aJ=aN(aJ,aM)}aH.push('
'+aJ+"")}if(aF){aH.push("")}if(aH.length==0){return}var aL=''+aH.join("")+"
";if(O.legend.container!=null){c(O.legend.container).html(aL)}else{var aI="",aC=O.legend.position,aD=O.legend.margin;if(aD[0]==null){aD=[aD,aD]}if(aC.charAt(0)=="n"){aI+="top:"+(aD[1]+q.top)+"px;"}else{if(aC.charAt(0)=="s"){aI+="bottom:"+(aD[1]+q.bottom)+"px;"}}if(aC.charAt(1)=="e"){aI+="right:"+(aD[0]+q.right)+"px;"}else{if(aC.charAt(1)=="w"){aI+="left:"+(aD[0]+q.left)+"px;"}}var aK=c('
'+aL.replace('style="','style="position:absolute;'+aI+";")+"
").appendTo(av);if(O.legend.backgroundOpacity!=0){var aG=O.legend.backgroundColor;if(aG==null){aG=O.grid.backgroundColor;if(aG&&typeof aG=="string"){aG=c.color.parse(aG)}else{aG=c.color.extract(aK,"background-color")}aG.a=1;aG=aG.toString()}var aB=aK.children();c('
').prependTo(aK).css("opacity",O.legend.backgroundOpacity)}}}var ab=[],M=null;function K(aI,aG,aD){var aO=O.grid.mouseActiveRadius,a0=aO*aO+1,aY=null,aR=false,aW,aU;for(aW=Q.length-1;aW>=0;--aW){if(!aD(Q[aW])){continue}var aP=Q[aW],aH=aP.xaxis,aF=aP.yaxis,aV=aP.datapoints.points,aT=aP.datapoints.pointsize,aQ=aH.c2p(aI),aN=aF.c2p(aG),aC=aO/aH.scale,aB=aO/aF.scale;if(aH.options.inverseTransform){aC=Number.MAX_VALUE}if(aF.options.inverseTransform){aB=Number.MAX_VALUE}if(aP.lines.show||aP.points.show){for(aU=0;aUaC||aK-aQ<-aC||aJ-aN>aB||aJ-aN<-aB){continue}var aM=Math.abs(aH.p2c(aK)-aI),aL=Math.abs(aF.p2c(aJ)-aG),aS=aM*aM+aL*aL;if(aS=Math.min(aZ,aK)&&aN>=aJ+aE&&aN<=aJ+aX):(aQ>=aK+aE&&aQ<=aK+aX&&aN>=Math.min(aZ,aJ)&&aN<=Math.max(aZ,aJ))){aY=[aW,aU/aT]}}}}if(aY){aW=aY[0];aU=aY[1];aT=Q[aW].datapoints.pointsize;return{datapoint:Q[aW].datapoints.points.slice(aU*aT,(aU+1)*aT),dataIndex:aU,series:Q[aW],seriesIndex:aW}}return null}function aa(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return aC.hoverable!=false})}}function l(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return false})}}function R(aB){u("plotclick",aB,function(aC){return aC.clickable!=false})}function u(aC,aB,aD){var aE=y.offset(),aH=aB.pageX-aE.left-q.left,aF=aB.pageY-aE.top-q.top,aJ=C({left:aH,top:aF});aJ.pageX=aB.pageX;aJ.pageY=aB.pageY;var aK=K(aH,aF,aD);if(aK){aK.pageX=parseInt(aK.series.xaxis.p2c(aK.datapoint[0])+aE.left+q.left);aK.pageY=parseInt(aK.series.yaxis.p2c(aK.datapoint[1])+aE.top+q.top)}if(O.grid.autoHighlight){for(var aG=0;aGaH.max||aIaG.max){return}var aF=aE.points.radius+aE.points.lineWidth/2;A.lineWidth=aF;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aB=1.5*aF,aC=aH.p2c(aC),aI=aG.p2c(aI);A.beginPath();if(aE.points.symbol=="circle"){A.arc(aC,aI,aB,0,2*Math.PI,false)}else{aE.points.symbol(A,aC,aI,aB,false)}A.closePath();A.stroke()}function v(aE,aB){A.lineWidth=aE.bars.lineWidth;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aD=c.color.parse(aE.color).scale("a",0.5).toString();var aC=aE.bars.align=="left"?0:-aE.bars.barWidth/2;E(aB[0],aB[1],aB[2]||0,aC,aC+aE.bars.barWidth,0,function(){return aD},aE.xaxis,aE.yaxis,A,aE.bars.horizontal,aE.bars.lineWidth)}function am(aJ,aB,aH,aC){if(typeof aJ=="string"){return aJ}else{var aI=H.createLinearGradient(0,aH,0,aB);for(var aE=0,aD=aJ.colors.length;aE12){n=n-12}else{if(n==0){n=12}}}for(var g=0;g0&&L.which!=M.which)||E(L.target).is(M.not)){return }}switch(L.type){case"mousedown":E.extend(M,E(K).offset(),{elem:K,target:L.target,pageX:L.pageX,pageY:L.pageY});A.add(document,"mousemove mouseup",H,M);G(K,false);F.dragging=null;return false;case !F.dragging&&"mousemove":if(I(L.pageX-M.pageX)+I(L.pageY-M.pageY) max) { - // make sure min < max - var tmp = min; - min = max; - max = tmp; - } - - var range = max - min; - if (zr && - ((zr[0] != null && range < zr[0]) || - (zr[1] != null && range > zr[1]))) - return; - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotzoom", [ plot ]); - } - - plot.pan = function (args) { - var delta = { - x: +args.left, - y: +args.top - }; - - if (isNaN(delta.x)) - delta.x = 0; - if (isNaN(delta.y)) - delta.y = 0; - - $.each(plot.getAxes(), function (_, axis) { - var opts = axis.options, - min, max, d = delta[axis.direction]; - - min = axis.c2p(axis.p2c(axis.min) + d), - max = axis.c2p(axis.p2c(axis.max) + d); - - var pr = opts.panRange; - if (pr === false) // no panning on this axis - return; - - if (pr) { - // check whether we hit the wall - if (pr[0] != null && pr[0] > min) { - d = pr[0] - min; - min += d; - max += d; - } - - if (pr[1] != null && pr[1] < max) { - d = pr[1] - max; - min += d; - max += d; - } - } - - opts.min = min; - opts.max = max; - }); - - plot.setupGrid(); - plot.draw(); - - if (!args.preventEvent) - plot.getPlaceholder().trigger("plotpan", [ plot ]); - } - - function shutdown(plot, eventHolder) { - eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); - eventHolder.unbind("mousewheel", onMouseWheel); - eventHolder.unbind("dragstart", onDragStart); - eventHolder.unbind("drag", onDrag); - eventHolder.unbind("dragend", onDragEnd); - if (panTimeout) - clearTimeout(panTimeout); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'navigate', - version: '1.3' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js deleted file mode 100644 index ecf63c93ba5b..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.navigate.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(i){i.fn.drag=function(j,k,l){if(k){this.bind("dragstart",j)}if(l){this.bind("dragend",l)}return !j?this.trigger("drag"):this.bind("drag",k?k:j)};var d=i.event,c=d.special,h=c.drag={not:":input",distance:0,which:1,dragging:false,setup:function(j){j=i.extend({distance:h.distance,which:h.which,not:h.not},j||{});j.distance=e(j.distance);d.add(this,"mousedown",f,j);if(this.attachEvent){this.attachEvent("ondragstart",a)}},teardown:function(){d.remove(this,"mousedown",f);if(this===h.dragging){h.dragging=h.proxy=false}g(this,true);if(this.detachEvent){this.detachEvent("ondragstart",a)}}};c.dragstart=c.dragend={setup:function(){},teardown:function(){}};function f(j){var k=this,l,m=j.data||{};if(m.elem){k=j.dragTarget=m.elem;j.dragProxy=h.proxy||k;j.cursorOffsetX=m.pageX-m.left;j.cursorOffsetY=m.pageY-m.top;j.offsetX=j.pageX-j.cursorOffsetX;j.offsetY=j.pageY-j.cursorOffsetY}else{if(h.dragging||(m.which>0&&j.which!=m.which)||i(j.target).is(m.not)){return}}switch(j.type){case"mousedown":i.extend(m,i(k).offset(),{elem:k,target:j.target,pageX:j.pageX,pageY:j.pageY});d.add(document,"mousemove mouseup",f,m);g(k,false);h.dragging=null;return false;case !h.dragging&&"mousemove":if(e(j.pageX-m.pageX)+e(j.pageY-m.pageY)w){var A=B;B=w;w=A}var y=w-B;if(E&&((E[0]!=null&&yE[1]))){return}D.min=B;D.max=w});o.setupGrid();o.draw();if(!q.preventEvent){o.getPlaceholder().trigger("plotzoom",[o])}};o.pan=function(p){var q={x:+p.left,y:+p.top};if(isNaN(q.x)){q.x=0}if(isNaN(q.y)){q.y=0}b.each(o.getAxes(),function(s,u){var v=u.options,t,r,w=q[u.direction];t=u.c2p(u.p2c(u.min)+w),r=u.c2p(u.p2c(u.max)+w);var x=v.panRange;if(x===false){return}if(x){if(x[0]!=null&&x[0]>t){w=x[0]-t;t+=w;r+=w}if(x[1]!=null&&x[1]1) - options.series.pie.tilt=1; - if (options.series.pie.tilt<0) - options.series.pie.tilt=0; - - // add processData hook to do transformations on the data - plot.hooks.processDatapoints.push(processDatapoints); - plot.hooks.drawOverlay.push(drawOverlay); - - // add draw hook - plot.hooks.draw.push(draw); - } - } - - // bind hoverable events - function bindEvents(plot, eventHolder) - { - var options = plot.getOptions(); - - if (options.series.pie.show && options.grid.hoverable) - eventHolder.unbind('mousemove').mousemove(onMouseMove); - - if (options.series.pie.show && options.grid.clickable) - eventHolder.unbind('click').click(onClick); - } - - - // debugging function that prints out an object - function alertObject(obj) - { - var msg = ''; - function traverse(obj, depth) - { - if (!depth) - depth = 0; - for (var i = 0; i < obj.length; ++i) - { - for (var j=0; jcanvas.width-maxRadius) - centerLeft = canvas.width-maxRadius; - } - - function fixData(data) - { - for (var i = 0; i < data.length; ++i) - { - if (typeof(data[i].data)=='number') - data[i].data = [[1,data[i].data]]; - else if (typeof(data[i].data)=='undefined' || typeof(data[i].data[0])=='undefined') - { - if (typeof(data[i].data)!='undefined' && typeof(data[i].data.label)!='undefined') - data[i].label = data[i].data.label; // fix weirdness coming from flot - data[i].data = [[1,0]]; - - } - } - return data; - } - - function combine(data) - { - data = fixData(data); - calcTotal(data); - var combined = 0; - var numCombined = 0; - var color = options.series.pie.combine.color; - - var newdata = []; - for (var i = 0; i < data.length; ++i) - { - // make sure its a number - data[i].data[0][1] = parseFloat(data[i].data[0][1]); - if (!data[i].data[0][1]) - data[i].data[0][1] = 0; - - if (data[i].data[0][1]/total<=options.series.pie.combine.threshold) - { - combined += data[i].data[0][1]; - numCombined++; - if (!color) - color = data[i].color; - } - else - { - newdata.push({ - data: [[1,data[i].data[0][1]]], - color: data[i].color, - label: data[i].label, - angle: (data[i].data[0][1]*(Math.PI*2))/total, - percent: (data[i].data[0][1]/total*100) - }); - } - } - if (numCombined>0) - newdata.push({ - data: [[1,combined]], - color: color, - label: options.series.pie.combine.label, - angle: (combined*(Math.PI*2))/total, - percent: (combined/total*100) - }); - return newdata; - } - - function draw(plot, newCtx) - { - if (!target) return; // if no series were passed - ctx = newCtx; - - setupPie(); - var slices = plot.getData(); - - var attempts = 0; - while (redraw && attempts0) - maxRadius *= shrink; - attempts += 1; - clear(); - if (options.series.pie.tilt<=0.8) - drawShadow(); - drawPie(); - } - if (attempts >= redrawAttempts) { - clear(); - target.prepend('
Could not draw pie with labels contained inside canvas
'); - } - - if ( plot.setSeries && plot.insertLegend ) - { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() - { - ctx.clearRect(0,0,canvas.width,canvas.height); - target.children().filter('.pieLabel, .pieLabelBackground').remove(); - } - - function drawShadow() - { - var shadowLeft = 5; - var shadowTop = 15; - var edge = 10; - var alpha = 0.02; - - // set radius - if (options.series.pie.radius>1) - var radius = options.series.pie.radius; - else - var radius = maxRadius * options.series.pie.radius; - - if (radius>=(canvas.width/2)-shadowLeft || radius*options.series.pie.tilt>=(canvas.height/2)-shadowTop || radius<=edge) - return; // shadow would be outside canvas, so don't draw it - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = '#000'; - - // center and rotate to starting position - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - for (var i=1; i<=edge; i++) - { - ctx.beginPath(); - ctx.arc(0,0,radius,0,Math.PI*2,false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() - { - startAngle = Math.PI*options.series.pie.startAngle; - - // set radius - if (options.series.pie.radius>1) - var radius = options.series.pie.radius; - else - var radius = maxRadius * options.series.pie.radius; - - // center and rotate to starting position - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) - { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - ctx.restore(); - - // draw donut hole - drawDonutHole(ctx); - - // draw labels - if (options.series.pie.label.show) - drawLabels(); - - // restore to original state - ctx.restore(); - - function drawSlice(angle, color, fill) - { - if (angle<=0) - return; - - if (fill) - ctx.fillStyle = color; - else - { - ctx.strokeStyle = color; - ctx.lineJoin = 'round'; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI*2) > 0.000000001) - ctx.moveTo(0,0); // Center of the pie - //ctx.arc(0,0,radius,0,angle,false); // This doesn't work properly in Opera - ctx.arc(0,0,radius,currentAngle,currentAngle+angle,false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) - ctx.fill(); - else - ctx.stroke(); - } - - function drawLabels() - { - var currentAngle = startAngle; - - // set radius - if (options.series.pie.label.radius>1) - var radius = options.series.pie.label.radius; - else - var radius = maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) - { - if (slices[i].percent >= options.series.pie.label.threshold*100) - drawLabel(slices[i], currentAngle, i); - currentAngle += slices[i].angle; - } - - function drawLabel(slice, startAngle, index) - { - if (slice.data[0][1]==0) - return; - - // format label text - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - if (lf) - text = lf(slice.label, slice); - else - text = slice.label; - if (plf) - text = plf(text, slice); - - var halfAngle = ((startAngle+slice.angle) + startAngle)/2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = '' + text + ""; - target.append(html); - var label = target.children('#pieLabel'+index); - var labelTop = (y - label.height()/2); - var labelLeft = (x - label.width()/2); - label.css('top', labelTop); - label.css('left', labelLeft); - - // check to make sure that the label is not outside the canvas - if (0-labelTop>0 || 0-labelLeft>0 || canvas.height-(labelTop+label.height())<0 || canvas.width-(labelLeft+label.width())<0) - redraw = true; - - if (options.series.pie.label.background.opacity != 0) { - // put in the transparent background separately to avoid blended labels and label boxes - var c = options.series.pie.label.background.color; - if (c == null) { - c = slice.color; - } - var pos = 'top:'+labelTop+'px;left:'+labelLeft+'px;'; - $('
').insertBefore(label).css('opacity', options.series.pie.label.background.opacity); - } - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - function drawDonutHole(layer) - { - // draw donut hole - if(options.series.pie.innerRadius > 0) - { - // subtract the center - layer.save(); - innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = 'destination-out'; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0,0,innerRadius,0,Math.PI*2,false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0,0,innerRadius,0,Math.PI*2,false); - layer.stroke(); - layer.closePath(); - layer.restore(); - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) - { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) - { - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - for (var i = 0; i < slices.length; ++i) - { - var s = slices[i]; - - if(s.pie.show) - { - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0,0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0,0,radius,s.startAngle,s.startAngle+s.angle,false); - ctx.closePath(); - x = mouseX-centerLeft; - y = mouseY-centerTop; - if(ctx.isPointInPath) - { - if (ctx.isPointInPath(mouseX-centerLeft, mouseY-centerTop)) - { - //alert('found slice!'); - ctx.restore(); - return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; - } - } - else - { - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - p1X = (radius * Math.cos(s.startAngle)); - p1Y = (radius * Math.sin(s.startAngle)); - p2X = (radius * Math.cos(s.startAngle+(s.angle/4))); - p2Y = (radius * Math.sin(s.startAngle+(s.angle/4))); - p3X = (radius * Math.cos(s.startAngle+(s.angle/2))); - p3Y = (radius * Math.sin(s.startAngle+(s.angle/2))); - p4X = (radius * Math.cos(s.startAngle+(s.angle/1.5))); - p4Y = (radius * Math.sin(s.startAngle+(s.angle/1.5))); - p5X = (radius * Math.cos(s.startAngle+s.angle)); - p5Y = (radius * Math.sin(s.startAngle+s.angle)); - arrPoly = [[0,0],[p1X,p1Y],[p2X,p2Y],[p3X,p3Y],[p4X,p4Y],[p5X,p5Y]]; - arrPoint = [x,y]; - // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? - if(isPointInPoly(arrPoly, arrPoint)) - { - ctx.restore(); - return {datapoint: [s.percent, s.data], dataIndex: 0, series: s, seriesIndex: i}; - } - } - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) - { - triggerClickHoverEvent('plothover', e); - } - - function onClick(e) - { - triggerClickHoverEvent('plotclick', e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - function triggerClickHoverEvent(eventname, e) - { - var offset = plot.offset(), - canvasX = parseInt(e.pageX - offset.left), - canvasY = parseInt(e.pageY - offset.top), - item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) - { - // clear auto-highlights - for (var i = 0; i < highlights.length; ++i) - { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) - unhighlight(h.series); - } - } - - // highlight the slice - if (item) - highlight(item.series, eventname); - - // trigger any hover bind events - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [ pos, item ]); - } - - function highlight(s, auto) - { - if (typeof s == "number") - s = series[s]; - - var i = indexOfHighlight(s); - if (i == -1) - { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } - else if (!auto) - highlights[i].auto = false; - } - - function unhighlight(s) - { - if (s == null) - { - highlights = []; - plot.triggerRedrawOverlay(); - } - - if (typeof s == "number") - s = series[s]; - - var i = indexOfHighlight(s); - if (i != -1) - { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) - { - for (var i = 0; i < highlights.length; ++i) - { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) - { - //alert(options.series.pie.radius); - var options = plot.getOptions(); - //alert(options.series.pie.radius); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (i = 0; i < highlights.length; ++i) - drawHighlight(highlights[i].series); - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) - { - if (series.angle < 0) return; - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, "+options.series.pie.highlight.opacity+")"; // this is temporary until we have access to parseColor - - octx.beginPath(); - if (Math.abs(series.angle - Math.PI*2) > 0.000000001) - octx.moveTo(0,0); // Center of the pie - octx.arc(0,0,radius,series.startAngle,series.startAngle+series.angle,false); - octx.closePath(); - octx.fill(); - } - - } - - } // end init (plugin body) - - // define pie specific options and their default values - var options = { - series: { - pie: { - show: false, - radius: 'auto', // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius:0, /* for donut */ - startAngle: 3/2, - tilt: 1, - offset: { - top: 0, - left: 'auto' - }, - stroke: { - color: '#FFF', - width: 1 - }, - label: { - show: 'auto', - formatter: function(label, slice){ - return '
'+label+'
'+Math.round(slice.percent)+'%
'; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: 'Other' // label to give the new slice - }, - highlight: { - //color: '#FFF', // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.0" - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js deleted file mode 100644 index b7bf870d759a..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.pie.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){function c(D){var h=null;var L=null;var n=null;var B=null;var p=null;var M=0;var F=true;var o=10;var w=0.95;var A=0;var d=false;var z=false;var j=[];D.hooks.processOptions.push(g);D.hooks.bindEvents.push(e);function g(O,N){if(N.series.pie.show){N.grid.show=false;if(N.series.pie.label.show=="auto"){if(N.legend.show){N.series.pie.label.show=false}else{N.series.pie.label.show=true}}if(N.series.pie.radius=="auto"){if(N.series.pie.label.show){N.series.pie.radius=3/4}else{N.series.pie.radius=1}}if(N.series.pie.tilt>1){N.series.pie.tilt=1}if(N.series.pie.tilt<0){N.series.pie.tilt=0}O.hooks.processDatapoints.push(E);O.hooks.drawOverlay.push(H);O.hooks.draw.push(r)}}function e(P,N){var O=P.getOptions();if(O.series.pie.show&&O.grid.hoverable){N.unbind("mousemove").mousemove(t)}if(O.series.pie.show&&O.grid.clickable){N.unbind("click").click(l)}}function G(O){var P="";function N(S,T){if(!T){T=0}for(var R=0;Rh.width-n){B=h.width-n}}}function v(O){for(var N=0;N0){R.push({data:[[1,P]],color:N,label:a.series.pie.combine.label,angle:(P*(Math.PI*2))/M,percent:(P/M*100)})}return R}function r(S,Q){if(!L){return}ctx=Q;I();var T=S.getData();var P=0;while(F&&P0){n*=w}P+=1;N();if(a.series.pie.tilt<=0.8){O()}R()}if(P>=o){N();L.prepend('
Could not draw pie with labels contained inside canvas
')}if(S.setSeries&&S.insertLegend){S.setSeries(T);S.insertLegend()}function N(){ctx.clearRect(0,0,h.width,h.height);L.children().filter(".pieLabel, .pieLabelBackground").remove()}function O(){var Z=5;var Y=15;var W=10;var X=0.02;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}if(U>=(h.width/2)-Z||U*a.series.pie.tilt>=(h.height/2)-Y||U<=W){return}ctx.save();ctx.translate(Z,Y);ctx.globalAlpha=X;ctx.fillStyle="#000";ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);for(var V=1;V<=W;V++){ctx.beginPath();ctx.arc(0,0,U,0,Math.PI*2,false);ctx.fill();U-=V}ctx.restore()}function R(){startAngle=Math.PI*a.series.pie.startAngle;if(a.series.pie.radius>1){var U=a.series.pie.radius}else{var U=n*a.series.pie.radius}ctx.save();ctx.translate(B,p);ctx.scale(1,a.series.pie.tilt);ctx.save();var Y=startAngle;for(var W=0;W1e-9){ctx.moveTo(0,0)}else{if(b.browser.msie){ab-=0.0001}}ctx.arc(0,0,U,Y,Y+ab,false);ctx.closePath();Y+=ab;if(aa){ctx.fill()}else{ctx.stroke()}}function V(){var ac=startAngle;if(a.series.pie.label.radius>1){var Z=a.series.pie.label.radius}else{var Z=n*a.series.pie.label.radius}for(var ab=0;ab=a.series.pie.label.threshold*100){aa(T[ab],ac,ab)}ac+=T[ab].angle}function aa(ap,ai,ag){if(ap.data[0][1]==0){return}var ar=a.legend.labelFormatter,aq,ae=a.series.pie.label.formatter;if(ar){aq=ar(ap.label,ap)}else{aq=ap.label}if(ae){aq=ae(aq,ap)}var aj=((ai+ap.angle)+ai)/2;var ao=B+Math.round(Math.cos(aj)*Z);var am=p+Math.round(Math.sin(aj)*Z)*a.series.pie.tilt;var af=''+aq+"";L.append(af);var an=L.children("#pieLabel"+ag);var ad=(am-an.height()/2);var ah=(ao-an.width()/2);an.css("top",ad);an.css("left",ah);if(0-ad>0||0-ah>0||h.height-(ad+an.height())<0||h.width-(ah+an.width())<0){F=true}if(a.series.pie.label.background.opacity!=0){var ak=a.series.pie.label.background.color;if(ak==null){ak=ap.color}var al="top:"+ad+"px;left:"+ah+"px;";b('
').insertBefore(an).css("opacity",a.series.pie.label.background.opacity)}}}}}function J(N){if(a.series.pie.innerRadius>0){N.save();innerRadius=a.series.pie.innerRadius>1?a.series.pie.innerRadius:n*a.series.pie.innerRadius;N.globalCompositeOperation="destination-out";N.beginPath();N.fillStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.fill();N.closePath();N.restore();N.save();N.beginPath();N.strokeStyle=a.series.pie.stroke.color;N.arc(0,0,innerRadius,0,Math.PI*2,false);N.stroke();N.closePath();N.restore()}}function s(Q,R){for(var S=false,P=-1,N=Q.length,O=N-1;++P1?O.series.pie.radius:n*O.series.pie.radius;for(var Q=0;Q1?P.series.pie.radius:n*P.series.pie.radius;R.save();R.translate(B,p);R.scale(1,P.series.pie.tilt);for(i=0;i1e-9){R.moveTo(0,0)}R.arc(0,0,N,S.startAngle,S.startAngle+S.angle,false);R.closePath();R.fill()}}}var a={series:{pie:{show:false,radius:"auto",innerRadius:0,startAngle:3/2,tilt:1,offset:{top:0,left:"auto"},stroke:{color:"#FFF",width:1},label:{show:"auto",formatter:function(d,e){return'
'+d+"
"+Math.round(e.percent)+"%
"},radius:1,background:{color:null,opacity:0},threshold:0},combine:{threshold:-1,color:null,label:"Other"},highlight:{opacity:0.5}}}};b.plot.plugins.push({init:c,options:a,name:"pie",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.resize.js b/frontend/src/vendor/jquery.flot/jquery.flot.resize.js deleted file mode 100644 index 69dfb24f38e2..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.resize.js +++ /dev/null @@ -1,60 +0,0 @@ -/* -Flot plugin for automatically redrawing plots when the placeholder -size changes, e.g. on window resizes. - -It works by listening for changes on the placeholder div (through the -jQuery resize event plugin) - if the size changes, it will redraw the -plot. - -There are no options. If you need to disable the plugin for some -plots, you can just fix the size of their placeholders. -*/ - - -/* Inline dependency: - * jQuery resize event - v1.1 - 3/14/2010 - * http://benalman.com/projects/jquery-resize-plugin/ - * - * Copyright (c) 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ -(function($,h,c){var a=$([]),e=$.resize=$.extend($.resize,{}),i,k="setTimeout",j="resize",d=j+"-special-event",b="delay",f="throttleWindow";e[b]=250;e[f]=true;$.event.special[j]={setup:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.add(l);$.data(this,d,{w:l.width(),h:l.height()});if(a.length===1){g()}},teardown:function(){if(!e[f]&&this[k]){return false}var l=$(this);a=a.not(l);l.removeData(d);if(!a.length){clearTimeout(i)}},add:function(l){if(!e[f]&&this[k]){return false}var n;function m(s,o,p){var q=$(this),r=$.data(this,d);r.w=o!==c?o:q.width();r.h=p!==c?p:q.height();n.apply(this,arguments)}if($.isFunction(l)){n=l;return m}else{n=l.handler;l.handler=m}}};function g(){i=h[k](function(){a.each(function(){var n=$(this),m=n.width(),l=n.height(),o=$.data(this,d);if(m!==o.w||l!==o.h){n.trigger(j,[o.w=m,o.h=l])}});g()},e[b])}})(jQuery,this); - - -(function ($) { - var options = { }; // no options - - function init(plot) { - function onResize() { - var placeholder = plot.getPlaceholder(); - - // somebody might have hidden us and we can't plot - // when we don't have the dimensions - if (placeholder.width() == 0 || placeholder.height() == 0) - return; - - plot.resize(); - plot.setupGrid(); - plot.draw(); - } - - function bindEvents(plot, eventHolder) { - plot.getPlaceholder().resize(onResize); - } - - function shutdown(plot, eventHolder) { - plot.getPlaceholder().unbind("resize", onResize); - } - - plot.hooks.bindEvents.push(bindEvents); - plot.hooks.shutdown.push(shutdown); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'resize', - version: '1.0' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js deleted file mode 100644 index 1fa0771f570e..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.resize.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(n,p,u){var w=n([]),s=n.resize=n.extend(n.resize,{}),o,l="setTimeout",m="resize",t=m+"-special-event",v="delay",r="throttleWindow";s[v]=250;s[r]=true;n.event.special[m]={setup:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.add(a);n.data(this,t,{w:a.width(),h:a.height()});if(w.length===1){q()}},teardown:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.not(a);a.removeData(t);if(!w.length){clearTimeout(o)}},add:function(b){if(!s[r]&&this[l]){return false}var c;function a(d,h,g){var f=n(this),e=n.data(this,t);e.w=h!==u?h:f.width();e.h=g!==u?g:f.height();c.apply(this,arguments)}if(n.isFunction(b)){c=b;return a}else{c=b.handler;b.handler=a}}};function q(){o=p[l](function(){w.each(function(){var d=n(this),a=d.width(),b=d.height(),c=n.data(this,t);if(a!==c.w||b!==c.h){d.trigger(m,[c.w=a,c.h=b])}});q()},s[v])}})(jQuery,this);(function(b){var a={};function c(f){function e(){var h=f.getPlaceholder();if(h.width()==0||h.height()==0){return}f.resize();f.setupGrid();f.draw()}function g(i,h){i.getPlaceholder().resize(e)}function d(i,h){i.getPlaceholder().unbind("resize",e)}f.hooks.bindEvents.push(g);f.hooks.shutdown.push(d)}b.plot.plugins.push({init:c,options:a,name:"resize",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.selection.js b/frontend/src/vendor/jquery.flot/jquery.flot.selection.js deleted file mode 100644 index 7f7b32694bda..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.selection.js +++ /dev/null @@ -1,344 +0,0 @@ -/* -Flot plugin for selecting regions. - -The plugin defines the following options: - - selection: { - mode: null or "x" or "y" or "xy", - color: color - } - -Selection support is enabled by setting the mode to one of "x", "y" or -"xy". In "x" mode, the user will only be able to specify the x range, -similarly for "y" mode. For "xy", the selection becomes a rectangle -where both ranges can be specified. "color" is color of the selection -(if you need to change the color later on, you can get to it with -plot.getOptions().selection.color). - -When selection support is enabled, a "plotselected" event will be -emitted on the DOM element you passed into the plot function. The -event handler gets a parameter with the ranges selected on the axes, -like this: - - placeholder.bind("plotselected", function(event, ranges) { - alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) - // similar for yaxis - with multiple axes, the extra ones are in - // x2axis, x3axis, ... - }); - -The "plotselected" event is only fired when the user has finished -making the selection. A "plotselecting" event is fired during the -process with the same parameters as the "plotselected" event, in case -you want to know what's happening while it's happening, - -A "plotunselected" event with no arguments is emitted when the user -clicks the mouse to remove the selection. - -The plugin allso adds the following methods to the plot object: - -- setSelection(ranges, preventEvent) - - Set the selection rectangle. The passed in ranges is on the same - form as returned in the "plotselected" event. If the selection mode - is "x", you should put in either an xaxis range, if the mode is "y" - you need to put in an yaxis range and both xaxis and yaxis if the - selection mode is "xy", like this: - - setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - - setSelection will trigger the "plotselected" event when called. If - you don't want that to happen, e.g. if you're inside a - "plotselected" handler, pass true as the second parameter. If you - are using multiple axes, you can specify the ranges on any of those, - e.g. as x2axis/x3axis/... instead of xaxis, the plugin picks the - first one it sees. - -- clearSelection(preventEvent) - - Clear the selection rectangle. Pass in true to avoid getting a - "plotunselected" event. - -- getSelection() - - Returns the current selection in the same format as the - "plotselected" event. If there's currently no selection, the - function returns null. - -*/ - -(function ($) { - function init(plot) { - var selection = { - first: { x: -1, y: -1}, second: { x: -1, y: -1}, - show: false, - active: false - }; - - // FIXME: The drag handling implemented here should be - // abstracted out, there's some similar code from a library in - // the navigation plugin, this should be massaged a bit to fit - // the Flot cases here better and reused. Doing this would - // make this plugin much slimmer. - var savedhandlers = {}; - - var mouseUpHandler = null; - - function onMouseMove(e) { - if (selection.active) { - updateSelection(e); - - plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); - } - } - - function onMouseDown(e) { - if (e.which != 1) // only accept left-click - return; - - // cancel out any text selections - document.body.focus(); - - // prevent text selection and drag in old-school browsers - if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { - savedhandlers.onselectstart = document.onselectstart; - document.onselectstart = function () { return false; }; - } - if (document.ondrag !== undefined && savedhandlers.ondrag == null) { - savedhandlers.ondrag = document.ondrag; - document.ondrag = function () { return false; }; - } - - setSelectionPos(selection.first, e); - - selection.active = true; - - // this is a bit silly, but we have to use a closure to be - // able to whack the same handler again - mouseUpHandler = function (e) { onMouseUp(e); }; - - $(document).one("mouseup", mouseUpHandler); - } - - function onMouseUp(e) { - mouseUpHandler = null; - - // revert drag stuff for old-school browsers - if (document.onselectstart !== undefined) - document.onselectstart = savedhandlers.onselectstart; - if (document.ondrag !== undefined) - document.ondrag = savedhandlers.ondrag; - - // no more dragging - selection.active = false; - updateSelection(e); - - if (selectionIsSane()) - triggerSelectedEvent(); - else { - // this counts as a clear - plot.getPlaceholder().trigger("plotunselected", [ ]); - plot.getPlaceholder().trigger("plotselecting", [ null ]); - } - - return false; - } - - function getSelection() { - if (!selectionIsSane()) - return null; - - var r = {}, c1 = selection.first, c2 = selection.second; - $.each(plot.getAxes(), function (name, axis) { - if (axis.used) { - var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); - r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; - } - }); - return r; - } - - function triggerSelectedEvent() { - var r = getSelection(); - - plot.getPlaceholder().trigger("plotselected", [ r ]); - - // backwards-compat stuff, to be removed in future - if (r.xaxis && r.yaxis) - plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); - } - - function clamp(min, value, max) { - return value < min ? min: (value > max ? max: value); - } - - function setSelectionPos(pos, e) { - var o = plot.getOptions(); - var offset = plot.getPlaceholder().offset(); - var plotOffset = plot.getPlotOffset(); - pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); - pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); - - if (o.selection.mode == "y") - pos.x = pos == selection.first ? 0 : plot.width(); - - if (o.selection.mode == "x") - pos.y = pos == selection.first ? 0 : plot.height(); - } - - function updateSelection(pos) { - if (pos.pageX == null) - return; - - setSelectionPos(selection.second, pos); - if (selectionIsSane()) { - selection.show = true; - plot.triggerRedrawOverlay(); - } - else - clearSelection(true); - } - - function clearSelection(preventEvent) { - if (selection.show) { - selection.show = false; - plot.triggerRedrawOverlay(); - if (!preventEvent) - plot.getPlaceholder().trigger("plotunselected", [ ]); - } - } - - // function taken from markings support in Flot - function extractRange(ranges, coord) { - var axis, from, to, key, axes = plot.getAxes(); - - for (var k in axes) { - axis = axes[k]; - if (axis.direction == coord) { - key = coord + axis.n + "axis"; - if (!ranges[key] && axis.n == 1) - key = coord + "axis"; // support x1axis as xaxis - if (ranges[key]) { - from = ranges[key].from; - to = ranges[key].to; - break; - } - } - } - - // backwards-compat stuff - to be removed in future - if (!ranges[key]) { - axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; - from = ranges[coord + "1"]; - to = ranges[coord + "2"]; - } - - // auto-reverse as an added bonus - if (from != null && to != null && from > to) { - var tmp = from; - from = to; - to = tmp; - } - - return { from: from, to: to, axis: axis }; - } - - function setSelection(ranges, preventEvent) { - var axis, range, o = plot.getOptions(); - - if (o.selection.mode == "y") { - selection.first.x = 0; - selection.second.x = plot.width(); - } - else { - range = extractRange(ranges, "x"); - - selection.first.x = range.axis.p2c(range.from); - selection.second.x = range.axis.p2c(range.to); - } - - if (o.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plot.height(); - } - else { - range = extractRange(ranges, "y"); - - selection.first.y = range.axis.p2c(range.from); - selection.second.y = range.axis.p2c(range.to); - } - - selection.show = true; - plot.triggerRedrawOverlay(); - if (!preventEvent && selectionIsSane()) - triggerSelectedEvent(); - } - - function selectionIsSane() { - var minSize = 5; - return Math.abs(selection.second.x - selection.first.x) >= minSize && - Math.abs(selection.second.y - selection.first.y) >= minSize; - } - - plot.clearSelection = clearSelection; - plot.setSelection = setSelection; - plot.getSelection = getSelection; - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var o = plot.getOptions(); - if (o.selection.mode != null) { - eventHolder.mousemove(onMouseMove); - eventHolder.mousedown(onMouseDown); - } - }); - - - plot.hooks.drawOverlay.push(function (plot, ctx) { - // draw selection - if (selection.show && selectionIsSane()) { - var plotOffset = plot.getPlotOffset(); - var o = plot.getOptions(); - - ctx.save(); - ctx.translate(plotOffset.left, plotOffset.top); - - var c = $.color.parse(o.selection.color); - - ctx.strokeStyle = c.scale('a', 0.8).toString(); - ctx.lineWidth = 1; - ctx.lineJoin = "round"; - ctx.fillStyle = c.scale('a', 0.4).toString(); - - var x = Math.min(selection.first.x, selection.second.x), - y = Math.min(selection.first.y, selection.second.y), - w = Math.abs(selection.second.x - selection.first.x), - h = Math.abs(selection.second.y - selection.first.y); - - ctx.fillRect(x, y, w, h); - ctx.strokeRect(x, y, w, h); - - ctx.restore(); - } - }); - - plot.hooks.shutdown.push(function (plot, eventHolder) { - eventHolder.unbind("mousemove", onMouseMove); - eventHolder.unbind("mousedown", onMouseDown); - - if (mouseUpHandler) - $(document).unbind("mouseup", mouseUpHandler); - }); - - } - - $.plot.plugins.push({ - init: init, - options: { - selection: { - mode: null, // one of null, "x", "y" or "xy" - color: "#e8cfac" - } - }, - name: 'selection', - version: '1.1' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js deleted file mode 100644 index badc0052dbe0..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.selection.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(a){function b(k){var p={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var m={};var r=null;function e(s){if(p.active){l(s);k.getPlaceholder().trigger("plotselecting",[g()])}}function n(s){if(s.which!=1){return}document.body.focus();if(document.onselectstart!==undefined&&m.onselectstart==null){m.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&m.ondrag==null){m.ondrag=document.ondrag;document.ondrag=function(){return false}}d(p.first,s);p.active=true;r=function(t){j(t)};a(document).one("mouseup",r)}function j(s){r=null;if(document.onselectstart!==undefined){document.onselectstart=m.onselectstart}if(document.ondrag!==undefined){document.ondrag=m.ondrag}p.active=false;l(s);if(f()){i()}else{k.getPlaceholder().trigger("plotunselected",[]);k.getPlaceholder().trigger("plotselecting",[null])}return false}function g(){if(!f()){return null}var u={},t=p.first,s=p.second;a.each(k.getAxes(),function(v,w){if(w.used){var y=w.c2p(t[w.direction]),x=w.c2p(s[w.direction]);u[v]={from:Math.min(y,x),to:Math.max(y,x)}}});return u}function i(){var s=g();k.getPlaceholder().trigger("plotselected",[s]);if(s.xaxis&&s.yaxis){k.getPlaceholder().trigger("selected",[{x1:s.xaxis.from,y1:s.yaxis.from,x2:s.xaxis.to,y2:s.yaxis.to}])}}function h(t,u,s){return us?s:u)}function d(w,t){var v=k.getOptions();var u=k.getPlaceholder().offset();var s=k.getPlotOffset();w.x=h(0,t.pageX-u.left-s.left,k.width());w.y=h(0,t.pageY-u.top-s.top,k.height());if(v.selection.mode=="y"){w.x=w==p.first?0:k.width()}if(v.selection.mode=="x"){w.y=w==p.first?0:k.height()}}function l(s){if(s.pageX==null){return}d(p.second,s);if(f()){p.show=true;k.triggerRedrawOverlay()}else{q(true)}}function q(s){if(p.show){p.show=false;k.triggerRedrawOverlay();if(!s){k.getPlaceholder().trigger("plotunselected",[])}}}function c(s,w){var t,y,z,A,x=k.getAxes();for(var u in x){t=x[u];if(t.direction==w){A=w+t.n+"axis";if(!s[A]&&t.n==1){A=w+"axis"}if(s[A]){y=s[A].from;z=s[A].to;break}}}if(!s[A]){t=w=="x"?k.getXAxes()[0]:k.getYAxes()[0];y=s[w+"1"];z=s[w+"2"]}if(y!=null&&z!=null&&y>z){var v=y;y=z;z=v}return{from:y,to:z,axis:t}}function o(t,s){var v,u,w=k.getOptions();if(w.selection.mode=="y"){p.first.x=0;p.second.x=k.width()}else{u=c(t,"x");p.first.x=u.axis.p2c(u.from);p.second.x=u.axis.p2c(u.to)}if(w.selection.mode=="x"){p.first.y=0;p.second.y=k.height()}else{u=c(t,"y");p.first.y=u.axis.p2c(u.from);p.second.y=u.axis.p2c(u.to)}p.show=true;k.triggerRedrawOverlay();if(!s&&f()){i()}}function f(){var s=5;return Math.abs(p.second.x-p.first.x)>=s&&Math.abs(p.second.y-p.first.y)>=s}k.clearSelection=q;k.setSelection=o;k.getSelection=g;k.hooks.bindEvents.push(function(t,s){var u=t.getOptions();if(u.selection.mode!=null){s.mousemove(e);s.mousedown(n)}});k.hooks.drawOverlay.push(function(v,D){if(p.show&&f()){var t=v.getPlotOffset();var s=v.getOptions();D.save();D.translate(t.left,t.top);var z=a.color.parse(s.selection.color);D.strokeStyle=z.scale("a",0.8).toString();D.lineWidth=1;D.lineJoin="round";D.fillStyle=z.scale("a",0.4).toString();var B=Math.min(p.first.x,p.second.x),A=Math.min(p.first.y,p.second.y),C=Math.abs(p.second.x-p.first.x),u=Math.abs(p.second.y-p.first.y);D.fillRect(B,A,C,u);D.strokeRect(B,A,C,u);D.restore()}});k.hooks.shutdown.push(function(t,s){s.unbind("mousemove",e);s.unbind("mousedown",n);if(r){a(document).unbind("mouseup",r)}})}a.plot.plugins.push({init:b,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.1"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.stack.js b/frontend/src/vendor/jquery.flot/jquery.flot.stack.js deleted file mode 100644 index a31d5dc9b586..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.stack.js +++ /dev/null @@ -1,184 +0,0 @@ -/* -Flot plugin for stacking data sets, i.e. putting them on top of each -other, for accumulative graphs. - -The plugin assumes the data is sorted on x (or y if stacking -horizontally). For line charts, it is assumed that if a line has an -undefined gap (from a null point), then the line above it should have -the same gap - insert zeros instead of "null" if you want another -behaviour. This also holds for the start and end of the chart. Note -that stacking a mix of positive and negative values in most instances -doesn't make sense (so it looks weird). - -Two or more series are stacked when their "stack" attribute is set to -the same key (which can be any number or string or just "true"). To -specify the default stack, you can set - - series: { - stack: null or true or key (number/string) - } - -or specify it for a specific series - - $.plot($("#placeholder"), [{ data: [ ... ], stack: true }]) - -The stacking order is determined by the order of the data series in -the array (later series end up on top of the previous). - -Internally, the plugin modifies the datapoints in each series, adding -an offset to the y value. For line series, extra data points are -inserted through interpolation. If there's a second y value, it's also -adjusted (e.g for bar charts or filled areas). -*/ - -(function ($) { - var options = { - series: { stack: null } // or number/string - }; - - function init(plot) { - function findMatchingSeries(s, allseries) { - var res = null - for (var i = 0; i < allseries.length; ++i) { - if (s == allseries[i]) - break; - - if (allseries[i].stack == s.stack) - res = allseries[i]; - } - - return res; - } - - function stackData(plot, s, datapoints) { - if (s.stack == null) - return; - - var other = findMatchingSeries(s, plot.getData()); - if (!other) - return; - - var ps = datapoints.pointsize, - points = datapoints.points, - otherps = other.datapoints.pointsize, - otherpoints = other.datapoints.points, - newpoints = [], - px, py, intery, qx, qy, bottom, - withlines = s.lines.show, - horizontal = s.bars.horizontal, - withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), - withsteps = withlines && s.lines.steps, - fromgap = true, - keyOffset = horizontal ? 1 : 0, - accumulateOffset = horizontal ? 0 : 1, - i = 0, j = 0, l; - - while (true) { - if (i >= points.length) - break; - - l = newpoints.length; - - if (points[i] == null) { - // copy gaps - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - i += ps; - } - else if (j >= otherpoints.length) { - // for lines, we can't use the rest of the points - if (!withlines) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - } - i += ps; - } - else if (otherpoints[j] == null) { - // oops, got a gap - for (m = 0; m < ps; ++m) - newpoints.push(null); - fromgap = true; - j += otherps; - } - else { - // cases where we actually got two points - px = points[i + keyOffset]; - py = points[i + accumulateOffset]; - qx = otherpoints[j + keyOffset]; - qy = otherpoints[j + accumulateOffset]; - bottom = 0; - - if (px == qx) { - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - newpoints[l + accumulateOffset] += qy; - bottom = qy; - - i += ps; - j += otherps; - } - else if (px > qx) { - // we got past point below, might need to - // insert interpolated extra point - if (withlines && i > 0 && points[i - ps] != null) { - intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); - newpoints.push(qx); - newpoints.push(intery + qy); - for (m = 2; m < ps; ++m) - newpoints.push(points[i + m]); - bottom = qy; - } - - j += otherps; - } - else { // px < qx - if (fromgap && withlines) { - // if we come from a gap, we just skip this point - i += ps; - continue; - } - - for (m = 0; m < ps; ++m) - newpoints.push(points[i + m]); - - // we might be able to interpolate a point below, - // this can give us a better y - if (withlines && j > 0 && otherpoints[j - otherps] != null) - bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); - - newpoints[l + accumulateOffset] += bottom; - - i += ps; - } - - fromgap = false; - - if (l != newpoints.length && withbottom) - newpoints[l + 2] += bottom; - } - - // maintain the line steps invariant - if (withsteps && l != newpoints.length && l > 0 - && newpoints[l] != null - && newpoints[l] != newpoints[l - ps] - && newpoints[l + 1] != newpoints[l - ps + 1]) { - for (m = 0; m < ps; ++m) - newpoints[l + ps + m] = newpoints[l + m]; - newpoints[l + 1] = newpoints[l - ps + 1]; - } - } - - datapoints.points = newpoints; - } - - plot.hooks.processDatapoints.push(stackData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'stack', - version: '1.2' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js deleted file mode 100644 index bba2a0e5ff7e..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.stack.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){var a={series:{stack:null}};function c(f){function d(k,j){var h=null;for(var g=0;g2&&(G?g.format[2].x:g.format[2].y),n=u&&v.lines.steps,E=true,q=G?1:0,H=G?0:1,D=0,B=0,A;while(true){if(D>=F.length){break}A=t.length;if(F[D]==null){for(m=0;m=y.length){if(!u){for(m=0;mJ){if(u&&D>0&&F[D-z]!=null){k=w+(F[D-z+H]-w)*(J-x)/(F[D-z+q]-x);t.push(J);t.push(k+I);for(m=2;m0&&y[B-h]!=null){r=I+(y[B-h+H]-I)*(x-J)/(y[B-h+q]-J)}t[A+H]+=r;D+=z}}E=false;if(A!=t.length&&o){t[A+2]+=r}}}}if(n&&A!=t.length&&A>0&&t[A]!=null&&t[A]!=t[A-z]&&t[A+1]!=t[A-z+1]){for(m=0;m s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.rect(x - size, y - size, size + size, size + size); - }, - diamond: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) - var size = radius * Math.sqrt(Math.PI / 2); - ctx.moveTo(x - size, y); - ctx.lineTo(x, y - size); - ctx.lineTo(x + size, y); - ctx.lineTo(x, y + size); - ctx.lineTo(x - size, y); - }, - triangle: function (ctx, x, y, radius, shadow) { - // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) - var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); - var height = size * Math.sin(Math.PI / 3); - ctx.moveTo(x - size/2, y + height/2); - ctx.lineTo(x + size/2, y + height/2); - if (!shadow) { - ctx.lineTo(x, y - height/2); - ctx.lineTo(x - size/2, y + height/2); - } - }, - cross: function (ctx, x, y, radius, shadow) { - // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 - var size = radius * Math.sqrt(Math.PI) / 2; - ctx.moveTo(x - size, y - size); - ctx.lineTo(x + size, y + size); - ctx.moveTo(x - size, y + size); - ctx.lineTo(x + size, y - size); - } - } - - var s = series.points.symbol; - if (handlers[s]) - series.points.symbol = handlers[s]; - } - - function init(plot) { - plot.hooks.processDatapoints.push(processRawData); - } - - $.plot.plugins.push({ - init: init, - name: 'symbols', - version: '1.0' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js deleted file mode 100644 index 272e003ab49c..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.symbol.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(b){function a(h,e,g){var d={square:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.rect(j-l,n-l,l+l,l+l)},diamond:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI/2);k.moveTo(j-l,n);k.lineTo(j,n-l);k.lineTo(j+l,n);k.lineTo(j,n+l);k.lineTo(j-l,n)},triangle:function(l,k,o,j,n){var m=j*Math.sqrt(2*Math.PI/Math.sin(Math.PI/3));var i=m*Math.sin(Math.PI/3);l.moveTo(k-m/2,o+i/2);l.lineTo(k+m/2,o+i/2);if(!n){l.lineTo(k,o-i/2);l.lineTo(k-m/2,o+i/2)}},cross:function(k,j,n,i,m){var l=i*Math.sqrt(Math.PI)/2;k.moveTo(j-l,n-l);k.lineTo(j+l,n+l);k.moveTo(j-l,n+l);k.lineTo(j+l,n-l)}};var f=e.points.symbol;if(d[f]){e.points.symbol=d[f]}}function c(d){d.hooks.processDatapoints.push(a)}b.plot.plugins.push({init:c,name:"symbols",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js b/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js deleted file mode 100644 index 0b2e7ac82a73..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.js +++ /dev/null @@ -1,103 +0,0 @@ -/* -Flot plugin for thresholding data. Controlled through the option -"threshold" in either the global series options - - series: { - threshold: { - below: number - color: colorspec - } - } - -or in a specific series - - $.plot($("#placeholder"), [{ data: [ ... ], threshold: { ... }}]) - -The data points below "below" are drawn with the specified color. This -makes it easy to mark points below 0, e.g. for budget data. - -Internally, the plugin works by splitting the data into two series, -above and below the threshold. The extra series below the threshold -will have its label cleared and the special "originSeries" attribute -set to the original series. You may need to check for this in hover -events. -*/ - -(function ($) { - var options = { - series: { threshold: null } // or { below: number, color: color spec} - }; - - function init(plot) { - function thresholdData(plot, s, datapoints) { - if (!s.threshold) - return; - - var ps = datapoints.pointsize, i, x, y, p, prevp, - thresholded = $.extend({}, s); // note: shallow copy - - thresholded.datapoints = { points: [], pointsize: ps }; - thresholded.label = null; - thresholded.color = s.threshold.color; - thresholded.threshold = null; - thresholded.originSeries = s; - thresholded.data = []; - - var below = s.threshold.below, - origpoints = datapoints.points, - addCrossingPoints = s.lines.show; - - threspoints = []; - newpoints = []; - - for (i = 0; i < origpoints.length; i += ps) { - x = origpoints[i] - y = origpoints[i + 1]; - - prevp = p; - if (y < below) - p = threspoints; - else - p = newpoints; - - if (addCrossingPoints && prevp != p && x != null - && i > 0 && origpoints[i - ps] != null) { - var interx = (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]) * (below - y) + x; - prevp.push(interx); - prevp.push(below); - for (m = 2; m < ps; ++m) - prevp.push(origpoints[i + m]); - - p.push(null); // start new segment - p.push(null); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - p.push(interx); - p.push(below); - for (m = 2; m < ps; ++m) - p.push(origpoints[i + m]); - } - - p.push(x); - p.push(y); - } - - datapoints.points = newpoints; - thresholded.datapoints.points = threspoints; - - if (thresholded.datapoints.points.length > 0) - plot.getData().push(thresholded); - - // FIXME: there are probably some edge cases left in bars - } - - plot.hooks.processDatapoints.push(thresholdData); - } - - $.plot.plugins.push({ - init: init, - options: options, - name: 'threshold', - version: '1.0' - }); -})(jQuery); diff --git a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js b/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js deleted file mode 100644 index d8b79dfc93c9..000000000000 --- a/frontend/src/vendor/jquery.flot/jquery.flot.threshold.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(B){var A={series:{threshold:null}};function C(D){function E(L,S,M){if(!S.threshold){return }var F=M.pointsize,I,O,N,G,K,H=B.extend({},S);H.datapoints={points:[],pointsize:F};H.label=null;H.color=S.threshold.color;H.threshold=null;H.originSeries=S;H.data=[];var P=S.threshold.below,Q=M.points,R=S.lines.show;threspoints=[];newpoints=[];for(I=0;I0&&Q[I-F]!=null){var J=(O-Q[I-F])/(N-Q[I-F+1])*(P-N)+O;K.push(J);K.push(P);for(m=2;m0){L.getData().push(H)}}D.hooks.processDatapoints.push(E)}B.plot.plugins.push({init:C,options:A,name:"threshold",version:"1.0"})})(jQuery); \ No newline at end of file diff --git a/modules/backlogs/app/components/backlogs/backlog_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_component.html.erb new file mode 100644 index 000000000000..cbbe2339740c --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_component.html.erb @@ -0,0 +1,63 @@ +<%# -- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++# %> + +<%= component_wrapper(tag: :section) do %> + <%= render(Primer::Beta::BorderBox.new(**@system_arguments)) do |border_box| %> + <% border_box.with_header do %> + <%= render(Backlogs::BacklogHeaderComponent.new(backlog:, project: @project, folded: folded?)) %> + <% end %> + <% if backlog.stories.empty? %> + <% border_box.with_row(data: { empty_list_item: true }) do %> + <%= + render Primer::Beta::Blankslate.new(role: "status", aria: { live: "polite" }) do |blankslate| + blankslate.with_heading(tag: :h4).with_content(t(".blankslate_title", name: sprint.name)) + blankslate.with_description_content(t(".blankslate_description")) + end + %> + <% end %> + <% end %> + <% backlog.stories.each do |story| %> + <% border_box.with_row( + id: dom_id(story), + classes: "Box-row--hover-blue Box-row--focus-gray Box-row--clickable Box-row--draggable", + data: draggable_item_config(story).merge( + story: true, + controller: "backlogs--story", + backlogs__story_id_value: story.id, + backlogs__story_split_url_value: details_backlogs_project_backlogs_path(project, story), + backlogs__story_full_url_value: work_package_path(story), + backlogs__story_selected_class: "Box-row--blue" + ), + tabindex: 0 + ) do %> + <%= render(Backlogs::StoryComponent.new(story:, sprint:, max_position:)) %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/modules/backlogs/app/components/backlogs/backlog_component.rb b/modules/backlogs/app/components/backlogs/backlog_component.rb new file mode 100644 index 000000000000..a25366262a33 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_component.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class BacklogComponent < ApplicationComponent + include Primer::AttributesHelper + include OpTurbo::Streamable + include RbCommonHelper + + attr_reader :backlog, :project, :current_user + + delegate :sprint, :stories, to: :backlog + + def initialize(backlog:, project:, current_user: User.current, **system_arguments) + super() + + @backlog = backlog + @project = project + @current_user = current_user + + @system_arguments = system_arguments + @system_arguments[:id] = dom_id(backlog) + @system_arguments[:list_id] = "#{@system_arguments[:id]}-list" + @system_arguments[:padding] = :condensed + @system_arguments[:data] = merge_data( + @system_arguments, + { data: drop_target_config } + ) + end + + def wrapper_uniq_by + backlog.sprint_id + end + + private + + def folded? + current_user.backlogs_preference(:versions_default_fold_state) == "closed" + end + + def max_position + stories.filter_map(&:position).max + end + + def drop_target_config + { + generic_drag_and_drop_target: "container", + target_container_accessor: ":scope > ul", + target_id: backlog.sprint_id, + target_allowed_drag_type: "story" + } + end + + def draggable_item_config(story) + { + draggable_id: story.id, + draggable_type: "story", + drop_url: move_backlogs_project_sprint_story_path(project, sprint, story) + } + end + end +end diff --git a/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb new file mode 100644 index 000000000000..9d6f47b15188 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_header_component.html.erb @@ -0,0 +1,90 @@ +<%# -- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++# %> + +<%= component_wrapper(tag: :header) do %> + <% if show? %> + <%= grid_layout("op-backlogs-header", tag: :div) do |grid| %> + <% grid.with_area(:collapsible) do %> + <%= + render( + Backlogs::CollapsibleComponent.new( + collapsible_id: "#{dom_id(backlog)}-list", + toggle_label: t(".label_toggle_backlog", name: sprint.name), + collapsed: + ) + ) do |collapsible| + collapsible.with_title { sprint.name } + collapsible.with_count( + scheme: :default, + count: story_count, + round: true, + aria: { + label: t(".label_story_count", count: story_count), + live: "polite" + } + ) + collapsible.with_description(role: "group") do + format_date_range(date_range) + end + end + %> + <% end %> + + <% grid.with_area(:points) do %> + <%= + render( + Primer::Beta::Text.new( + color: :subtle, + classes: "velocity", + aria: { live: "polite" } + ) + ) do + %> + <%= story_points %> + <%= t(:"backlogs.points_label", count: story_points) %> + <% end %> + <% end %> + + <% grid.with_area(:menu) do %> + <%= render(Backlogs::BacklogMenuComponent.new(backlog:, project: @project)) %> + <% end %> + <% end %> + <% else %> + <%= + primer_form_with( + url: backlogs_project_sprint_path(project, sprint), + model: sprint, + method: :patch, + class: "op-backlogs-header-form" + ) do |f| + render(Backlogs::BacklogHeaderForm.new(f, cancel_path: show_name_backlogs_project_sprint_path(project, sprint))) + end + %> + <% end %> +<% end %> diff --git a/modules/backlogs/app/components/backlogs/backlog_header_component.rb b/modules/backlogs/app/components/backlogs/backlog_header_component.rb new file mode 100644 index 000000000000..d8c621ca19c6 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_header_component.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class BacklogHeaderComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + include Primer::FetchOrFallbackHelper + include Redmine::I18n + include RbCommonHelper + + STATE_DEFAULT = :show + STATE_OPTIONS = [STATE_DEFAULT, :edit].freeze + + attr_reader :backlog, :project, :state, :collapsed, :current_user + + delegate :sprint, :stories, to: :backlog + delegate :name, to: :sprint, prefix: :sprint + delegate :edit?, :show?, to: :state + + def initialize( + backlog:, + project:, + state: STATE_DEFAULT, + folded: false, + current_user: User.current + ) + super() + + @backlog = backlog + @project = project + @state = ActiveSupport::StringInquirer.new(fetch_or_fallback(STATE_OPTIONS, state, STATE_DEFAULT).to_s) + @collapsed = folded + @current_user = current_user + end + + def wrapper_uniq_by + backlog.sprint_id + end + + private + + def story_points + @story_points ||= stories.sum { |story| story.story_points || 0 } + end + + def story_count + @story_count ||= stories.size + end + + def date_range + [sprint.start_date, sprint.effective_date] + end + end +end diff --git a/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb b/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb new file mode 100644 index 000000000000..78f56b0dba4c --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_menu_component.html.erb @@ -0,0 +1,116 @@ +<%# -- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++# %> + +<%= + render(Primer::Alpha::ActionMenu.new(anchor_align: :end, classes: "hide-when-print")) do |menu| + menu.with_show_button( + scheme: :invisible, + icon: :"kebab-horizontal", + "aria-label": t(".label_actions"), + tooltip_direction: :se + ) + + if user_allowed?(:update_sprints) + menu.with_item( + label: t(".action_menu.edit_sprint"), + href: edit_name_backlogs_project_sprint_path(project, sprint), + content_arguments: { data: { turbo_stream: true } } + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + end + + if user_allowed?(:add_work_packages) + menu.with_item( + label: t(".action_menu.new_story"), + href: new_project_work_packages_dialog_path( + project, + version_id: sprint.id, + type_id: available_story_types.first + ), + content_arguments: { data: { turbo_stream: true } } + ) do |item| + item.with_leading_visual_icon(icon: :compose) + end + end + + if user_allowed?(:update_sprints) || user_allowed?(:add_work_packages) + menu.with_divider + end + + menu.with_item( + label: t(".action_menu.stories_tasks"), + tag: :a, + href: backlogs_project_sprint_query_path(project, sprint) + ) do |item| + item.with_leading_visual_icon(icon: :"op-view-list") + end + + if backlog.sprint_backlog? + if user_allowed?(:view_taskboards) + menu.with_item( + label: t(".action_menu.task_board"), + tag: :a, + href: backlogs_project_sprint_taskboard_path(project, sprint) + ) do |item| + item.with_leading_visual_icon(icon: :"op-view-cards") + end + end + + menu.with_item( + label: t(".action_menu.burndown_chart"), + tag: :a, + href: backlogs_project_sprint_burndown_chart_path(project, sprint), + disabled: !sprint.has_burndown? + ) do |item| + item.with_leading_visual_icon(icon: :graph) + end + + if project.module_enabled? "wiki" + menu.with_item( + label: t(".action_menu.wiki"), + tag: :a, + href: edit_backlogs_project_sprint_wiki_path(project, sprint) + ) do |item| + item.with_leading_visual_icon(icon: :book) + end + end + end + + if user_allowed?(:manage_versions) + menu.with_item( + label: t(".action_menu.properties"), + tag: :a, + href: edit_version_path(sprint, back_url: backlogs_project_backlogs_path(project), project_id: project) + ) do |item| + item.with_leading_visual_icon(icon: :gear) + end + end + end +%> diff --git a/modules/backlogs/app/components/backlogs/backlog_menu_component.rb b/modules/backlogs/app/components/backlogs/backlog_menu_component.rb new file mode 100644 index 000000000000..07f5ad79bad5 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/backlog_menu_component.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class BacklogMenuComponent < ApplicationComponent + include RbCommonHelper + + attr_reader :backlog, :project, :current_user + + delegate :sprint, :stories, to: :backlog + + def initialize(backlog:, project:, current_user: User.current) + super() + + @backlog = backlog + @project = project + @current_user = current_user + end + + private + + def user_allowed?(permission) + current_user.allowed_in_project?(permission, project) + end + + def available_story_types + @available_story_types ||= story_types & project.types + end + end +end diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.html.erb b/modules/backlogs/app/components/backlogs/collapsible_component.html.erb new file mode 100644 index 000000000000..efe6818b35a8 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/collapsible_component.html.erb @@ -0,0 +1,61 @@ +<%# -- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++# %> + +<%= render(Primer::BaseComponent.new(**@system_arguments)) do %> + <%= + render( + Primer::BaseComponent.new( + tag: :div, + role: "button", + tabindex: 0, + classes: "op-backlogs-collapsible", + aria: { + label: @toggle_label, + controls: @collapsible_id, + expanded: !@collapsed + }, + data: { + target: "collapsible-header.triggerElement", + action: "click:collapsible-header#toggle keydown:collapsible-header#toggleViaKeyboard" + } + ) + ) do + %> + <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--title-line")) do %> + <%= title %> + <%= count %> + <%= render(Primer::BaseComponent.new(tag: :div, classes: "op-backlogs-collapsible--toggle")) do %> + <%= render(Primer::Beta::Octicon.new(icon: "chevron-up", hidden: @collapsed, data: { target: "collapsible-header.arrowUp" })) %> + <%= render(Primer::Beta::Octicon.new(icon: "chevron-down", hidden: !@collapsed, data: { target: "collapsible-header.arrowDown" })) %> + <% end %> + <% end %> + + <%= description %> + <% end %> +<% end %> diff --git a/modules/backlogs/app/components/backlogs/collapsible_component.rb b/modules/backlogs/app/components/backlogs/collapsible_component.rb new file mode 100644 index 000000000000..3ab049402716 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/collapsible_component.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class CollapsibleComponent < Primer::Component + include OpPrimer::ComponentHelpers + + renders_one :title, ->(**system_arguments) { + system_arguments[:classes] = class_names( + system_arguments[:classes], + "op-backlogs-collapsible--title", + "Box-title" + ) + + Primer::Beta::Truncate.new(tag: :h3, **system_arguments) + } + + renders_one :count, ->(**system_arguments) { + system_arguments[:mr] ||= 2 + system_arguments[:scheme] ||= :primary + system_arguments[:classes] = class_names( + system_arguments[:classes], + "op-backlogs-collapsible--count" + ) + + Primer::Beta::Counter.new(**system_arguments) + } + + renders_one :description, ->(**system_arguments) { + system_arguments[:color] ||= :subtle + system_arguments[:hidden] = @collapsed + system_arguments[:classes] = class_names( + system_arguments[:classes], + "op-backlogs-collapsible--description" + ) + + Primer::Beta::Text.new(**system_arguments) + } + + def initialize(collapsible_id:, toggle_label:, collapsed: false, **system_arguments) + super() + + @collapsible_id = collapsible_id + @toggle_label = toggle_label + @collapsed = collapsed + + @system_arguments = deny_tag_argument(**system_arguments) + @system_arguments[:tag] = :"collapsible-header" + @system_arguments[:classes] = class_names( + system_arguments[:classes], + "CollapsibleHeader", + "CollapsibleHeader--collapsed" => @collapsed + ) + if @collapsed + @system_arguments[:data] = merge_data( + @system_arguments, { + data: { collapsed: @collapsed } + } + ) + end + end + end +end diff --git a/modules/backlogs/app/views/shared/_validation_errors.html.erb b/modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb similarity index 81% rename from modules/backlogs/app/views/shared/_validation_errors.html.erb rename to modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb index 8837b4d6ba77..7b8ba671461a 100644 --- a/modules/backlogs/app/views/shared/_validation_errors.html.erb +++ b/modules/backlogs/app/components/backlogs/sprint_page_header_component.html.erb @@ -1,4 +1,4 @@ -<%#-- copyright +<%# -- copyright OpenProject is an open source project management software. Copyright (C) the OpenProject GmbH @@ -25,12 +25,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. -++#%> +++# %> -<%= validation_errors.length > 1 ? t(:error_intro_plural) : t(:error_intro_singular) %> -
    - <%- validation_errors.each_full do |msg| %> -
  • <%= msg %>
  • - <%- end %> -
-<%= t(:error_outro) %> +<%= render(@page_header) do |header| %> + <% header.with_title_content(@sprint.name) %> + <% header.with_description { format_date_range(date_range) } %> + <% header.with_breadcrumbs(breadcrumb_items) %> + <%= content %> +<% end %> diff --git a/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb new file mode 100644 index 000000000000..60158874b0c2 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/sprint_page_header_component.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module Backlogs + class SprintPageHeaderComponent < ApplicationComponent + include ApplicationHelper + include RbCommonHelper + + delegate :with_action_button, to: :@page_header + + def initialize(sprint:, project:) + super + + @sprint = sprint + @project = project + + @page_header = Primer::OpenProject::PageHeader.new + end + + def breadcrumb_items + [{ href: project_overview_path(@project), text: @project.name }, + { href: backlogs_project_backlogs_path(@project), text: t(:label_backlogs) }, + @sprint.name] + end + + private + + def date_range + [@sprint.start_date, @sprint.effective_date] + end + end +end diff --git a/modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb b/modules/backlogs/app/components/backlogs/story_component.html.erb similarity index 50% rename from modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb rename to modules/backlogs/app/components/backlogs/story_component.html.erb index 77d2f4f29703..d18cd643c735 100644 --- a/modules/backlogs/app/views/rb_master_backlogs/_backlog.html.erb +++ b/modules/backlogs/app/components/backlogs/story_component.html.erb @@ -1,4 +1,4 @@ -<%#-- copyright +<%# -- copyright OpenProject is an open source project management software. Copyright (C) the OpenProject GmbH @@ -25,27 +25,42 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. -++#%> - -<% - folded = current_user.backlogs_preference(:versions_default_fold_state) == "closed" - editable = User.current.allowed_in_project?(:edit_work_packages, @project) -%> -
-
- <% icon = folded ? "icon-arrow-down1" : "icon-arrow-up1" %> -
">
- <%= render partial: "rb_sprints/sprint", object: backlog.sprint %> -
- <%= render_backlog_menu backlog %> -
-
    <%= " prevent_drag" unless editable %>"> - <% reset_cycle "stories" %> - <% backlog.stories.each_with_index do |story, index| %> - <% higher_item = index == 0 ? nil : backlog.stories[index - 1] %> - - <%= render partial: "rb_stories/story", - locals: { story:, higher_item: } %> +++# %> + +<%= grid_layout("op-backlogs-story", tag: :article) do |grid| %> + <% grid.with_area(:drag_handle, classes: "hide-when-print") do %> + <%= + render( + Primer::OpenProject::DragHandle.new( + role: "button", + classes: "op-backlogs-story--drag_handle_button", + tabindex: 0, + aria: { + label: t(".label_drag_story", name: story.subject) + } + ) + ) + %> + <% end %> + + <% grid.with_area(:info_line) do %> + <%= render(WorkPackages::InfoLineComponent.new(work_package: story)) %> + <% end %> + + <% grid.with_area(:points) do %> + <%= render(Primer::Beta::Text.new(color: :subtle)) do %> + <%= story_points %> + <%= t(:"backlogs.points_label", count: story_points) %> + <% end %> + <% end %> + + <% grid.with_area(:menu) do %> + <%= render(Backlogs::StoryMenuComponent.new(story:, sprint:, max_position:)) %> + <% end %> + + <% grid.with_area(:subject) do %> + <%= render(Primer::Beta::Text.new(font_weight: :semibold)) do %> + <%= story.subject %> <% end %> -
-
+ <% end %> +<% end %> diff --git a/modules/backlogs/app/components/backlogs/story_component.rb b/modules/backlogs/app/components/backlogs/story_component.rb new file mode 100644 index 000000000000..39c37f48a7f9 --- /dev/null +++ b/modules/backlogs/app/components/backlogs/story_component.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class StoryComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + attr_reader :story, :sprint, :max_position, :current_user + + def initialize(story:, sprint:, max_position:, current_user: User.current) + super() + + @story = story + @sprint = sprint + @max_position = max_position + @current_user = current_user + end + + private + + def story_points + story.story_points || 0 + end + end +end diff --git a/modules/backlogs/app/views/layouts/backlogs.html.erb b/modules/backlogs/app/components/backlogs/story_menu_component.html.erb similarity index 54% rename from modules/backlogs/app/views/layouts/backlogs.html.erb rename to modules/backlogs/app/components/backlogs/story_menu_component.html.erb index fb3aeec57ad3..617e70857335 100644 --- a/modules/backlogs/app/views/layouts/backlogs.html.erb +++ b/modules/backlogs/app/components/backlogs/story_menu_component.html.erb @@ -1,4 +1,4 @@ -<%#-- copyright +<%# -- copyright OpenProject is an open source project management software. Copyright (C) the OpenProject GmbH @@ -25,16 +25,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. -++#%> - -<% content_for :header_tags do %> - <%= frontend_stylesheet_link_tag "backlogs.css" %> -<% end %> - -<% content_for :additional_js_dom_ready do %> - <%= render(partial: "shared/server_variables", formats: [:js]) %> -<% end %> - -<% content_controller "backlogs" %> - -<%= render template: "layouts/base", locals: local_assigns.merge(turbo_opt_out: true) %> +++# %> + +<%= + render(Primer::Alpha::ActionMenu.new(anchor_align: :end, classes: "hide-when-print")) do |menu| + menu.with_show_button( + scheme: :invisible, + icon: :"kebab-horizontal", + "aria-label": t(".label_actions"), + tooltip_direction: :se + ) + + menu.with_item( + tag: :a, + label: t(:"js.button_open_details"), + href: details_backlogs_project_backlogs_path(project, story), + content_arguments: { turbo_frame: "content-bodyRight", turbo_action: "advance" } + ) do |item| + item.with_leading_visual_icon(icon: :"op-view-split") + end + + menu.with_item( + tag: :a, + label: t(:"js.button_open_fullscreen"), + href: work_package_path(story), + content_arguments: { turbo_frame: "_top" } + ) do |item| + item.with_leading_visual_icon(icon: :"screen-full") + end + + if show_move_items? + menu.with_divider + build_move_menu(menu) + end + end +%> diff --git a/modules/backlogs/app/components/backlogs/story_menu_component.rb b/modules/backlogs/app/components/backlogs/story_menu_component.rb new file mode 100644 index 000000000000..99581a19047f --- /dev/null +++ b/modules/backlogs/app/components/backlogs/story_menu_component.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class StoryMenuComponent < ApplicationComponent + attr_reader :story, :sprint, :project, :max_position, :current_user + + def initialize(story:, sprint:, max_position:, current_user: User.current) + super() + + @story = story + @sprint = sprint + @project = sprint.project + @max_position = max_position + @current_user = current_user + end + + private + + def show_move_items? + !(first_item? && last_item?) + end + + def build_move_menu(menu) + unless first_item? + build_move_item(menu, label: I18n.t(:label_sort_highest), direction: "highest", icon: :"move-to-top") + build_move_item(menu, label: I18n.t(:label_sort_higher), direction: "higher", icon: :"chevron-up") + end + unless last_item? + build_move_item(menu, label: I18n.t(:label_sort_lower), direction: "lower", icon: :"chevron-down") + build_move_item(menu, label: I18n.t(:label_sort_lowest), direction: "lowest", icon: :"move-to-bottom") + end + end + + def build_move_item(menu, label:, direction:, icon:) + menu.with_item( + label:, + tag: :button, + href: reorder_backlogs_project_sprint_story_path(project, sprint, story), + form_arguments: { method: :post, inputs: [{ name: "direction", value: direction }] } + ) do |item| + item.with_leading_visual_icon(icon:) + end + end + + def first_item? + story.position == 1 + end + + def last_item? + story.position == max_position + end + end +end diff --git a/modules/backlogs/app/controllers/backlogs_settings_controller.rb b/modules/backlogs/app/controllers/backlogs_settings_controller.rb index 09318d09c684..845c60b6b7f9 100644 --- a/modules/backlogs/app/controllers/backlogs_settings_controller.rb +++ b/modules/backlogs/app/controllers/backlogs_settings_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH diff --git a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb index 6fa3587d0fc8..b53f8be524b8 100644 --- a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb +++ b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -30,7 +32,7 @@ class Projects::Settings::BacklogsController < Projects::SettingsController menu_item :settings_backlogs def show - @statuses_done_for_project = @project.done_statuses.select(:id).map(&:id) + @statuses_done_for_project = @project.done_statuses.pluck(:id) end def update diff --git a/modules/backlogs/app/controllers/rb_application_controller.rb b/modules/backlogs/app/controllers/rb_application_controller.rb index 37e0502db725..944ce7c2452b 100644 --- a/modules/backlogs/app/controllers/rb_application_controller.rb +++ b/modules/backlogs/app/controllers/rb_application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -32,10 +34,6 @@ class RbApplicationController < ApplicationController before_action :load_sprint_and_project, :check_if_plugin_is_configured, :authorize - # Use special backlogs layout to initialize stimulus side-loading legacy backlogs scripts - # and CSS from frontend - layout "backlogs" - private # Loads the project to be used by the authorize filter to determine if diff --git a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb index de32109c5d48..914d1f0a2148 100644 --- a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb +++ b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH diff --git a/modules/backlogs/app/controllers/rb_impediments_controller.rb b/modules/backlogs/app/controllers/rb_impediments_controller.rb index d67c86b22dc7..1a95316f107c 100644 --- a/modules/backlogs/app/controllers/rb_impediments_controller.rb +++ b/modules/backlogs/app/controllers/rb_impediments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH diff --git a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb index 0602221ec5d5..dd6838e553ba 100644 --- a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb +++ b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,12 +29,35 @@ #++ class RbMasterBacklogsController < RbApplicationController + include WorkPackages::WithSplitView + menu_item :backlogs + before_action :load_backlogs, only: :index + def index + if turbo_frame_request? + render partial: "list", layout: false + else + render :index + end + end + + def details + if turbo_frame_request? + render "work_packages/split_view", layout: false + else + load_backlogs + render :index + end + end + + def split_view_base_route = backlogs_project_backlogs_path(request.query_parameters) + + private + + def load_backlogs @owner_backlogs = Backlog.owner_backlogs(@project) @sprint_backlogs = Backlog.sprint_backlogs(@project) - - @last_update = (@sprint_backlogs + @owner_backlogs).filter_map(&:updated_at).max end end diff --git a/modules/backlogs/app/controllers/rb_queries_controller.rb b/modules/backlogs/app/controllers/rb_queries_controller.rb index da11a8155037..eaf990c0fab1 100644 --- a/modules/backlogs/app/controllers/rb_queries_controller.rb +++ b/modules/backlogs/app/controllers/rb_queries_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH diff --git a/modules/backlogs/app/controllers/rb_sprints_controller.rb b/modules/backlogs/app/controllers/rb_sprints_controller.rb index 3603a756b444..60abfe686dc5 100644 --- a/modules/backlogs/app/controllers/rb_sprints_controller.rb +++ b/modules/backlogs/app/controllers/rb_sprints_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,30 +28,64 @@ # See COPYRIGHT and LICENSE files for more details. #++ -# Responsible for exposing sprint CRUD. It SHOULD NOT be used for displaying the -# taskboard since the taskboard is a management interface used for managing -# objects within a sprint. For info about the taskboard, see -# RbTaskboardsController class RbSprintsController < RbApplicationController + include OpTurbo::ComponentStream + + def edit_name + update_header_component_via_turbo_stream(state: :edit) + respond_with_turbo_streams + end + + def show_name + update_header_component_via_turbo_stream(state: :show) + respond_with_turbo_streams + end + def update - result = @sprint.update(params.permit(:name, - :start_date, - :effective_date)) - status = (result ? 200 : 400) + call = Versions::UpdateService + .new(user: current_user, model: @sprint) + .call(attributes: sprint_params) - respond_to do |format| - format.html { render partial: "sprint", status:, object: @sprint } + if call.success? + status = 200 + state = :show + @sprint = call.result + render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update)) + else + status = 422 + state = :edit + render_error_flash_message_via_turbo_stream( + message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message) + ) end + + update_header_component_via_turbo_stream(state:) + respond_with_turbo_streams(status:) end - # Overwrite load_sprint_and_project to load the sprint from the :id instead of - # :sprint_id + private + + def update_header_component_via_turbo_stream(state: :show) + @backlog = Backlog.for(sprint: @sprint, project: @project) + + update_via_turbo_stream( + component: Backlogs::BacklogHeaderComponent.new( + backlog: @backlog, + project: @project, + state: + ) + ) + end + + # Overrides load_sprint_and_project to load the sprint from :id instead of :sprint_id def load_sprint_and_project - if params[:id] - @sprint = Sprint.find(params[:id]) - @project = @sprint.project - end + @sprint = Sprint.visible.find(params[:id]) + @project = @sprint.project # This overrides sprint's project if we set another project, say a subproject - @project = Project.find(params[:project_id]) if params[:project_id] + @project = Project.visible.find(params[:project_id]) + end + + def sprint_params + params.expect(sprint: %i[name start_date effective_date]) end end diff --git a/modules/backlogs/app/controllers/rb_stories_controller.rb b/modules/backlogs/app/controllers/rb_stories_controller.rb index 3b6dbc3402a3..0b1048a2349c 100644 --- a/modules/backlogs/app/controllers/rb_stories_controller.rb +++ b/modules/backlogs/app/controllers/rb_stories_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,50 +29,75 @@ #++ class RbStoriesController < RbApplicationController - # This is a constant here because we will recruit it elsewhere to whitelist - # attributes. This is necessary for now as we still directly use `attributes=` - # in non-controller code. - PERMITTED_PARAMS = %i[id status_id version_id - story_points type_id subject author_id - sprint_id] - - def create - call = Stories::CreateService - .new(user: current_user) - .call(attributes: story_params, - prev: params[:prev]) - - respond_with_story(call) - end + include OpTurbo::ComponentStream + + before_action :load_story - def update - story = Story.find(params[:id]) + def move # rubocop:disable Metrics/AbcSize + # The update service reloads the story internally (via #move_after), + # so we memoize the previous version_id before the call. + version_id_was = @story.version_id call = Stories::UpdateService - .new(user: current_user, story:) - .call(attributes: story_params, - prev: params[:prev]) + .new(user: current_user, story: @story) + .call( + attributes: { version_id: move_params[:target_id] }, + position: move_params[:position].to_i + ) unless call.success? - # reload the story to be able to display it correctly - call.result.reload + render_error_flash_message_via_turbo_stream( + message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message) + ) + end + + replace_backlog_component_via_turbo_stream(sprint: @sprint) + + if @story.version_id != version_id_was + new_sprint = @story.version.becomes(Sprint) + + render_success_flash_message_via_turbo_stream( + message: I18n.t(:notice_successful_move, from: @sprint.name, to: new_sprint.name) + ) + replace_backlog_component_via_turbo_stream(sprint: new_sprint) end - respond_with_story(call) + respond_with_turbo_streams + end + + def reorder + call = Stories::UpdateService + .new(user: current_user, story: @story) + .call(attributes: { move_to: reorder_param }) + + unless call.success? + render_error_flash_message_via_turbo_stream( + message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message) + ) + end + + replace_backlog_component_via_turbo_stream(sprint: @sprint) + + respond_with_turbo_streams end private - def respond_with_story(call) - status = call.success? ? 200 : 400 - story = call.result + def replace_backlog_component_via_turbo_stream(sprint:) + @backlog = Backlog.for(sprint:, project: @project) + replace_via_turbo_stream(component: Backlogs::BacklogComponent.new(backlog: @backlog, project: @project)) + end - respond_to do |format| - format.html { render partial: "story", object: story, status:, locals: { errors: call.errors } } - end + def load_story + @story = Story.visible.find(params[:id]) + end + + def move_params + params.require(%i[position target_id]) + params.permit(:position, :target_id) end - def story_params - params.permit(PERMITTED_PARAMS).merge(project: @project).to_h + def reorder_param + params.expect(:direction) end end diff --git a/modules/backlogs/app/controllers/rb_taskboards_controller.rb b/modules/backlogs/app/controllers/rb_taskboards_controller.rb index 73cda7f7b903..05196ec200e1 100644 --- a/modules/backlogs/app/controllers/rb_taskboards_controller.rb +++ b/modules/backlogs/app/controllers/rb_taskboards_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH diff --git a/modules/backlogs/app/controllers/rb_tasks_controller.rb b/modules/backlogs/app/controllers/rb_tasks_controller.rb index d1bd84f1559b..5182715d05ff 100644 --- a/modules/backlogs/app/controllers/rb_tasks_controller.rb +++ b/modules/backlogs/app/controllers/rb_tasks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -31,12 +33,12 @@ class RbTasksController < RbApplicationController # attributes. This is necessary for now as we still directly use `attributes=` # in non-controller code. PERMITTED_PARAMS = ["id", "subject", "assigned_to_id", "remaining_hours", "parent_id", - "estimated_hours", "status_id", "sprint_id"] + "estimated_hours", "status_id", "sprint_id"].freeze def create call = ::Tasks::CreateService .new(user: current_user) - .call(attributes: task_params.merge(project: @project), prev: params[:prev]) + .call(attributes: task_params.merge(project: @project), prev_id: params[:prev]) respond_with_task call end @@ -46,7 +48,7 @@ def update call = ::Tasks::UpdateService .new(user: current_user, task:) - .call(attributes: task_params, prev: params[:prev]) + .call(attributes: task_params, prev_id: params[:prev]) respond_with_task call end diff --git a/modules/backlogs/app/controllers/rb_wikis_controller.rb b/modules/backlogs/app/controllers/rb_wikis_controller.rb index 4de3f05da697..7684363cffac 100644 --- a/modules/backlogs/app/controllers/rb_wikis_controller.rb +++ b/modules/backlogs/app/controllers/rb_wikis_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -32,10 +34,10 @@ class RbWikisController < RbApplicationController # # NOTE: The methods #show and #edit create a template page when called. def show - redirect_to controller: "/wiki", action: "index", project_id: @project.id, id: @sprint.wiki_page + redirect_to controller: "/wiki", action: "index", project_id: @project, id: @sprint.wiki_page end def edit - redirect_to controller: "/wiki", action: "edit", project_id: @project.id, id: @sprint.wiki_page + redirect_to controller: "/wiki", action: "edit", project_id: @project, id: @sprint.wiki_page end end diff --git a/modules/backlogs/app/forms/backlogs/backlog_header_form.rb b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb new file mode 100644 index 000000000000..f8b298d9411e --- /dev/null +++ b/modules/backlogs/app/forms/backlogs/backlog_header_form.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Backlogs + class BacklogHeaderForm < ApplicationForm + attr_reader :cancel_path + + form do |f| + f.text_field( + name: :name, + label: attribute_name(:name), + placeholder: attribute_name(:name), + visually_hide_label: true, + autofocus: true, + autocomplete: "off" + ) + + f.group(layout: :horizontal) do |dates| + dates.single_date_picker( + name: :start_date, + label: attribute_name(:start_date), + placeholder: attribute_name(:start_date), + visually_hide_label: true, + leading_visual: { icon: :calendar }, + datepicker_options: {} + ) + dates.single_date_picker( + name: :effective_date, + label: attribute_name(:effective_date), + placeholder: attribute_name(:effective_date), + visually_hide_label: true, + leading_visual: { icon: :calendar }, + datepicker_options: {} + ) + end + + f.group(layout: :horizontal) do |buttons| + buttons.submit(scheme: :primary, name: :submit, label: I18n.t(:button_save)) + buttons.button( + scheme: :secondary, + name: :cancel, + label: I18n.t(:button_cancel), + tag: :a, + data: { turbo_stream: true }, + href: cancel_path + ) + end + end + + def initialize(cancel_path:) + super() + + @cancel_path = cancel_path + end + end +end diff --git a/modules/backlogs/app/helpers/burndown_charts_helper.rb b/modules/backlogs/app/helpers/burndown_charts_helper.rb index 03c031eab721..1c087f111259 100644 --- a/modules/backlogs/app/helpers/burndown_charts_helper.rb +++ b/modules/backlogs/app/helpers/burndown_charts_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,52 +29,23 @@ #++ module BurndownChartsHelper - def yaxis_labels(burndown) - max = burndown.max[:points] - - mvalue = (max / 25) + 1 - - labels = (0..mvalue).map { |i| "[#{i * 25}, #{i * 25}]" } - - mvalue = mvalue + 1 if mvalue == 1 || ((max % 25) == 0) - - labels << "[#{mvalue * 25}, '#{I18n.t('backlogs.points')}']" - - result = labels.join(", ") - - result.html_safe - end - def xaxis_labels(burndown) # 14 entries (plus the axis label) have come along as the best value for a good optical result. # Thus it is enough space between the entries. entries_displayed = (burndown.days.length / 14.0).ceil - result = burndown.days.enum_for(:each_with_index).map do |d, i| + burndown.days.enum_for(:each_with_index).map do |d, i| if (i % entries_displayed) == 0 - "[#{i + 1}, '#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])} #{d.strftime('%d/%m')}']" + ["#{escape_javascript(::I18n.t('date.abbr_day_names')[d.wday % 7])} #{d.strftime('%d/%m')}"] end - end.join(",").html_safe + - ", [#{burndown.days.length + 1}, - '#{I18n.t('backlogs.date')}']".html_safe + end end def dataseries(burndown) - dataset = {} - burndown.series.each do |s| - dataset[s.first] = { - label: I18n.t("backlogs." + s.first.to_s), - data: s.last.enum_for(:each_with_index).map { |val, i| [i + 1, val] } + burndown.series.map do |s| + { + label: I18n.t("burndown.#{s.first}"), + data: s.last.enum_for(:each) } end - - dataset - end - - def burndown_series_checkboxes(burndown) - boxes = "" - burndown.series(:all).map { |s| s.first.to_s }.sort.each do |series| - boxes += "#{I18n.t('backlogs.' + series.to_s)}
" - end - boxes.html_safe end end diff --git a/modules/backlogs/app/helpers/rb_common_helper.rb b/modules/backlogs/app/helpers/rb_common_helper.rb index d25776d4fe73..b28a5891b700 100644 --- a/modules/backlogs/app/helpers/rb_common_helper.rb +++ b/modules/backlogs/app/helpers/rb_common_helper.rb @@ -27,6 +27,13 @@ #++ module RbCommonHelper + def format_date_range(dates) + return nil if dates.all?(&:nil?) + + from, to = dates.map { |date| tag.time(datetime: date.iso8601) { format_date(date) } if date } + safe_join([from, "–", to], " ") # – and   + end + def assignee_id_or_empty(story) story.assigned_to_id.to_s end @@ -57,14 +64,6 @@ def color_contrast_class(task) end end - # Return true if the difference between two colors - # matches the W3C recommendations for readability - # See http://www.wat-c.org/tools/CCA/1.1/ - def colors_diff_ok?(color_1, color_2) - cont, bright = find_color_diff color_1, color_2 - (cont > 500) && (bright > 125) # Acceptable diff according to w3c - end - def color_contrast(color) _, bright = find_color_diff 0x000000, color (bright > 128) @@ -95,18 +94,13 @@ def is_assigned_task?(task) def background_color_hex(task) background_color = get_backlogs_preference(task.assigned_to, :task_color) - background_color_hex = background_color.sub("#", "0x").hex + background_color.sub("#", "0x").hex end def id_or_empty(item) item.id.to_s end - def shortened_id(record) - id = record.id.to_s - (id.length > 8 ? "#{id[0..1]}...#{id[-4..-1]}" : id) - end - def work_package_link_or_empty(work_package) modal_link_to_work_package(work_package.id, work_package, class: "prevent_edit") unless work_package.new_record? end @@ -120,53 +114,14 @@ def modal_link_to(title, path, options = {}) link_to(title, path, options.merge(id: html_id, target: "_blank")) end - def sprint_link_or_empty(item) - item_id = item.id.to_s - text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id) - if item.new_record? - "" - else - link_to(text, backlogs_project_sprint_path(id: item.id, project_id: item.project.identifier), class: "prevent_edit") - end - end - def mark_if_closed(story) !story.new_record? && work_package_status_for_id(story.status_id).is_closed? ? "closed" : "" end - def story_points_or_empty(story) - story.story_points.to_s - end - - def status_id_or_default(story) - story.new_record? ? new_record_status.id : story.status_id - end - - def status_label_or_default(story) - story.new_record? ? new_record_status.name : h(work_package_status_for_id(story.status_id).name) - end - - def sprint_html_id_or_empty(sprint) - sprint.id.nil? ? "" : "sprint_#{sprint.id}" - end - def story_html_id_or_empty(story) story.id.nil? ? "" : "story_#{story.id}" end - def type_id_or_empty(story) - story.type_id.to_s - end - - def type_name_or_empty(story) - return "" if story.type_id.nil? - - type = backlogs_types_by_id[story.type_id] - return "" if type.nil? - - h(type.name) - end - def date_string_with_milliseconds(d, add = 0) return "" if d.blank? @@ -177,49 +132,8 @@ def remaining_hours(item) item.remaining_hours.blank? || item.remaining_hours == 0 ? "" : item.remaining_hours end - def available_story_types - @available_story_types ||= begin - types = story_types & @project.types if @project - - types - end - end - - def available_statuses_by_type - @available_statuses_by_type ||= begin - available_statuses_by_type = Hash.new do |type_hash, type| - type_hash[type] = Hash.new do |status_hash, status| - status_hash[status] = [status] - end - end - - all_workflows.each do |w| - type_status = available_statuses_by_type[story_types_by_id[w.type_id]][w.old_status] - - type_status << w.new_status unless type_status.include?(w.new_status) - end - - available_statuses_by_type - end - end - - def show_burndown_link(project, sprint) - link_to(I18n.t("backlogs.show_burndown_chart"), - backlogs_project_sprint_burndown_chart_path(project.identifier, sprint), - class: "show_burndown_chart button", - target: :_blank, rel: :noopener) - end - private - def new_record_status - @new_record_status ||= all_work_package_status.first - end - - def default_work_package_status - @default_work_package_status ||= all_work_package_status.detect(&:is_default) - end - def work_package_status_for_id(id) @all_work_package_status_by_id ||= all_work_package_status.inject({}) do |mem, status| mem[status.id] = status @@ -229,18 +143,6 @@ def work_package_status_for_id(id) @all_work_package_status_by_id[id] end - # Returns all distinct virtual workflows for the roles the current user has in the project and the story types. - # Virtual workflow because not every instance of a workflow in the database will be returned but a representation - # distinct by type_id, old_status_id and new_status_id. This helps in case a lot of workflows are configured. - def all_workflows - Workflow - .includes(%i[new_status old_status]) - .where(role_id: User.current.roles_for_project(@project).map(&:id), - type_id: story_types.map(&:id)) - .group(:type_id, :old_status_id, :new_status_id) - .reselect(:type_id, :old_status_id, :new_status_id) - end - def all_work_package_status @all_work_package_status ||= Status.order(Arel.sql("position ASC")) end @@ -254,13 +156,6 @@ def backlogs_types end end - def backlogs_types_by_id - @backlogs_types_by_id ||= backlogs_types.inject({}) do |mem, type| - mem[type.id] = type - mem - end - end - def story_types @story_types ||= begin backlogs_type_ids = Setting.plugin_openproject_backlogs["story_types"].map(&:to_i) @@ -269,20 +164,7 @@ def story_types end end - def story_types_by_id - @story_types_by_id ||= story_types.inject({}) do |mem, type| - mem[type.id] = type - mem - end - end - def get_backlogs_preference(assignee, attr) assignee.is_a?(User) ? assignee.backlogs_preference(attr) : "#24B3E7" end - - def template_story - Story.new.tap do |s| - s.type = available_story_types.first - end - end end diff --git a/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb b/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb deleted file mode 100644 index 89866fd1cff1..000000000000 --- a/modules/backlogs/app/helpers/rb_master_backlogs_helper.rb +++ /dev/null @@ -1,121 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module RbMasterBacklogsHelper - include Redmine::I18n - - def render_backlog_menu(backlog) - # associated javascript defined in taskboard.js - content_tag(:div, class: "backlog-menu") do - [ - content_tag(:div, "", class: "menu-trigger icon-context icon-pulldown icon-small"), - content_tag(:ul, class: "items") do - backlog_menu_items_for(backlog).map do |item| - content_tag(:li, item, class: "item") - end.join.html_safe - end - ].join.html_safe - end - end - - def backlog_menu_items_for(backlog) - items = common_backlog_menu_items_for(backlog) - - if backlog.sprint_backlog? - items.merge!(sprint_backlog_menu_items_for(backlog)) - end - - menu = [] - %i[new_story stories_tasks task_board burndown cards wiki configs properties].each do |key| - menu << items[key] if items.keys.include?(key) - end - - menu - end - - def common_backlog_menu_items_for(backlog) - items = {} - - if current_user.allowed_in_project?(:add_work_packages, @project) - items[:new_story] = content_tag(:a, - I18n.t("backlogs.add_new_story"), - href: "#", - class: "add_new_story") - end - - items[:stories_tasks] = link_to(I18n.t(:label_stories_tasks), - controller: "/rb_queries", - action: "show", - project_id: @project, - sprint_id: backlog.sprint) - - if current_user.allowed_in_project?(:manage_versions, @project) - items[:properties] = properties_link(backlog) - end - - items - end - - def properties_link(backlog) - back_path = backlogs_project_backlogs_path(@project) - - version_path = edit_version_path(backlog.sprint, back_url: back_path, project_id: @project.id) - - link_to(I18n.t(:"backlogs.properties"), version_path) - end - - def sprint_backlog_menu_items_for(backlog) - items = {} - - if current_user.allowed_in_project?(:view_taskboards, @project) - items[:task_board] = link_to(I18n.t(:label_task_board), - { controller: "/rb_taskboards", - action: "show", - project_id: @project, - sprint_id: backlog.sprint }, - class: "show_task_board") - end - - if backlog.sprint.has_burndown? - items[:burndown] = content_tag(:a, - I18n.t("backlogs.show_burndown_chart"), - href: "#", - class: "show_burndown_chart") - end - - if @project.module_enabled? "wiki" - items[:wiki] = link_to(I18n.t(:label_wiki), - controller: "/rb_wikis", - action: "edit", - project_id: @project, - sprint_id: backlog.sprint) - end - - items - end -end diff --git a/modules/backlogs/app/models/backlog.rb b/modules/backlogs/app/models/backlog.rb index 9dd2cbbaa898..482106b803cc 100644 --- a/modules/backlogs/app/models/backlog.rb +++ b/modules/backlogs/app/models/backlog.rb @@ -27,11 +27,18 @@ #++ class Backlog + extend ActiveModel::Naming + attr_accessor :sprint, :stories - def self.owner_backlogs(project, options = {}) - options.reverse_merge!(limit: nil) + delegate :id, to: :sprint, prefix: true + + def self.for(sprint:, project:) + owner_backlog = sprint.settings(project)&.display == VersionSetting::DISPLAY_RIGHT + new(sprint:, stories: sprint.stories(project), owner_backlog:) + end + def self.owner_backlogs(project) backlogs = Sprint.apply_to(project).with_status_open.displayed_right(project).order(:name) stories_by_sprints = Story.backlogs(project.id, backlogs.map(&:id)) @@ -47,11 +54,10 @@ def self.sprint_backlogs(project) sprints.map { |sprint| new(stories: stories_by_sprints[sprint.id], sprint:) } end - def initialize(options = {}) - options = options.with_indifferent_access - @sprint = options["sprint"] - @stories = options["stories"] - @owner_backlog = options["owner_backlog"] + def initialize(sprint:, stories:, owner_backlog: false) + @sprint = sprint + @stories = stories + @owner_backlog = owner_backlog end def updated_at @@ -65,4 +71,8 @@ def owner_backlog? def sprint_backlog? !owner_backlog? end + + def to_key + [sprint_id] + end end diff --git a/modules/backlogs/app/models/sprint.rb b/modules/backlogs/app/models/sprint.rb index 5cdb1f0f2454..cdb61d41a8d0 100644 --- a/modules/backlogs/app/models/sprint.rb +++ b/modules/backlogs/app/models/sprint.rb @@ -156,6 +156,10 @@ def impediments(project) Impediment.default_scope.where(version_id: self, project_id: project) end + def settings(project) + version_settings.find { it.project_id == project.id || it.project_id.nil? } + end + private def create_wiki_page(page_title, author: User.current) diff --git a/modules/backlogs/app/models/story.rb b/modules/backlogs/app/models/story.rb index d72ae19edf9a..6948b8dfdbca 100644 --- a/modules/backlogs/app/models/story.rb +++ b/modules/backlogs/app/models/story.rb @@ -29,11 +29,13 @@ class Story < WorkPackage extend OpenProject::Backlogs::Mixins::PreventIssueSti - def self.backlogs(project_id, sprint_ids, options = {}) + def self.backlogs(project_id, sprint_ids, options = {}) # rubocop:disable Metrics/AbcSize options.reverse_merge!(order: Story::ORDER, conditions: Story.condition(project_id, sprint_ids)) - candidates = Story.where(options[:conditions]).order(Arel.sql(options[:order])) + candidates = Story.where(options[:conditions]) + .includes(:status, :type) + .order(Arel.sql(options[:order])) stories_by_version = Hash.new do |hash, sprint_id| hash[sprint_id] = [] diff --git a/modules/backlogs/app/services/stories/create_service.rb b/modules/backlogs/app/services/stories/create_service.rb index f15a00de3f2d..d8c523914b93 100644 --- a/modules/backlogs/app/services/stories/create_service.rb +++ b/modules/backlogs/app/services/stories/create_service.rb @@ -33,13 +33,13 @@ def initialize(user:) self.user = user end - def call(attributes: {}, prev: nil) + def call(attributes: {}, position: nil) create_call = WorkPackages::CreateService .new(user:) .call(**attributes.symbolize_keys) if create_call.success? - create_call.result.move_after prev + create_call.result.move_after position: end create_call diff --git a/modules/backlogs/app/services/stories/update_service.rb b/modules/backlogs/app/services/stories/update_service.rb index 02f4862078f0..fd5f94f50d6e 100644 --- a/modules/backlogs/app/services/stories/update_service.rb +++ b/modules/backlogs/app/services/stories/update_service.rb @@ -34,14 +34,14 @@ def initialize(user:, story:) self.story = story end - def call(attributes: {}, prev: nil) + def call(attributes: {}, position: nil) create_call = WorkPackages::UpdateService .new(user:, model: story) - .call(**attributes.symbolize_keys) + .call(**attributes.to_h.symbolize_keys) - if create_call.success? && prev - create_call.result.move_after prev + if create_call.success? && position + create_call.result.move_after(position:) end create_call diff --git a/modules/backlogs/app/services/tasks/create_service.rb b/modules/backlogs/app/services/tasks/create_service.rb index 47591c790f0a..fd34ec2bb97f 100644 --- a/modules/backlogs/app/services/tasks/create_service.rb +++ b/modules/backlogs/app/services/tasks/create_service.rb @@ -33,7 +33,7 @@ def initialize(user:) self.user = user end - def call(attributes: {}, prev: "") + def call(attributes: {}, prev_id: "") attributes[:type_id] = Task.type create_call = WorkPackages::CreateService @@ -41,7 +41,7 @@ def call(attributes: {}, prev: "") .call(**attributes) if create_call.success? - create_call.result.move_after prev + create_call.result.move_after prev_id: end create_call diff --git a/modules/backlogs/app/services/tasks/update_service.rb b/modules/backlogs/app/services/tasks/update_service.rb index b936db0344f4..a249626d024f 100644 --- a/modules/backlogs/app/services/tasks/update_service.rb +++ b/modules/backlogs/app/services/tasks/update_service.rb @@ -34,14 +34,14 @@ def initialize(user:, task:) self.task = task end - def call(attributes: {}, prev: "") + def call(attributes: {}, prev_id: "") create_call = WorkPackages::UpdateService .new(user:, model: task) .call(**attributes) if create_call.success? - create_call.result.move_after prev + create_call.result.move_after prev_id: end create_call diff --git a/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb b/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb index 49424558d343..bb32c332dbf0 100644 --- a/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb +++ b/modules/backlogs/app/views/projects/settings/backlogs/show.html.erb @@ -31,8 +31,8 @@ See COPYRIGHT and LICENSE files for more details. render Primer::OpenProject::PageHeader.new do |header| header.with_title { t("backlogs.definition_of_done") } header.with_breadcrumbs( - [{ href: project_overview_path(@project.id), text: @project.name }, - { href: project_settings_general_path(@project.id), text: I18n.t(:label_project_settings) }, + [{ href: project_overview_path(@project), text: @project.name }, + { href: project_settings_general_path(@project), text: I18n.t(:label_project_settings) }, t("backlogs.definition_of_done")] ) end diff --git a/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb b/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb index 5dc27ca0548f..487f17ba88a5 100644 --- a/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb +++ b/modules/backlogs/app/views/rb_burndown_charts/_burndown.html.erb @@ -27,112 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= nonced_javascript_tag do %> - jQuery(function () { - var Burndown = { - datasets: <%= dataseries(burndown).to_json.html_safe %> , - previousPoint: null, - - setDatasetColor: function () { - var i = 0; - - jQuery.each(Burndown.datasets, function (key, val) { - val.color = i; - val.points = {show: false, radius: 2}; - val.lines = {show: true}; - ++i; - }); - }, - - plotAccordingToChoices: function () { - var data = []; - - jQuery('.burndown_control').find("input:checked").each(function () { - var key = jQuery(this).attr('value'); - - if (key && Burndown.datasets[key]) { - data.push(Burndown.datasets[key]); - } - }); - - if (data.length === 0) { //in order to render an empty graph if no data is selected - data.push({data : []}); - } - - Burndown.plot(data); - }, - - plot: function (data) { - if (data.length > 0) { - jQuery.plot(jQuery(".burndown_chart"), data, { - yaxis: { min: 0, - ticks: [ <%= yaxis_labels(burndown) %> ] }, - xaxis: { - ticks: [ <%= xaxis_labels(burndown) %> ], - tickDecimals: 0, - max: <%= burndown.days.length + 1 %>, - min: 1 - }, - grid: { hoverable: true, clickable: true } - }); - } - }, - - showTooltip: function(x, y, contents) { - jQuery('
' + contents + '
').css( { - position: 'absolute', - display: 'none', - top: y + 5, - left: x + 5, - border: '1px solid #fdd', - padding: '2px', - 'background-color': '#fee', - opacity: 0.80 - }).appendTo("body").css('z-index', 2000).fadeIn(200); - }, - - showTooltipOnHover: function (event, pos, item) { - - if (item) { - if (Burndown.previousPoint != item.dataIndex) { - Burndown.previousPoint = item.dataIndex; - - jQuery("#tooltip").remove(); - var x = item.datapoint[0].toFixed(0), - y = item.datapoint[1].toFixed(0); - - Burndown.showTooltip(item.pageX, item.pageY, - item.series.label + ": " + y); - } - } - else { - jQuery("#tooltip").remove(); - Burndown.previousPoint = null; - } - }, - - init: function () { - Burndown.setDatasetColor(); - - jQuery('.burndown_control input').click(Burndown.plotAccordingToChoices); - jQuery(".burndown_chart").bind("plothover", Burndown.showTooltipOnHover); - - Burndown.plotAccordingToChoices(); - }, - - saveInit: function() { - // Ensure jQuery.plot is defined before progressing. - // This static page might already be ready but the webpack required - // jquery.flot.js file might not be loaded yet. - - if (jQuery.plot) { - this.init(); - } else { - setTimeout(() => { this.saveInit()}, 50); - } - } - }; - - Burndown.saveInit(); - }); -<% end %> +<%= angular_component_tag "opce-burndown-chart", "chart-data": { + labels: xaxis_labels(@burndown), + datasets: dataseries(@burndown) + }.to_json %> diff --git a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb b/modules/backlogs/app/views/rb_burndown_charts/show.html.erb index 3282a40457d7..5d0524637451 100644 --- a/modules/backlogs/app/views/rb_burndown_charts/show.html.erb +++ b/modules/backlogs/app/views/rb_burndown_charts/show.html.erb @@ -27,19 +27,35 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -

- <%= "#{@sprint.name}: #{@sprint.start_date.present? ? I18n.l(@sprint.start_date) : ''} - #{@sprint.effective_date.present? ? I18n.l(@sprint.effective_date) : ''}" %> -

+<% html_title @sprint.name %> + +<%= content_for :header_tags do %> + +<% end -%> + +<%= + render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header| + header.with_action_button( + tag: :a, + href: backlogs_project_sprint_taskboard_path(@project, @sprint), + mobile_icon: :"op-view-cards", + mobile_label: t(:label_task_board), + aria: { label: t(:label_task_board) } + ) do |button| + button.with_leading_visual_icon(icon: :"op-view-cards") + t(:label_task_board) + end + end +%> <% if @burndown %> <%= render partial: "burndown", locals: { div: "burndown_", burndown: @burndown } %> - -
<%= t("backlogs.generating_chart") %>
- -
- <%= t("backlogs.chart_options") %> - <%= burndown_series_checkboxes(@burndown) %> -
<% else %> - <%= t("backlogs.no_burndown_data") %> + <%= + render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate| + blankslate.with_visual_icon(icon: :graph) + blankslate.with_heading(tag: :h2).with_content(t(".blankslate_title")) + blankslate.with_description_content(t(".blankslate_description")) + end + %> <% end %> diff --git a/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb b/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb new file mode 100644 index 000000000000..4295b3a50ac1 --- /dev/null +++ b/modules/backlogs/app/views/rb_master_backlogs/_list.html.erb @@ -0,0 +1,23 @@ + + <% if @owner_backlogs.empty? && @sprint_backlogs.empty? %> + <%= + render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate| + blankslate.with_visual_icon(icon: :versions) + blankslate.with_heading(tag: :h2).with_content(t(:backlogs_empty_title)) + + if current_user.allowed_in_project?(:manage_versions, @project) + blankslate.with_description_content(t(:backlogs_empty_action_text)) + end + end + %> + <% else %> +
+
+ <%= render(Backlogs::BacklogComponent.with_collection(@sprint_backlogs, project: @project)) %> +
+
+ <%= render(Backlogs::BacklogComponent.with_collection(@owner_backlogs, project: @project)) %> +
+
+ <% end %> +
diff --git a/modules/backlogs/app/views/rb_master_backlogs/index.html.erb b/modules/backlogs/app/views/rb_master_backlogs/index.html.erb index 0ecc2ab079ad..ee5f07a80e8b 100644 --- a/modules/backlogs/app/views/rb_master_backlogs/index.html.erb +++ b/modules/backlogs/app/views/rb_master_backlogs/index.html.erb @@ -27,50 +27,40 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -
<% html_title t(:label_backlogs) %> -<%= - render Primer::OpenProject::PageHeader.new do |header| - header.with_title { t(:label_backlogs) } - header.with_breadcrumbs( - [{ href: project_overview_path(@project.id), text: @project.name }, - t(:label_backlogs)] - ) - end -%> +<% content_controller "backlogs", + "backlogs-list-url-value": backlogs_project_backlogs_path(@project), + "backlogs-backlogs--story-outlet": "li[data-story]" %> -<%= render(Primer::OpenProject::SubHeader.new) do |subheader| - subheader.with_action_button( - scheme: :primary, - leading_icon: :plus, - label: I18n.t(:label_version_new), - tag: :a, - href: url_for({ controller: "/versions", action: "new", project_id: @project }) - ) do - t("activerecord.models.version") - end - end %> +<% content_for :content_header do %> + <%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_backlogs) } + header.with_breadcrumbs( + [{ href: project_overview_path(@project), text: @project.name }, + t(:label_backlogs)] + ) + end + %> -<% if (@owner_backlogs.empty? && @sprint_backlogs.empty?) %> - <%= no_results_box action_url: new_project_version_path(@project), - display_action: authorize_for("versions", "new"), - custom_title: t(:backlogs_empty_title), - custom_action_text: t(:backlogs_empty_action_text) %> + <%= render(Primer::OpenProject::SubHeader.new) do |subheader| + subheader.with_action_button( + scheme: :primary, + leading_icon: :plus, + label: I18n.t(:label_version_new), + tag: :a, + href: url_for({ controller: "/versions", action: "new", project_id: @project }) + ) do + t("activerecord.models.version") + end + end %> <% end %> -
-
-
- <%= render partial: "backlog", collection: @owner_backlogs %> -
-
- <%= render partial: "backlog", collection: @sprint_backlogs %> -
-
+<% content_for :content_body do %> + <%= render partial: "list" %> +<% end %> -
- <%= render partial: "rb_stories/helpers" %> -
<%= date_string_with_milliseconds(@last_update, 0.001) %>
-
-
+<% content_for :content_body_right do %> + <%= render(split_view_instance) if render_work_package_split_view? %> +<% end %> diff --git a/modules/backlogs/app/views/rb_sprints/_sprint.html.erb b/modules/backlogs/app/views/rb_sprints/_sprint.html.erb deleted file mode 100644 index af87f44bda95..000000000000 --- a/modules/backlogs/app/views/rb_sprints/_sprint.html.erb +++ /dev/null @@ -1,78 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -
-
-
<%= sprint_link_or_empty(sprint) %>
-
<%= id_or_empty(sprint) %>
-
- - <% editable = User.current.allowed_in_project?(:update_sprints, @project) ? "editable" : "" %> - -
-
<%= sprint.effective_date %>
-
<%= sprint.start_date %>
-
<%= sprint.name %>
-
- - <% if User.current.allowed_in_project?(:update_sprints, @project) %> -
- <%= angular_component_tag "opce-basic-single-date-picker", - inputs: { - value: sprint.effective_date, - inputClassNames: "effective_date editor", - id: "effective_date_#{sprint.id}", - name: :effective_date - } %> - <%= angular_component_tag "opce-basic-single-date-picker", - inputs: { - value: sprint.start_date, - inputClassNames: "start_date editor", - id: "start_date_#{sprint.id}", - name: :start_date - } %> - <%= text_field_tag :name, - sprint.name, - class: "name editor" %> -
- <% end %> - -
- <%= render partial: "shared/model_errors", object: sprint.errors %> -
-
diff --git a/modules/backlogs/app/views/rb_stories/_helpers.html.erb b/modules/backlogs/app/views/rb_stories/_helpers.html.erb deleted file mode 100644 index 34816d40855f..000000000000 --- a/modules/backlogs/app/views/rb_stories/_helpers.html.erb +++ /dev/null @@ -1,75 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - - -<% available_statuses_by_type.each do |type, statuses| %> - <% statuses.each do |old_status, allowed_statuses| %> - - <% end %> -<% end %> - -<% all_work_package_status.each do |old_status| %> - -<% end %> - - - - - -
- <%= render partial: "rb_stories/story", object: template_story, locals: { project: @project, permission: :add_work_packages } %> -
diff --git a/modules/backlogs/app/views/rb_stories/_story.html.erb b/modules/backlogs/app/views/rb_stories/_story.html.erb deleted file mode 100644 index 0f791ab2aacd..000000000000 --- a/modules/backlogs/app/views/rb_stories/_story.html.erb +++ /dev/null @@ -1,79 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<% - project ||= story.project - permission ||= :edit_work_packages - other_fields_editable = User.current.allowed_in_project?(permission, project) - status_field_editable = other_fields_editable || User.current.allowed_in_project?(:change_work_package_status, project) -%> -
  • " id="<%= story_html_id_or_empty(story) %>"> -
    -
    <%= work_package_link_or_empty(story) %>
    -
    <%= id_or_empty(story) %>
    -
    -
    <%= story_points_or_empty(story) %>
    -
    -
    <%= status_label_or_default(story) %>
    -
    <%= status_id_or_default(story) %>
    -
    -
    -
    <%= type_name_or_empty(story) %>:
    -
    <%= type_id_or_empty(story) %>
    -
    -
    <%= story.subject %>
    -
    <%= story.version_id %>
    -
    <%= !defined?(higher_item) || higher_item.nil? ? "" : higher_item.id %>
    -
    - <%= render(partial: "shared/model_errors", object: errors) if defined?(errors) && !errors.empty? %> -
    -
  • diff --git a/modules/backlogs/app/views/rb_taskboards/show.html.erb b/modules/backlogs/app/views/rb_taskboards/show.html.erb index 7416de928d27..23dc8eb7a2d9 100644 --- a/modules/backlogs/app/views/rb_taskboards/show.html.erb +++ b/modules/backlogs/app/views/rb_taskboards/show.html.erb @@ -27,33 +27,50 @@ See COPYRIGHT and LICENSE files for more details. ++#%> +<% content_for :additional_js_dom_ready do %> + <%= render(partial: "shared/server_variables", formats: [:js]) %> +<% end %> + +<% content_controller "backlogs--taskboard-legacy" %> + <% html_title @sprint.name %> + <%= - render Primer::OpenProject::PageHeader.new do |header| - header.with_title { @sprint.name } - header.with_breadcrumbs( - [{ href: project_overview_path(@project.id), text: @project.name }, - { href: backlogs_project_backlogs_path(@project), text: t(:label_backlogs) }, - @sprint.name] - ) + render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header| + header.with_action_button( + tag: :a, + href: backlogs_project_sprint_burndown_chart_path(@project, @sprint), + mobile_icon: :graph, + mobile_label: t(:"backlogs.show_burndown_chart"), + aria: { label: t(:"backlogs.show_burndown_chart") }, + disabled: !@sprint.has_burndown? + ) do |button| + button.with_leading_visual_icon(icon: :graph) + t(:"backlogs.show_burndown_chart") + end end %> -<%# we decided to keep current toolbar design for taskboard %> -
    -
    -
  • -
    - -
    - -
  • - <% if @sprint.has_burndown? %> -
  • - <%= show_burndown_link(@project, @sprint) %> -
  • + +<%= render(Primer::OpenProject::SubHeader.new) do |component| %> + <% component.with_filter_component(id: "col_width") do %> + <%= + render( + Primer::Alpha::TextField.new( + name: :col_width_input, + type: :number, + label: t(:"backlogs.column_width"), + placeholder: t(:"backlogs.column_width"), + visually_hide_label: true, + leading_visual: { icon: :"zoom-in" }, + step: 1, + input_width: :xsmall, + autocomplete: "off" + ) + ) + %> <% end %> -
    -
    +<% end %> +
    diff --git a/modules/backlogs/app/views/shared/_server_variables.js.erb b/modules/backlogs/app/views/shared/_server_variables.js.erb index d03965ee336b..2c3199bbe297 100644 --- a/modules/backlogs/app/views/shared/_server_variables.js.erb +++ b/modules/backlogs/app/views/shared/_server_variables.js.erb @@ -36,25 +36,15 @@ RB.constants = { sprint_id: <%= @sprint ? @sprint.id : "null" %> }; -RB.i18n = { - generating_graph: '<%= j I18n.t("backlogs.generating_chart").html_safe %>', - burndown_graph: '<%= j I18n.t("backlogs.burndown_graph").html_safe %>' -}; - RB.urlFor = (function () { const routes = { - update_sprint: '<%= backlogs_project_sprint_path(project_id: @project.identifier, id: ":id") %>', - - create_story: '<%= backlogs_project_sprint_stories_path(project_id: @project.identifier, sprint_id: ":sprint_id") %>', - update_story: '<%= backlogs_project_sprint_story_path(project_id: @project.identifier, sprint_id: ":sprint_id", id: ":id") %>', - - create_task: '<%= backlogs_project_sprint_tasks_path(project_id: @project.identifier, sprint_id: ":sprint_id") %>', - update_task: '<%= backlogs_project_sprint_task_path(project_id: @project.identifier, sprint_id: ":sprint_id", id: ":id") %>', + update_sprint: '<%= backlogs_project_sprint_path(project_id: @project, id: ":id") %>', - create_impediment: '<%= backlogs_project_sprint_impediments_path(project_id: @project.identifier, sprint_id: ":sprint_id") %>', - update_impediment: '<%= backlogs_project_sprint_impediment_path(project_id: @project.identifier, sprint_id: ":sprint_id", id: ":id") %>', + create_task: '<%= backlogs_project_sprint_tasks_path(project_id: @project, sprint_id: ":sprint_id") %>', + update_task: '<%= backlogs_project_sprint_task_path(project_id: @project, sprint_id: ":sprint_id", id: ":id") %>', - show_burndown_chart: '<%= backlogs_project_sprint_burndown_chart_path(project_id: @project.identifier, sprint_id: ":sprint_id") %>' + create_impediment: '<%= backlogs_project_sprint_impediments_path(project_id: @project, sprint_id: ":sprint_id") %>', + update_impediment: '<%= backlogs_project_sprint_impediment_path(project_id: @project, sprint_id: ":sprint_id", id: ":id") %>' }; return function (routeName, options) { diff --git a/modules/backlogs/app/views/shared/not_configured.html.erb b/modules/backlogs/app/views/shared/not_configured.html.erb index 098b7924ebeb..03689ecfd3bd 100644 --- a/modules/backlogs/app/views/shared/not_configured.html.erb +++ b/modules/backlogs/app/views/shared/not_configured.html.erb @@ -1,4 +1,4 @@ -<%#-- copyright +<%# -- copyright OpenProject is an open source project management software. Copyright (C) the OpenProject GmbH @@ -25,13 +25,31 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. -++#%> - -
    - <%= t( - :label_backlogs_unconfigured, - administration: t(:label_administration), - plugins: t(:label_plugins), - configure: t(:button_configure) - ) %> -
    +++# %> + +<% html_title t(:label_backlogs) %> + +<% content_for :content_header do %> + <%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_backlogs) } + header.with_breadcrumbs( + [{ href: project_overview_path(@project), text: @project.name }, + t(:label_backlogs)] + ) + end + %> +<% end %> + +<% content_for :content_body do %> + <%= + render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate| + blankslate.with_visual_icon(icon: :"op-backlogs") + blankslate.with_heading(tag: :h2).with_content(t(:backlogs_not_configured_title)) + blankslate.with_description_content(t(:backlogs_not_configured_description)) + blankslate.with_secondary_action(href: admin_backlogs_settings_path, scheme: :default) do + t(:backlogs_not_configured_action_text) + end + end + %> +<% end %> diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index 17a4515f2fff..dbeee1707a24 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -34,6 +34,8 @@ en: activerecord: attributes: + sprint: + duration: "Sprint duration" work_package: position: "Position" story_points: "Story Points" @@ -55,145 +57,112 @@ en: task_type: "Task type" backlogs: - add_new_story: "New Story" any: "any" - backlog_settings: "Backlogs settings" - burndown_graph: "Burndown Graph" - card_paper_size: "Paper size for card printing" - chart_options: "Chart options" - close: "Close" - column_width: "Column width:" - date: "Day" + column_width: "Column width" definition_of_done: "Definition of Done" - generating_chart: "Generating Graph..." - hours: "Hours" impediment: "Impediment" label_versions_default_fold_state: "Show versions folded" caption_versions_default_fold_state: "Versions will not be expanded by default when viewing backlogs. Each one has to be manually expanded." work_package_is_closed: "Work package is done, when" label_is_done_status: "Status %{status_name} means done" - no_burndown_data: "No burndown data available. It is necessary to have the sprint start- and end dates set." - points: "Points" + points_label: + one: "point" + other: "points" positions_could_not_be_rebuilt: "Positions could not be rebuilt." positions_rebuilt_successfully: "Positions rebuilt successfully." - properties: "Properties" rebuild: "Rebuild" rebuild_positions: "Rebuild positions" remaining_hours: "Remaining work" - remaining_hours_ideal: "Remaining work (ideal)" show_burndown_chart: "Burndown Chart" story: "Story" - story_points: "Story Points" - story_points_ideal: "Story Points (ideal)" + story_points: + one: "%{count} story point" + other: "%{count} story points" task: "Task" task_color: "Task color" unassigned: "Unassigned" user_preference: header_backlogs: "Backlogs module" button_update_backlogs: "Update backlogs module" - x_more: "%{count} more..." - backlogs_active: "active" - backlogs_any: "any" - backlogs_inactive: "Project shows no activity" + backlog_component: + blankslate_title: "%{name} is empty" + blankslate_description: "No items planned yet. Drag items here to add them." + + backlog_header_component: + label_toggle_backlog: "Collapse/Expand %{name}" + label_story_count: + zero: "No stories in backlog" + one: "%{count} story in backlog" + other: "%{count} stories in backlog" + + backlog_menu_component: + label_actions: "Backlog actions" + action_menu: + edit_sprint: "Edit sprint" + new_story: "New story" + stories_tasks: "Stories/Tasks" + task_board: "Task board" + burndown_chart: "Burndown chart" + wiki: "Wiki" + properties: "Properties" + + story_component: + label_drag_story: "Move %{name}" + + story_menu_component: + label_actions: "Story actions" + backlogs_points_burn_direction: "Points burn up/down" backlogs_product_backlog: "Product backlog" - backlogs_product_backlog_is_empty: "Product backlog is empty" - backlogs_product_backlog_unsized: "The top of the product backlog has unsized stories" - backlogs_sizing_inconsistent: "Story sizes vary against their estimates" - backlogs_sprint_notes_missing: "Closed sprints without retrospective/review notes" - backlogs_sprint_unestimated: "Closed or active sprints with unestimated stories" - backlogs_sprint_unsized: "Project has stories on active or recently closed sprints that were not sized" - backlogs_sprints: "Sprints" backlogs_story: "Story" backlogs_story_type: "Story types" backlogs_task: "Task" backlogs_task_type: "Task type" - backlogs_velocity_missing: "No velocity could be calculated for this project" - backlogs_velocity_varies: "Velocity varies significantly over sprints" backlogs_wiki_template: "Template for sprint wiki page" - backlogs_empty_title: "No versions are defined to be used in backlogs" - backlogs_empty_action_text: "To get started with backlogs, create a new version and assign it to a backlogs column." + backlogs_empty_title: "No versions are defined yet" + backlogs_empty_action_text: "To start using backlogs, please create a version first" + backlogs_not_configured_title: "Backlogs not configured" + backlogs_not_configured_description: "Story and task types need to be set before using this module." + backlogs_not_configured_action_text: "Configure Backlogs" - button_edit_wiki: "Edit wiki page" + burndown: + story_points: "Story points" + story_points_ideal: "Story points (ideal)" errors: attributes: task_type: cannot_be_story_type: "can not also be a story type" - error_intro_plural: "The following errors were encountered:" - error_intro_singular: "The following error was encountered:" - error_outro: "Please correct the above errors before submitting again." - - event_sprint_description: "%{summary}: %{url}\n%{description}" - event_sprint_summary: "%{project}: %{summary}" - - ideal: "ideal" - - inclusion: "is not included in the list" - - label_back_to_project: "Back to project page" - label_backlog: "Backlog" label_backlogs: "Backlogs" label_backlogs_unconfigured: "You have not configured Backlogs yet. Please go to %{administration} > %{plugins}, then click on the %{configure} link for this plugin. Once you have set the fields, come back to this page to start using the tool." label_blocks_ids: "IDs of blocked work packages" - label_burndown: "Burndown" label_column_in_backlog: "Column in backlog" - label_hours: "hours" - label_work_package_hierarchy: "Work package Hierarchy" - label_master_backlog: "Master Backlog" - label_not_prioritized: "not prioritized" - label_points: "points" label_points_burn_down: "Down" label_points_burn_up: "Up" - label_product_backlog: "product backlog" - label_select_all: "Select all" label_select_type: "Select a type" label_select_types: "Select types" label_selected_type: "Selected type" label_selected_types: "Selected types" - label_sprint_backlog: "sprint backlog" - label_sprint_cards: "Export cards" label_sprint_impediments: "Sprint Impediments" - label_sprint_name: "Sprint \"%{name}\"" - label_sprint_velocity: "Velocity %{velocity}, based on %{sprints} sprints with an average %{days} days" - label_stories: "Stories" - label_stories_tasks: "Stories/Tasks" label_task_board: "Task board" - label_version_setting: "Versions" - label_version: 'Version' - label_webcal: "Webcal Feed" - label_wiki: "Wiki" permission_view_master_backlog: "View master backlog" permission_view_taskboards: "View taskboards" permission_select_done_statuses: "Select done statuses" permission_update_sprints: "Update sprints" - points_accepted: "points accepted" - points_committed: "points committed" - points_resolved: "points resolved" - points_to_accept: "points not accepted" - points_to_resolve: "points not resolved" - project_module_backlogs: "Backlogs" - rb_label_copy_tasks: "Copy work packages" - rb_label_copy_tasks_all: "All" - rb_label_copy_tasks_none: "None" - rb_label_copy_tasks_open: "Open" - rb_label_link_to_original: "Include link to original story" + rb_burndown_charts: + show: + blankslate_title: "No burndown data available" + blankslate_description: "Set start and end date for the sprint to generate a burndown chart." remaining_hours: "remaining work" - required_burn_rate_hours: "required burn rate (hours)" - required_burn_rate_points: "required burn rate (points)" - - todo_work_package_description: "%{summary}: %{url}\n%{description}" - todo_work_package_summary: "%{type}: %{summary}" - version_settings_display_label: "Column in backlog" version_settings_display_option_left: "left" version_settings_display_option_none: "none" diff --git a/modules/backlogs/config/locales/js-en.yml b/modules/backlogs/config/locales/js-en.yml index de420c5e3583..059e83f9b75e 100644 --- a/modules/backlogs/config/locales/js-en.yml +++ b/modules/backlogs/config/locales/js-en.yml @@ -31,3 +31,6 @@ en: work_packages: properties: storyPoints: "Story Points" + burndown: + day: "Day" + points: "Points" diff --git a/modules/backlogs/config/routes.rb b/modules/backlogs/config/routes.rb index e3a3bb299459..ee9d1aba9068 100644 --- a/modules/backlogs/config/routes.rb +++ b/modules/backlogs/config/routes.rb @@ -29,9 +29,17 @@ Rails.application.routes.draw do scope "", as: "backlogs" do scope "projects/:project_id", as: "project" do - resources :backlogs, controller: :rb_master_backlogs, only: :index + resources :backlogs, controller: :rb_master_backlogs, only: :index do + collection do + get "details/:work_package_id(/:tab)", + action: :details, + as: :details, + work_package_split_view: true, + defaults: { tab: :overview } + end + end - resources :sprints, controller: :rb_sprints, only: %i[show update] do + resources :sprints, controller: :rb_sprints, only: %i[update] do resource :query, controller: :rb_queries, only: :show resource :taskboard, controller: :rb_taskboards, only: :show @@ -44,7 +52,17 @@ resources :tasks, controller: :rb_tasks, only: %i[create update] - resources :stories, controller: :rb_stories, only: %i[create update] + resources :stories, controller: :rb_stories, only: [] do + member do + put :move + post :reorder + end + end + + member do + get :edit_name + get :show_name + end end resource :query, controller: :rb_queries, only: :show diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 3d9ee5b82716..18210c6e1e00 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -54,27 +54,23 @@ def self.settings settings:) do Rails.application.reloader.to_prepare do OpenProject::AccessControl.permission(:add_work_packages).tap do |add| - add.controller_actions << "rb_stories/create" add.controller_actions << "rb_tasks/create" add.controller_actions << "rb_impediments/create" end OpenProject::AccessControl.permission(:edit_work_packages).tap do |edit| - edit.controller_actions << "rb_stories/update" + edit.controller_actions << "rb_stories/move" + edit.controller_actions << "rb_stories/reorder" edit.controller_actions << "rb_tasks/update" edit.controller_actions << "rb_impediments/update" end - - OpenProject::AccessControl.permission(:change_work_package_status).tap do |edit| - edit.controller_actions << "rb_stories/update" - end end project_module :backlogs, dependencies: :work_package_tracking do # Master backlog permissions permission :view_master_backlog, - { rb_master_backlogs: :index, - rb_sprints: %i[index show], + { rb_master_backlogs: %i[index details], + rb_sprints: %i[index show show_name], rb_wikis: :show, rb_stories: %i[index show], rb_queries: :show, @@ -102,7 +98,7 @@ def self.settings # :show_sprints and :list_sprints are implicit in :view_master_backlog permission permission :update_sprints, { - rb_sprints: %i[edit update], + rb_sprints: %i[edit_name update], rb_wikis: %i[edit update] }, permissible_on: :project, diff --git a/modules/backlogs/lib/open_project/backlogs/list.rb b/modules/backlogs/lib/open_project/backlogs/list.rb index 0a593a33b80c..e884a450b5e5 100644 --- a/modules/backlogs/lib/open_project/backlogs/list.rb +++ b/modules/backlogs/lib/open_project/backlogs/list.rb @@ -53,28 +53,28 @@ def scope_condition end module InstanceMethods - def move_after(prev_id) + def move_after(position: nil, prev_id: nil) + if acts_as_list_list.none?(:position) + # If no items have a position, create an order on position + # silently. This can happen when sorting inside a version for the first + # time after backlogs was activated and there have already been items + # inside the version at the time of backlogs activation + set_default_prev_positions_silently(acts_as_list_list.last) + end + # Remove so the potential 'prev' has a correct position remove_from_list reload + id_or_position = position ? { position: position - 1 } : { id: prev_id } - prev = self.class.find_by(id: prev_id.to_i) + prev = acts_as_list_list.find_by(**id_or_position) - # If it should be the first story, move it to the 1st position if prev.blank? + # If it should be the first story, move it to the 1st position insert_at move_to_top - - # If its predecessor has no position, create an order on position - # silently. This can happen when sorting inside a version for the first - # time after backlogs was activated and there have already been items - # inside the version at the time of backlogs activation - elsif !prev.in_list? - prev_pos = set_default_prev_positions_silently(prev) - insert_at(prev_pos += 1) - - # There's a valid predecessor else + # There's a valid predecessor insert_at(prev.position + 1) end end @@ -148,6 +148,8 @@ def fix_own_work_package_position end def set_default_prev_positions_silently(prev) + return if prev.nil? + if prev.is_task? prev.version.rebuild_task_positions(prev) else diff --git a/modules/backlogs/lib/open_project/backlogs/patches/base_contract_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/base_contract_patch.rb index 0da6bb57435c..70eb1d3aadba 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/base_contract_patch.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/base_contract_patch.rb @@ -31,5 +31,6 @@ module OpenProject::Backlogs::Patches::BaseContractPatch included do attribute :story_points + attribute :position end end diff --git a/modules/backlogs/spec/components/backlogs/backlog_component_spec.rb b/modules/backlogs/spec/components/backlogs/backlog_component_spec.rb new file mode 100644 index 000000000000..b6f759bf3ee8 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/backlog_component_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::BacklogComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:default_status) { create(:default_status) } + shared_let(:default_priority) { create(:default_priority) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project, types: [type_feature, type_task]) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) } + let(:stories) { [] } + let(:backlog) { Backlog.new(sprint:, stories:) } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s) + + allow(user).to receive(:backlogs_preference).with(:versions_default_fold_state).and_return("open") + end + + def render_component + render_inline(described_class.new(backlog:, project:, current_user: user)) + end + + describe "rendering" do + context "with stories" do + let(:story1) do + create(:story, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: 5, + position: 1, + version: sprint) + end + let(:story2) do + create(:story, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: 3, + position: 2, + version: sprint) + end + let(:stories) { [story1, story2] } + + it "renders a Primer::Beta::BorderBox" do + render_component + + expect(page).to have_css(".Box") + end + + it "has the sprint ID in the DOM id" do + render_component + + expect(page).to have_css(".Box#backlog_#{sprint.id}") + end + + it "renders BacklogHeaderComponent in header" do + render_component + + expect(page).to have_css(".Box-header h3", text: "Sprint 1") + end + + it "renders StoryComponent for each story" do + render_component + + expect(page).to have_css(".Box-row", count: 2) # 2 stories + expect(page).to have_text(story1.subject) + expect(page).to have_text(story2.subject) + end + + it "has drop target data attributes" do + render_component + + box = page.find(".Box") + expect(box["data-target-id"]).to eq(sprint.id.to_s) + expect(box["data-target-allowed-drag-type"]).to eq("story") + end + + it "has draggable data attributes on story rows" do + render_component + + story_row = page.find(".Box-row[id='story_#{story1.id}']") + expect(story_row["data-draggable-id"]).to eq(story1.id.to_s) + expect(story_row["data-draggable-type"]).to eq("story") + expect(story_row["data-drop-url"]).to include("move") + end + + it "renders story rows with proper classes" do + render_component + + story_row = page.find(".Box-row[id='story_#{story1.id}']") + expect(story_row[:class]).to include("Box-row--hover-blue") + expect(story_row[:class]).to include("Box-row--focus-gray") + expect(story_row[:class]).to include("Box-row--clickable") + end + end + + context "without stories" do + let(:stories) { [] } + let(:rendered_component) { render_component } + + it_behaves_like "rendering Blank Slate", heading: "Sprint 1 is empty" + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/backlog_header_component_spec.rb b/modules/backlogs/spec/components/backlogs/backlog_header_component_spec.rb new file mode 100644 index 000000000000..40c25c764c3c --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/backlog_header_component_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::BacklogHeaderComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:default_status) { create(:default_status) } + shared_let(:default_priority) { create(:default_priority) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project, types: [type_feature, type_task]) } + let(:start_date) { Date.new(2024, 1, 15) } + let(:effective_date) { Date.new(2024, 1, 29) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date:) } + let(:stories) { [] } + let(:backlog) { Backlog.new(sprint:, stories:) } + let(:state) { :show } + let(:folded) { false } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s) + end + + def render_component(state: :show, folded: false) + render_inline(described_class.new(backlog:, project:, state:, folded:, current_user: user)) + end + + describe "show state (default)" do + context "with stories" do + let(:story1) do + create(:story, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: 5, + version: sprint) + end + let(:story2) do + create(:story, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: 3, + version: sprint) + end + let(:story_with_nil_points) do + create(:story, + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: nil, + version: sprint) + end + let(:stories) { [story1, story2, story_with_nil_points] } + + it "displays sprint name in h4" do + render_component + + expect(page).to have_css("h3", text: "Sprint 1") + end + + it "shows story count via Primer::Beta::Counter" do + render_component + + expect(page).to have_css(".Counter", text: "3") + end + + it "shows formatted date range with time tags" do + render_component + + expect(page).to have_css("time[datetime='2024-01-15']") + expect(page).to have_css("time[datetime='2024-01-29']") + end + + it "shows story points total (nil treated as 0)" do + render_component + + # 5 + 3 + 0 = 8 points + expect(page).to have_text("8 points", normalize_ws: true) + end + + it "renders collapse/expand chevrons" do + render_component + + expect(page).to have_octicon(:"chevron-up", visible: :all) + expect(page).to have_octicon(:"chevron-down", visible: :all) + end + + it "renders BacklogMenuComponent" do + render_component + + expect(page).to have_css("action-menu") + end + end + + context "with no stories" do + let(:stories) { [] } + + it "shows 0 story count" do + render_component + + expect(page).to have_css(".Counter", text: "0") + end + + it "shows 0 points" do + render_component + + expect(page).to have_text("0 points", normalize_ws: true) + end + end + + context "when sprint has no dates" do + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) } + + it "renders without date range" do + render_component + + expect(page).to have_no_css("time") + end + end + end + + describe "folded state" do + context "when folded is true" do + it "renders chevron-up hidden and chevron-down visible" do + render_component(folded: true) + + # When folded, chevron-up is hidden (has hidden attribute on svg) + # and chevron-down is visible (for expanding) + expect(page).to have_css("svg[hidden][data-target='collapsible-header.arrowUp']", visible: :hidden) + expect(page).to have_css("svg[data-target='collapsible-header.arrowDown']:not([hidden])", visible: :all) + end + end + + context "when folded is false" do + it "renders chevron-down hidden and chevron-up visible" do + render_component(folded: false) + + # When expanded, chevron-down is hidden (has hidden attribute) + # and chevron-up is visible (for collapsing) + expect(page).to have_css("svg[hidden][data-target='collapsible-header.arrowDown']", visible: :hidden) + expect(page).to have_css("svg[data-target='collapsible-header.arrowUp']:not([hidden])", visible: :all) + end + end + end + + describe "edit state" do + it "renders a form" do + render_component(state: :edit) + + expect(page).to have_css("form") + end + + it "renders text field for name" do + render_component(state: :edit) + + expect(page).to have_field(Sprint.human_attribute_name(:name), with: "Sprint 1") + end + + it "renders date picker components" do + render_component(state: :edit) + + # Date pickers have calendar icons as leading visuals + expect(page).to have_octicon(:calendar, count: 2) + end + + it "shows Save button" do + render_component(state: :edit) + + expect(page).to have_button(I18n.t(:button_save)) + end + + it "shows Cancel button" do + render_component(state: :edit) + + expect(page).to have_link(I18n.t(:button_cancel)) + end + end + + describe "state validation" do + it "raises an InvalidValueError for invalid state values" do + expect { render_component(state: :invalid) } + .to raise_error(Primer::FetchOrFallbackHelper::InvalidValueError) + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/backlog_menu_component_spec.rb b/modules/backlogs/spec/components/backlogs/backlog_menu_component_spec.rb new file mode 100644 index 000000000000..a5273fbf06d5 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/backlog_menu_component_spec.rb @@ -0,0 +1,207 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::BacklogMenuComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + + let(:project) { create(:project, types: [type_feature, type_task]) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) } + let(:stories) { [] } + let(:backlog) { Backlog.new(sprint:, stories:) } + let(:user) { create(:user) } + let(:permissions) { [] } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s) + + # Set up user with specific permissions + create(:member, + project:, + principal: user, + roles: [create(:project_role, permissions:)]) + login_as(user) + end + + def render_component + render_inline(described_class.new(backlog:, project:, current_user: user)) + end + + describe "permission-based items" do + context "with :update_sprints permission" do + let(:permissions) { %i[view_master_backlog update_sprints] } + + it "shows Edit item with pencil icon" do + render_component + + expect(page).to have_css("action-menu") + expect(page).to have_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint")) + expect(page).to have_octicon(:pencil) + end + end + + context "without :update_sprints permission" do + let(:permissions) { [:view_master_backlog] } + + it "does not show Edit item" do + render_component + + expect(page).to have_no_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint")) + end + end + + context "with :add_work_packages permission" do + let(:permissions) { %i[view_master_backlog add_work_packages] } + + it "shows Add new story item with compose icon" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story")) + expect(page).to have_octicon(:compose) + end + end + + context "without :add_work_packages permission" do + let(:permissions) { [:view_master_backlog] } + + it "does not show Add new story item" do + render_component + + expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story")) + end + end + + context "with :manage_versions permission" do + let(:permissions) { %i[view_master_backlog manage_versions] } + + it "shows Properties item with gear icon" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties")) + expect(page).to have_octicon(:gear) + end + end + + context "without :manage_versions permission" do + let(:permissions) { [:view_master_backlog] } + + it "does not show Properties item" do + render_component + + expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties")) + end + end + + context "with :view_taskboards permission" do + let(:permissions) { %i[view_master_backlog view_taskboards] } + + it "shows Task board item" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.task_board")) + end + end + + context "without :view_taskboards permission" do + let(:permissions) { [:view_master_backlog] } + + it "does not show Task board item" do + render_component + + expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.task_board")) + end + end + end + + describe "always-visible items" do + let(:permissions) { [:view_master_backlog] } + + it "shows Stories/Tasks link" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.stories_tasks")) + end + + it "shows Burndown chart link" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.burndown_chart")) + end + + context "when sprint has no burndown (no dates)" do + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) } + + it "shows Burndown chart link as disabled" do + render_component + + burndown_item = page.find("li", text: I18n.t(:"backlogs.backlog_menu_component.action_menu.burndown_chart")) + expect(burndown_item[:class]).to include("ActionListItem--disabled") + end + end + + context "when sprint has burndown" do + it "shows Burndown chart link as enabled" do + render_component + + burndown_item = page.find("li", text: I18n.t(:"backlogs.backlog_menu_component.action_menu.burndown_chart")) + expect(burndown_item[:class]).not_to include("ActionListItem--disabled") + end + end + end + + describe "module-based items" do + context "when wiki module is enabled" do + let(:permissions) { [:view_master_backlog] } + let(:project) { create(:project, types: [type_feature, type_task], enabled_module_names: %w[backlogs wiki]) } + + it "shows Wiki item" do + render_component + + expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.wiki")) + expect(page).to have_octicon(:book) + end + end + + context "when wiki module is disabled" do + let(:permissions) { [:view_master_backlog] } + let(:project) { create(:project, types: [type_feature, type_task], enabled_module_names: %w[backlogs]) } + + it "does not show Wiki item" do + render_component + + expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.wiki")) + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/sprint_page_header_component_spec.rb b/modules/backlogs/spec/components/backlogs/sprint_page_header_component_spec.rb new file mode 100644 index 000000000000..34c50fd51e81 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/sprint_page_header_component_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::SprintPageHeaderComponent, type: :component do + let(:project) { create(:project, name: "Test Project") } + let(:start_date) { Date.new(2024, 1, 15) } + let(:effective_date) { Date.new(2024, 1, 29) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date:) } + + def render_component + render_inline(described_class.new(sprint:, project:)) + end + + describe "rendering" do + it "renders Primer::OpenProject::PageHeader" do + render_component + + expect(page).to have_css(".PageHeader") + end + + it "displays sprint name as title" do + render_component + + expect(page).to have_css(".PageHeader-title", text: "Sprint 1") + end + + it "shows date range in description with time tags" do + render_component + + expect(page).to have_css("time[datetime='2024-01-15']") + expect(page).to have_css("time[datetime='2024-01-29']") + end + + it "renders breadcrumbs" do + render_component + + expect(page).to have_css(".PageHeader-breadcrumbs") + end + + it "includes project link in breadcrumbs" do + render_component + + expect(page).to have_link("Test Project") + end + + it "includes backlogs link in breadcrumbs" do + render_component + + expect(page).to have_link(I18n.t(:label_backlogs)) + end + + it "includes sprint name as text (not link) in breadcrumbs" do + render_component + + # The last breadcrumb item should be the sprint name as plain text + breadcrumbs = page.find(".PageHeader-breadcrumbs") + expect(breadcrumbs).to have_text("Sprint 1") + end + end + + describe "date handling" do + context "when sprint has only start_date" do + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date: nil) } + + it "renders only start date" do + render_component + + expect(page).to have_css("time[datetime='2024-01-15']") + expect(page).to have_no_css("time[datetime='2024-01-29']") + end + end + + context "when sprint has only effective_date" do + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date:) } + + it "renders only effective date" do + render_component + + expect(page).to have_no_css("time[datetime='2024-01-15']") + expect(page).to have_css("time[datetime='2024-01-29']") + end + end + + context "when sprint has no dates" do + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) } + + it "renders no time elements" do + render_component + + expect(page).to have_no_css("time") + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/story_component_spec.rb b/modules/backlogs/spec/components/backlogs/story_component_spec.rb new file mode 100644 index 000000000000..5481e6bfcbf1 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/story_component_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::StoryComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:default_status) { create(:default_status) } + shared_let(:default_priority) { create(:default_priority) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project, types: [type_feature, type_task]) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) } + let(:story_points) { 5 } + let(:story) do + create(:story, + subject: "Test Story Subject", + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points:, + position: 1, + version: sprint) + end + let(:max_position) { 3 } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s) + end + + def render_component + render_inline(described_class.new(story:, sprint:, max_position:, current_user: user)) + end + + describe "rendering" do + it "renders Primer::OpenProject::DragHandle" do + render_component + + # DragHandle renders with grabber icon + expect(page).to have_octicon(:grabber) + end + + it "renders WorkPackages::InfoLineComponent" do + render_component + + # InfoLine renders type and ID info + expect(page).to have_text("FEATURE") + expect(page).to have_text("##{story.id}") + end + + it "shows story subject in semibold text" do + render_component + + expect(page).to have_text("Test Story Subject") + end + + it "shows story points" do + render_component + + expect(page).to have_text("5 points", normalize_ws: true) + end + + it "renders StoryMenuComponent" do + render_component + + expect(page).to have_css("action-menu") + end + end + + describe "story points handling" do + context "when story_points is nil" do + let(:story_points) { nil } + + it "shows 0 points" do + render_component + + expect(page).to have_text("0 points", normalize_ws: true) + end + end + + context "when story_points is 0" do + let(:story_points) { 0 } + + it "shows 0 points" do + render_component + + expect(page).to have_text("0 points", normalize_ws: true) + end + end + + context "when story_points is 1" do + let(:story_points) { 1 } + + it "shows 1 point (singular)" do + render_component + + expect(page).to have_text("1 point", normalize_ws: true) + end + end + end +end diff --git a/modules/backlogs/spec/components/backlogs/story_menu_component_spec.rb b/modules/backlogs/spec/components/backlogs/story_menu_component_spec.rb new file mode 100644 index 000000000000..b9ac8feaac45 --- /dev/null +++ b/modules/backlogs/spec/components/backlogs/story_menu_component_spec.rb @@ -0,0 +1,181 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Backlogs::StoryMenuComponent, type: :component do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:default_status) { create(:default_status) } + shared_let(:default_priority) { create(:default_priority) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project, types: [type_feature, type_task]) } + let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) } + let(:position) { 2 } + let(:max_position) { 3 } + let(:story) do + create(:story, + subject: "Test Story", + project:, + type: type_feature, + status: default_status, + priority: default_priority, + story_points: 5, + position:, + version: sprint) + end + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s) + end + + def render_component(position: 2, max_position: 3) + story.update!(position:) + render_inline(described_class.new(story:, sprint:, max_position:, current_user: user)) + end + + describe "standard items" do + it "shows Open details link (split view)" do + render_component + + expect(page).to have_text(I18n.t(:"js.button_open_details")) + expect(page).to have_octicon(:"op-view-split") + end + + it "shows Open fullscreen link (full page)" do + render_component + + expect(page).to have_text(I18n.t(:"js.button_open_fullscreen")) + expect(page).to have_octicon(:"screen-full") + end + + it "shows a divider before move options" do + render_component + + expect(page).to have_css(".ActionList-sectionDivider") + end + end + + describe "move menu items" do + it "shows Move to top item with move-to-top icon" do + render_component + + expect(page).to have_text(I18n.t(:label_sort_highest)) + expect(page).to have_octicon(:"move-to-top") + end + + it "shows Move up item with chevron-up icon" do + render_component + + expect(page).to have_text(I18n.t(:label_sort_higher)) + expect(page).to have_octicon(:"chevron-up") + end + + it "shows Move down item with chevron-down icon" do + render_component + + expect(page).to have_text(I18n.t(:label_sort_lower)) + expect(page).to have_octicon(:"chevron-down") + end + + it "shows Move to bottom item with move-to-bottom icon" do + render_component + + expect(page).to have_text(I18n.t(:label_sort_lowest)) + expect(page).to have_octicon(:"move-to-bottom") + end + end + + describe "position logic" do + context "when item is first (position=1)" do + it "hides Move to top and Move up" do + render_component(position: 1, max_position: 3) + + expect(page).to have_no_text(I18n.t(:label_sort_highest)) + expect(page).to have_no_text(I18n.t(:label_sort_higher)) + end + + it "shows Move down and Move to bottom" do + render_component(position: 1, max_position: 3) + + expect(page).to have_text(I18n.t(:label_sort_lower)) + expect(page).to have_text(I18n.t(:label_sort_lowest)) + end + end + + context "when item is last (position=max)" do + it "hides Move down and Move to bottom" do + render_component(position: 3, max_position: 3) + + expect(page).to have_no_text(I18n.t(:label_sort_lower)) + expect(page).to have_no_text(I18n.t(:label_sort_lowest)) + end + + it "shows Move to top and Move up" do + render_component(position: 3, max_position: 3) + + expect(page).to have_text(I18n.t(:label_sort_highest)) + expect(page).to have_text(I18n.t(:label_sort_higher)) + end + end + + context "when item is in the middle" do + it "shows all move options" do + render_component(position: 2, max_position: 3) + + expect(page).to have_text(I18n.t(:label_sort_highest)) + expect(page).to have_text(I18n.t(:label_sort_higher)) + expect(page).to have_text(I18n.t(:label_sort_lower)) + expect(page).to have_text(I18n.t(:label_sort_lowest)) + end + end + + context "when there is only one item (position=1, max=1)" do + it "hides all move options" do + render_component(position: 1, max_position: 1) + + expect(page).to have_no_text(I18n.t(:label_sort_highest)) + expect(page).to have_no_text(I18n.t(:label_sort_higher)) + expect(page).to have_no_text(I18n.t(:label_sort_lower)) + expect(page).to have_no_text(I18n.t(:label_sort_lowest)) + end + + it "hides the divider" do + render_component(position: 1, max_position: 1) + + expect(page).to have_no_css(".ActionList-sectionDivider") + end + end + end +end diff --git a/modules/backlogs/spec/controllers/rb_master_backlogs_controller_spec.rb b/modules/backlogs/spec/controllers/rb_master_backlogs_controller_spec.rb new file mode 100644 index 000000000000..3d1152148288 --- /dev/null +++ b/modules/backlogs/spec/controllers/rb_master_backlogs_controller_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe RbMasterBacklogsController do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project) } + let(:status) { create(:status, name: "status 1", is_default: true) } + let(:sprint) { create(:sprint, project:) } + let(:story) { create(:story, status:, version: sprint, project:) } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id }) + end + + describe "GET #index" do + it "is successful", :aggregate_failures do + get :index, params: { project_id: project.id } + + expect(response).to be_successful + expect(assigns(:project)).to eq(project) + expect(assigns(:owner_backlogs)).to be_an(Array) + expect(assigns(:sprint_backlogs)).to be_an(Array) + end + + context "with a Turbo Frame request" do + before { request.headers["Turbo-Frame"] = "backlogs_container" } + + it "renders the list partial", :aggregate_failures do + get :index, params: { project_id: project.id } + + expect(response).to be_successful + expect(assigns(:project)).to eq(project) + expect(assigns(:owner_backlogs)).to be_an(Array) + expect(assigns(:sprint_backlogs)).to be_an(Array) + end + end + end + + describe "GET #details" do + it "is successful", :aggregate_failures do + get :details, params: { + project_id: project.id, + tab: :overview, + work_package_id: story.id, + work_package_split_view: true + } + + expect(response).to be_successful + expect(assigns(:project)).to eq(project) + expect(assigns(:owner_backlogs)).to be_an(Array) + expect(assigns(:sprint_backlogs)).to be_an(Array) + end + + context "with a Turbo Frame request" do + before { request.headers["Turbo-Frame"] = "content-bodyRight" } + + it "renders the split view without loading backlogs", :aggregate_failures do + get :details, params: { + project_id: project.id, + tab: :overview, + work_package_id: story.id, + work_package_split_view: true + } + + expect(response).to be_successful + expect(assigns(:project)).to eq(project) + expect(assigns(:owner_backlogs)).to be_nil + expect(assigns(:sprint_backlogs)).to be_nil + end + end + end +end diff --git a/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb b/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb new file mode 100644 index 000000000000..5513fdaff5e7 --- /dev/null +++ b/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe RbSprintsController do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:visible_projects_scope) { instance_double(ActiveRecord::Relation) } + let(:visible_sprints_scope) { instance_double(ActiveRecord::Relation) } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id }) + + allow(Project) + .to receive(:visible) + .and_return(visible_projects_scope) + + allow(visible_projects_scope) + .to receive(:find) + .with(project.identifier) + .and_return(project) + + allow(Sprint) + .to receive(:visible) + .and_return(visible_sprints_scope) + + allow(visible_sprints_scope) + .to receive(:find) + .with(sprint.id.to_s) + .and_return(sprint) + end + + describe "GET #edit_name" do + let(:project) { build_stubbed(:project) } + let(:sprint) { build_stubbed(:sprint) } + + it "responds with success", :aggregate_failures do + get :edit_name, params: { project_id: project.identifier, id: sprint.id }, format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:backlog)).to be_a(Backlog) + end + end + + describe "GET #show_name" do + let(:project) { build_stubbed(:project) } + let(:sprint) { build_stubbed(:sprint) } + + it "responds with success", :aggregate_failures do + get :show_name, params: { project_id: project.identifier, id: sprint.id }, format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:backlog)).to be_a(Backlog) + end + end + + describe "PATCH #update" do + let(:project) { build_stubbed(:project) } + let(:sprint) { build_stubbed(:sprint) } + + before do + update_service = instance_double(Versions::UpdateService, call: service_result) + + allow(Versions::UpdateService) + .to receive(:new) + .with(user:, model: sprint) + .and_return(update_service) + end + + context "when service call succeeds" do + let(:service_result) { ServiceResult.success(result: sprint) } + + it "responds with success", :aggregate_failures do + patch :update, params: { project_id: project.identifier, id: sprint.id, sprint: { name: "Updated Sprint" } }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:backlog)).to be_a(Backlog) + end + end + + context "when service call fails" do + let(:service_result) { ServiceResult.failure(result: sprint) } + + before do + project.name = "" + end + + it "responds with 422", :aggregate_failures do + patch :update, params: { project_id: project.identifier, id: sprint.id, sprint: { name: "" } }, + format: :turbo_stream + + expect(response).not_to be_successful + expect(response).to have_http_status :unprocessable_entity + expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:backlog)).to be_a(Backlog) + end + end + end +end diff --git a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb new file mode 100644 index 000000000000..90ce9199be1b --- /dev/null +++ b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe RbStoriesController do + shared_let(:type_feature) { create(:type_feature) } + shared_let(:type_task) { create(:type_task) } + shared_let(:user) { create(:admin) } + current_user { user } + + let(:project) { create(:project) } + let(:status) { create(:status, name: "status 1", is_default: true) } + let(:sprint) { create(:sprint, project:) } + let(:story) { create(:story, status:, version: sprint, project:) } + + before do + allow(Setting) + .to receive(:plugin_openproject_backlogs) + .and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id }) + end + + describe "PUT #move" do + let(:other_sprint) { create(:sprint, name: "Sprint 2", project:) } + + it "responds with success", :aggregate_failures do + put :move, params: { + project_id: project.id, + sprint_id: sprint.id, + id: story.id, + target_id: other_sprint.id, + position: 1 + }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:story)).to eq(story) + expect(assigns(:backlog)).to be_a(Backlog) + end + + context "when service call fails" do + let(:service_result) { ServiceResult.failure(message: "Something went wrong") } + + before do + update_service = instance_double(Stories::UpdateService, call: service_result) + + allow(Stories::UpdateService) + .to receive(:new) + .and_return(update_service) + end + + it "renders an error flash", :aggregate_failures do + put :move, params: { + project_id: project.id, + sprint_id: sprint.id, + id: story.id, + target_id: other_sprint.id, + position: 1 + }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + end + end + end + + describe "POST #reorder" do + it "responds with success", :aggregate_failures do + post :reorder, params: { project_id: project.id, sprint_id: sprint.id, id: story.id, direction: "highest" }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_http_status :ok + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(assigns(:project)).to eq(project) + expect(assigns(:sprint)).to eq(sprint) + expect(assigns(:story)).to eq(story) + expect(assigns(:backlog)).to be_a(Backlog) + end + + context "when service call fails" do + let(:service_result) { ServiceResult.failure(message: "Something went wrong") } + + before do + update_service = instance_double(Stories::UpdateService, call: service_result) + + allow(Stories::UpdateService) + .to receive(:new) + .and_return(update_service) + end + + it "renders an error flash", :aggregate_failures do + post :reorder, params: { project_id: project.id, sprint_id: sprint.id, id: story.id, direction: "highest" }, + format: :turbo_stream + + expect(response).to be_successful + expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{sprint.id}" + expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component" + end + end + end +end diff --git a/modules/backlogs/spec/features/backlogs/change_status_spec.rb b/modules/backlogs/spec/features/backlogs/change_status_spec.rb deleted file mode 100644 index 028e3d04fe17..000000000000 --- a/modules/backlogs/spec/features/backlogs/change_status_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" -require_relative "../../support/pages/backlogs" - -RSpec.describe "Backlogs context menu", :js do - shared_let(:story_type) { create(:type_feature) } - shared_let(:task_type) { create(:type_task) } - shared_let(:project) { create(:project, types: [story_type, task_type]) } - shared_let(:role) do - create(:project_role, - permissions: %i[edit_work_packages - change_work_package_status - view_master_backlog - view_work_packages]) - end - - shared_let(:user) do - create(:user, - member_with_roles: { project => role }) - end - shared_let(:sprint) do - create(:version, - project:, - name: "Sprint") - end - shared_let(:new_status) { create(:default_status, name: "New") } - shared_let(:in_progress_status) { create(:status, name: "In progress") } - shared_let(:default_priority) { create(:default_priority) } - shared_let(:story) do - create(:work_package, - type: story_type, - project:, - status: new_status, - priority: default_priority, - story_points: 3, - version: sprint) - end - shared_let(:workflow) do - create(:workflow, - old_status: new_status, - new_status: in_progress_status, - role:, - type: story_type) - end - - let(:backlogs_page) { Pages::Backlogs.new(project) } - - before do - allow(Setting) - .to receive(:plugin_openproject_backlogs) - .and_return("story_types" => [story_type.id.to_s], - "task_type" => task_type.id.to_s) - login_as(user) - end - - def expect_fields(enabled: [], disabled: [], none: []) - enabled.each do |field| - expect(page).to have_field(WorkPackage.human_attribute_name(field)) - end - - disabled.each do |field| - expect(page).to have_field(WorkPackage.human_attribute_name(field), disabled: true) - end - - none.each do |field| - expect(page).to have_no_field(WorkPackage.human_attribute_name(field), visible: :all) - end - end - - # this test acts as a control for the other tests in this file as it's easy to - # expect a field to not be present, and have the test still pass when the - # field is renamed. - context "when the user has edit_work_packages permission" do - it "is possible to edit any story field" do - backlogs_page.visit! - backlogs_page.enter_edit_story_mode(story) - - expect_fields(enabled: %i[type subject status story_points]) - - backlogs_page.alter_attributes_in_edit_story_mode(story, subject: "Hello subject") - backlogs_page.save_story_from_edit_mode(story) - - expect(story.reload.subject).to eq("Hello subject") - end - end - - context "when the user has only change_work_package_status permission" do - before do - RolePermission.where(permission: "edit_work_packages").delete_all - end - - it "is only possible to edit status field of stories" do - backlogs_page.visit! - backlogs_page.enter_edit_story_mode(story, text: story.status.name) - - expect_fields(enabled: %i[status], disabled: %i[type subject story_points]) - - backlogs_page.alter_attributes_in_edit_story_mode(story, status: in_progress_status.name) - backlogs_page.save_story_from_edit_mode(story) - - expect(story.reload.status).to eq(in_progress_status) - end - end - - context "when the user has neither change_work_package_status nor edit_work_packages permission" do - before do - RolePermission.where(permission: ["change_work_package_status", "edit_work_packages"]).delete_all - end - - it "is not possible to edit any story field" do - backlogs_page.visit! - backlogs_page.enter_edit_story_mode(story) - - expect_fields(none: %i[type subject status story_points]) - end - end -end diff --git a/modules/backlogs/spec/features/backlogs/context_menu_spec.rb b/modules/backlogs/spec/features/backlogs/context_menu_spec.rb index e64d2d59ed87..540699b5aea3 100644 --- a/modules/backlogs/spec/features/backlogs/context_menu_spec.rb +++ b/modules/backlogs/spec/features/backlogs/context_menu_spec.rb @@ -80,11 +80,12 @@ def within_backlog_context_menu(&) context "when the backlog is a sprint backlog (displayed on the left, the default)" do it "displays all menu entries" do within_backlog_context_menu do |menu| - expect(menu).to have_link I18n.t("backlogs.add_new_story") - expect(menu).to have_link I18n.t("label_stories_tasks") - expect(menu).to have_link I18n.t("label_task_board") - expect(menu).to have_link I18n.t("backlogs.show_burndown_chart") - expect(menu).to have_link I18n.t("label_wiki") + expect(menu).to have_selector :menuitem, count: 5 + expect(menu).to have_selector :menuitem, "New story" + expect(menu).to have_selector :menuitem, "Stories/Tasks" + expect(menu).to have_selector :menuitem, "Task board" + expect(menu).to have_selector :menuitem, "Burndown chart" + expect(menu).to have_selector :menuitem, "Wiki" end end end @@ -97,13 +98,14 @@ def within_backlog_context_menu(&) display: VersionSetting::DISPLAY_RIGHT) end - it 'only displays the "New story" and "Stories/Tasks" menu entries' do + it "only displays 2 menu entries" do within_backlog_context_menu do |menu| - expect(menu).to have_link I18n.t("backlogs.add_new_story") - expect(menu).to have_link I18n.t("label_stories_tasks") - expect(menu).to have_no_link I18n.t("label_task_board") - expect(menu).to have_no_link I18n.t("backlogs.show_burndown_chart") - expect(menu).to have_no_link I18n.t("label_wiki") + expect(menu).to have_selector :menuitem, count: 2 + expect(menu).to have_selector :menuitem, "New story" + expect(menu).to have_selector :menuitem, "Stories/Tasks" + expect(menu).to have_no_selector :menuitem, "Task board" + expect(menu).to have_no_selector :menuitem, "Burndown chart" + expect(menu).to have_no_selector :menuitem, "Wiki" end end end @@ -113,9 +115,9 @@ def within_backlog_context_menu(&) sprint.update(start_date: nil) end - it 'does not display the "Burndown chart" menu entry' do + it 'disables the "Burndown chart" menu entry' do within_backlog_context_menu do |menu| - expect(menu).to have_no_link I18n.t("backlogs.show_burndown_chart") + expect(menu).to have_selector :menuitem, "Burndown chart", disabled: true end end end @@ -125,9 +127,9 @@ def within_backlog_context_menu(&) sprint.update(effective_date: nil) end - it 'does not display the "Burndown chart" menu entry' do + it 'disables the "Burndown chart" menu entry' do within_backlog_context_menu do |menu| - expect(menu).to have_no_link I18n.t("backlogs.show_burndown_chart") + expect(menu).to have_selector :menuitem, "Burndown chart", disabled: true end end end @@ -139,7 +141,7 @@ def within_backlog_context_menu(&) it 'does not display the "New story" menu entry' do within_backlog_context_menu do |menu| - expect(menu).to have_no_link I18n.t("backlogs.add_new_story") + expect(menu).to have_no_selector :menuitem, "New story" end end end @@ -151,7 +153,7 @@ def within_backlog_context_menu(&) it 'does not display the "Task board" menu entry' do within_backlog_context_menu do |menu| - expect(menu).to have_no_link I18n.t("label_task_board") + expect(menu).to have_no_selector :menuitem, "Task board" end end end @@ -163,7 +165,7 @@ def within_backlog_context_menu(&) it 'does not display the "Wiki" menu entry' do within_backlog_context_menu do |menu| - expect(menu).to have_no_link I18n.t("label_wiki") + expect(menu).to have_no_selector :menuitem, "Wiki" end end end diff --git a/modules/backlogs/spec/features/backlogs/create_story_spec.rb b/modules/backlogs/spec/features/backlogs/create_story_spec.rb index abb3f9620c89..c2d8928e4f2a 100644 --- a/modules/backlogs/spec/features/backlogs/create_story_spec.rb +++ b/modules/backlogs/spec/features/backlogs/create_story_spec.rb @@ -27,6 +27,7 @@ #++ require "spec_helper" +require_relative "../../support/pages/backlogs" RSpec.describe "Backlogs", :js, :selenium, driver: :firefox_de do # using FF due to regression #64158 let(:story_type) do @@ -88,6 +89,8 @@ create(:default_priority) end + let(:backlogs_page) { Pages::Backlogs.new(project) } + before do login_as(user) @@ -100,32 +103,36 @@ end it "allows creating a new story" do - visit backlogs_project_backlogs_path(project) + backlogs_page.visit! + + backlogs_page.click_in_backlog_menu(backlog_version, "New story") - within("#backlog_#{backlog_version.id}", wait: 10) do - menu = find(".backlog-menu") - menu.click - click_link "New Story" + within_dialog "New work package" do fill_in "Subject", with: "The new story" - fill_in "Story Points", with: "5" + # TODO: removed in OP #57688, to be reimplemented + # fill_in "Story Points", with: "5" - # inactive types should not be selectable - # but the user can choose from the active types - expect(page) - .to have_no_css("option", text: inactive_story_type.name) + # inactive types should not be selectable but the user can choose from the + # active types + # TODO: removed in OP #57688, to be reimplemented + # expect(page).to have_no_css("option", text: inactive_story_type.name) - select story_type2.name, from: "Type" + select_combo_box_option story_type2.name, from: "Type" # saving the new story - find(:css, "input[name=subject]").native.send_key :return + click_on "Create" + end - # velocity should be summed up immediately - expect(page) - .to have_css(".velocity", text: "12") + expect_and_dismiss_flash type: :success, message: "New work package created" - # this will ensure that the page refresh is through before we check the order - menu.click - click_link "New Story" + # velocity should be summed up immediately + # TODO: removed in OP #57688, to be reimplemented + # xpect(page).to have_css(".velocity", text: "12") + + # this will ensure that the page refresh is through before we check the order + backlogs_page.click_in_backlog_menu(backlog_version, "New story") + + within_dialog "New work package" do fill_in "Subject", with: "Another story" end @@ -135,15 +142,15 @@ expect(page) .to have_no_content "Another story" - expect(page) - .to have_css ".story:nth-of-type(1)", text: "The new story" - expect(page) - .to have_css ".story:nth-of-type(2)", text: existing_story1.subject - expect(page) - .to have_css ".story:nth-of-type(3)", text: existing_story2.subject + new_story = WorkPackage.find_by(subject: "The new story") - # created with the selected type - expect(page) - .to have_css ".story:nth-of-type(1) .type_id", text: story_type2.name + # stories are ordered by position (ASC), with NULL positions at the end ordered by ID + # existing stories have positions 1 and 2, new story has no position so appears at end + backlogs_page.expect_stories_in_order(backlog_version, existing_story1, existing_story2, new_story) + + # created with the selected type (HighlightedTypeComponent renders type name in uppercase) + within("#story_#{new_story.id}") do + expect(page).to have_text(story_type2.name.upcase) + end end end diff --git a/modules/backlogs/spec/features/empty_backlogs_spec.rb b/modules/backlogs/spec/features/empty_backlogs_spec.rb index b4cf24b380f9..b558b1914aca 100644 --- a/modules/backlogs/spec/features/empty_backlogs_spec.rb +++ b/modules/backlogs/spec/features/empty_backlogs_spec.rb @@ -48,12 +48,11 @@ context "as admin" do let(:current_user) { create(:admin) } - it "shows a no results box with action" do - expect(page).to have_css(".generic-table--no-results-container", text: I18n.t(:backlogs_empty_title)) - expect(page).to have_css(".generic-table--no-results-description", text: I18n.t(:backlogs_empty_action_text)) - - link = page.find ".generic-table--no-results-description a" - expect(link[:href]).to include(new_project_version_path(project)) + it "shows blankslate with description" do + within ".blankslate" do + expect(page).to have_heading(I18n.t(:backlogs_empty_title)) + expect(page).to have_text(I18n.t(:backlogs_empty_action_text)) + end end end @@ -61,9 +60,11 @@ let(:role) { create(:project_role, permissions: %i(view_master_backlog)) } let(:current_user) { create(:user, member_with_roles: { project => role }) } - it "only shows a no results box" do - expect(page).to have_css(".generic-table--no-results-container", text: I18n.t(:backlogs_empty_title)) - expect(page).to have_no_css(".generic-table--no-results-description") + it "shows a blankslate without description" do + within ".blankslate" do + expect(page).to have_heading(I18n.t(:backlogs_empty_title)) + expect(page).to have_no_text(I18n.t(:backlogs_empty_action_text)) + end end end end diff --git a/modules/backlogs/spec/features/stories_in_backlog_spec.rb b/modules/backlogs/spec/features/stories_in_backlog_spec.rb index 26ee04562ecf..41e433b11910 100644 --- a/modules/backlogs/spec/features/stories_in_backlog_spec.rb +++ b/modules/backlogs/spec/features/stories_in_backlog_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -37,7 +39,7 @@ enabled_module_names: %w(work_package_tracking backlogs)) end let!(:story) { create(:type_feature) } - let!(:other_story) { create(:type) } + let!(:other_story) { create(:type, name: "Story") } let!(:task) { create(:type_task) } let!(:priority) { create(:default_priority) } let!(:default_status) { create(:status, is_default: true) } @@ -69,7 +71,7 @@ status: default_status, version: sprint, position: 1, - story_points: 10) + story_points: 8) end let!(:sprint_story1_task) do create(:work_package, @@ -92,7 +94,7 @@ status: default_status, version: sprint, position: 2, - story_points: 20) + story_points: 13) end let!(:backlog_story1) do create(:work_package, @@ -104,8 +106,8 @@ let!(:sprint) do create(:version, project:, - start_date: Date.today - 10.days, - effective_date: Date.today + 10.days, + start_date: Time.zone.today - 10.days, + effective_date: Time.zone.today + 10.days, version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_LEFT }]) end let!(:backlog) do @@ -127,7 +129,7 @@ type: story, status: default_status, version: sprint, - story_points: 10) + story_points: 5) end let(:backlogs_page) { Pages::Backlogs.new(project) } @@ -139,7 +141,7 @@ "task_type" => task.id.to_s) end - it "displays stories which are editable" do + it "displays stories which are editable via details view" do backlogs_page.visit! # All stories are visible in their sprint/backlog @@ -166,164 +168,16 @@ .expect_stories_in_order(sprint, sprint_story1, sprint_story2) # Velocity is calculated by summing up all story points in a sprint - backlogs_page - .expect_velocity(sprint, 30) - - SeleniumHubWaiter.wait - # Creating a story - backlogs_page - .click_in_backlog_menu(sprint, "New Story") - - SeleniumHubWaiter.wait - backlogs_page - .edit_new_story(subject: "New story", - story_points: 10) - - new_story = nil - retry_block do - new_story = WorkPackage.find_by(subject: "New story") - raise "Expected story" unless new_story - end - - backlogs_page - .expect_story_in_sprint(new_story, sprint) - - # All positions will be unique in the sprint - expect(Story.where(version: sprint, type: story, project:).pluck(:position)) - .to contain_exactly(1, 2, 3) - - backlogs_page - .expect_stories_in_order(sprint, new_story, sprint_story1, sprint_story2) - - # Creating the story will update the velocity - backlogs_page - .expect_velocity(sprint, 40) - - # Editing in a sprint - - SeleniumHubWaiter.wait - backlogs_page - .edit_story(sprint_story1, - subject: "Altered story1", - story_points: 15) - - retry_block do - sprint_story1.reload - raise "Expected story to be renamed" unless sprint_story1.subject == "Altered story1" - end - - backlogs_page - .expect_for_story(sprint_story1, subject: "Altered story1") - - # Updating the story_points of a story will update the velocity of the sprint - backlogs_page - .expect_velocity(sprint, 45) - - SeleniumHubWaiter.wait - # Moving a story to top - backlogs_page - .drag_in_sprint(sprint_story1, new_story) - - backlogs_page - .expect_stories_in_order(sprint, sprint_story1, new_story, sprint_story2) - - expect(Story.where(version: sprint, type: story, project:).pluck(:position)) - .to contain_exactly(1, 2, 3) - - # Moving a story to bottom - backlogs_page - .drag_in_sprint(sprint_story1, sprint_story2, before: false) - - backlogs_page - .expect_stories_in_order(sprint, new_story, sprint_story2, sprint_story1) - - expect(Story.where(version: sprint, type: story, project:).pluck(:position)) - .to contain_exactly(1, 2, 3) - - # Moving a story to from the backlog to the sprint (3rd position) - - SeleniumHubWaiter.wait - backlogs_page - .drag_in_sprint(backlog_story1, sprint_story2, before: false) - - backlogs_page - .expect_stories_in_order(sprint, new_story, sprint_story2, backlog_story1, sprint_story1) - - expect(Story.where(version: sprint, type: story, project:).pluck(:position)) - .to contain_exactly(1, 2, 3, 4) - - # Available statuses when editing - - backlogs_page - .enter_edit_story_mode(backlog_story1) - - # The available statuses include those available by the workflow: - # Current and every reachable one - backlogs_page - .expect_status_options(backlog_story1, - [default_status, other_status]) - - SeleniumHubWaiter.wait - backlogs_page - .alter_attributes_in_edit_story_mode(backlog_story1, - subject: "Altered backlog story1", - status: other_status.name) - backlogs_page - .save_story_from_edit_mode(backlog_story1) - - retry_block do - backlog_story1.reload - raise "Expected story to be renamed" unless backlog_story1.subject == "Altered backlog story1" - end - - expect(backlog_story1.status) - .to eql other_status - - backlogs_page - .expect_for_story(backlog_story1, - subject: "Altered backlog story1", - status: other_status.name) - - SeleniumHubWaiter.wait - backlogs_page - .enter_edit_story_mode(backlog_story1) - - # Since we switched to other status, only the current status and the next one is available now. - backlogs_page - .expect_status_options(backlog_story1, - [other_status]) - - # Available statuses when editing and switching the type - backlogs_page - .alter_attributes_in_edit_story_mode(backlog_story1, - type: other_story) - # This will result in an error as the current status is not available - backlogs_page - .save_story_from_edit_mode(backlog_story1) + backlogs_page.expect_velocity(sprint, 21) backlogs_page - .expect_for_story(backlog_story1, - subject: "Altered backlog story1", - status: default_status.name, - type: other_story.name) + .edit_story_in_details_view(sprint_story1, story_points: 5) - # Clicking would lead to having the burndown chart opened in another tab - # which seems hard to test with selenium. - backlogs_page - .expect_in_backlog_menu(sprint, "Burndown Chart") - - # One can switch to the work package page by clicking on the id - # Clicking on it will open the wp in another tab which seems to trip up selenium. - backlogs_page - .expect_story_link_to_wp_page(sprint_story1) + backlogs_page.expect_velocity(sprint, 18) - # Go to the index page of work packages within that sprint via the menu backlogs_page - .click_in_backlog_menu(sprint, "Stories/Tasks") - - wp_table = Pages::WorkPackagesTable.new(project) + .edit_story_in_details_view(sprint_story2, subject: "Updated story", story_points: 3) - wp_table - .expect_work_package_listed(new_story, sprint_story2, backlog_story1, sprint_story1) + backlogs_page.expect_velocity(sprint, 8) end end diff --git a/modules/backlogs/spec/helpers/rb_common_helper_spec.rb b/modules/backlogs/spec/helpers/rb_common_helper_spec.rb new file mode 100644 index 000000000000..7e9b2ffe23bd --- /dev/null +++ b/modules/backlogs/spec/helpers/rb_common_helper_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe RbCommonHelper do + describe "#format_date_range" do + let(:from) { Date.new(2025, 1, 6) } + let(:to) { Date.new(2025, 1, 17) } + + context "with an Array" do + it "renders both dates separated by an en-dash" do + expected = + "" \ + "\u00A0\u2013\u00A0" \ + "" + + expect(helper.format_date_range([from, to])).to be_html_eql(expected) + end + end + + context "when both dates are nil" do + it "returns nil" do + expect(helper.format_date_range([nil, nil])).to be_nil + end + end + + context "when only the start date is present" do + it "renders the start date with an en-dash" do + expected = + "" \ + "\u00A0\u2013\u00A0" + + expect(helper.format_date_range([from, nil])).to be_html_eql(expected) + end + end + + context "when only the end date is present" do + it "renders the end date with an en-dash" do + expected = + "\u00A0\u2013\u00A0" \ + "" + + expect(helper.format_date_range([nil, to])).to be_html_eql(expected) + end + end + end +end diff --git a/modules/backlogs/spec/models/backlog_spec.rb b/modules/backlogs/spec/models/backlog_spec.rb index c7fa23008393..8e3c4d2f75cc 100644 --- a/modules/backlogs/spec/models/backlog_spec.rb +++ b/modules/backlogs/spec/models/backlog_spec.rb @@ -53,4 +53,19 @@ end end end + + describe "ActiveModel naming" do + let(:sprint) { build_stubbed(:sprint) } + + subject(:instance) { described_class.new(sprint:, stories: []) } + + it "exposes an ActiveModel model_name" do + expect(described_class).to respond_to(:model_name) + expect(described_class.model_name).to respond_to(:param_key) + end + + it "implements #to_key" do + expect(instance).to respond_to(:to_key) + end + end end diff --git a/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb b/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb index bf96ed71ff03..4ee4440bccce 100644 --- a/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_master_backlogs_routing_spec.rb @@ -35,5 +35,16 @@ action: "index", project_id: "project_42") } + + it { + expect(get("/projects/project_42/backlogs/details/33")).to route_to( + controller: "rb_master_backlogs", + action: "details", + project_id: "project_42", + work_package_id: "33", + tab: :overview, + work_package_split_view: true + ) + } end end diff --git a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb b/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb index 03f8f518e3da..1390a902423f 100644 --- a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb @@ -30,6 +30,24 @@ RSpec.describe RbSprintsController do describe "routing" do + it { + expect(get("/projects/project_42/sprints/21/edit_name")).to route_to( + controller: "rb_sprints", + action: "edit_name", + project_id: "project_42", + id: "21" + ) + } + + it { + expect(get("/projects/project_42/sprints/21/show_name")).to route_to( + controller: "rb_sprints", + action: "show_name", + project_id: "project_42", + id: "21" + ) + } + it { expect(put("/projects/project_42/sprints/21")).to route_to(controller: "rb_sprints", action: "update", diff --git a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb index 14d137d7d6c5..d0af15bece99 100644 --- a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb @@ -31,18 +31,23 @@ RSpec.describe RbStoriesController do describe "routing" do it { - expect(post("/projects/project_42/sprints/21/stories")).to route_to(controller: "rb_stories", - action: "create", - project_id: "project_42", - sprint_id: "21") + expect(put("/projects/project_42/sprints/21/stories/85/move")).to route_to( + controller: "rb_stories", + action: "move", + project_id: "project_42", + sprint_id: "21", + id: "85" + ) } it { - expect(put("/projects/project_42/sprints/21/stories/85")).to route_to(controller: "rb_stories", - action: "update", - project_id: "project_42", - sprint_id: "21", - id: "85") + expect(post("/projects/project_42/sprints/21/stories/85/reorder")).to route_to( + controller: "rb_stories", + action: "reorder", + project_id: "project_42", + sprint_id: "21", + id: "85" + ) } end end diff --git a/modules/backlogs/spec/support/pages/backlogs.rb b/modules/backlogs/spec/support/pages/backlogs.rb index 7e8fe177624e..0674fe8398d7 100644 --- a/modules/backlogs/spec/support/pages/backlogs.rb +++ b/modules/backlogs/spec/support/pages/backlogs.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -37,51 +39,34 @@ def initialize(project) @project = project end - def enter_edit_story_mode(story, text: nil) - text ||= story.subject - within_story(story) do - find(:css, ".editable", text:).click - end - end - def enter_edit_backlog_mode(backlog) - within_backlog(backlog) do - find(".start_date.editable").click + within_backlog_menu(backlog) do |menu| + menu.find(:menuitem, "Edit sprint").click end end - def alter_attributes_in_edit_story_mode(story, attributes) - edit_proc = ->(*) do + def alter_attributes_in_details_view(story, **attributes) + within_details_view(story) do |details_view| attributes.each do |key, value| - field_name = WorkPackage.human_attribute_name(key) - case key - when :subject, :story_points - fill_in field_name, with: value.to_s - when :status, :type - select value.to_s, from: field_name - else - raise NotImplementedError - end - end - end + details_view + .edit_field(key.to_s.camelize(:lower)) + .update(value) # rubocop:disable Rails/SaveBang - if story - within_story(story, &edit_proc) - else - edit_proc.call + details_view.expect_and_dismiss_toaster message: "Successful update." + end end end - def alter_attributes_in_edit_backlog_mode(backlog, attributes) + def alter_attributes_in_edit_backlog_mode(backlog, **attributes) within_backlog(backlog) do attributes.each do |key, value| case key when :name - find("input[name=name]").set value + fill_in "Name", with: value when :start_date - find("input[name=start_date]").set value + fill_in "Start date", with: value when :effective_date - find("input[name=effective_date]").set value + fill_in "Finish date", with: value else raise NotImplementedError end @@ -89,64 +74,37 @@ def alter_attributes_in_edit_backlog_mode(backlog, attributes) end end - def save_story_from_edit_mode(story) - save_proc = ->(*) do - field = find_field(disabled: false, match: :first) - keys = [:return] - keys << :return if field.tag_name == "select" # select field needs a second return key sent for some reason - field.send_keys(*keys) - - expect(page).to have_no_field(WorkPackage.human_attribute_name(:subject)) - end - - if story - within_story(story, &save_proc) - else - save_proc.call - end - wait_for_save_completion - end - def save_backlog_from_edit_mode(backlog) within_backlog(backlog) do - find("input[name=name]").native.send_key :return - - expect(page) - .to have_css(".start_date.editable") + find_field("Name").send_keys :return end end - def wait_for_save_completion - expect(page).to have_no_css(".ajax-indicator") - end - - def edit_backlog(backlog, attributes) + def edit_backlog(backlog, **attributes) enter_edit_backlog_mode(backlog) - alter_attributes_in_edit_backlog_mode(backlog, attributes) + alter_attributes_in_edit_backlog_mode(backlog, **attributes) save_backlog_from_edit_mode(backlog) end - def edit_story(story, attributes) - enter_edit_story_mode(story) + def edit_story_in_details_view(story, **attributes) + click_in_story_menu(story, "Open details view") - alter_attributes_in_edit_story_mode(story, attributes) + expect(page).to have_current_path details_backlogs_project_backlogs_path(story.project, story) - save_story_from_edit_mode(story) + alter_attributes_in_details_view(story, **attributes) end - def edit_new_story(attributes) - within(".story.editing") do - alter_attributes_in_edit_story_mode(nil, attributes) - - save_story_from_edit_mode(nil) + def click_in_backlog_menu(backlog, item_name) + within_backlog_menu(backlog) do |menu| + menu.find(:menuitem, text: item_name).click end end - def click_in_backlog_menu(backlog, item_name) - within_backlog_menu(backlog) do |menu| - menu.find(".item", text: item_name).click + def click_in_story_menu(story, item_name) + within_story_menu(story) do |menu| + menu.find(:menuitem, text: item_name).click end end @@ -155,12 +113,11 @@ def drag_in_sprint(moved, target, before: true) target_element = find(story_selector(target)) drag_n_drop_element from: moved_element, to: target_element, offset_x: 0, offset_y: before ? -5 : +10 - wait_for_save_completion end def fold_backlog(backlog) within_backlog(backlog) do - find(".toggler").click + find(:button, accessible_name: "Collapse/Expand #{backlog.name}").click end end @@ -188,40 +145,6 @@ def expect_story_not_in_sprint(story, sprint) end end - def expect_for_story(story, attributes) - within_story(story) do - attributes.each do |key, value| - case key - when :subject - expect(page) - .to have_css("div.subject", text: value) - when :status - expect(page) - .to have_css("div.status_id", text: value) - when :type - expect(page) - .to have_css("div.type_id", text: value) - else - raise NotImplementedError - end - end - end - end - - def expect_story_link_to_wp_page(story) - within_story(story) do - expect(page) - .to have_link(story.to_param, href: work_package_path(story)) - end - end - - def expect_status_options(story, statuses) - within_story(story) do - expect(all(".status_id option").map { |n| n.text.strip }) - .to match_array(statuses.map(&:name)) - end - end - def expect_velocity(backlog, velocity) within("#backlog_#{backlog.id} .velocity") do expect(page) @@ -239,25 +162,10 @@ def expect_stories_in_order(backlog, *stories) end end - def expect_in_backlog_menu(backlog, item_name) - within_backlog(backlog) do - find(".header .menu-trigger").click - - expect(page) - .to have_css(".header .backlog-menu .item", text: item_name) - - # Close it again for next test - find(".header .menu-trigger").click - end - end - def expect_and_dismiss_error(message) - within ".ui-dialog" do - expect(page) - .to have_content message + expect(page).to have_content message - click_button("OK") - end + click_on "Cancel" end def path @@ -266,13 +174,28 @@ def path def within_backlog_menu(backlog, &) within_backlog(backlog) do - menu = find(".backlog-menu") - menu.click + find(:button, accessible_name: "Backlog actions").click - yield menu + within(:menu, &) end end + def within_story_menu(story, &) + within_story(story) do + find(:button, accessible_name: "Story actions").click + + within(:menu, &) + end + end + + def within_details_view(story, &) + details_view = Pages::PrimerizedSplitWorkPackage.new(story) + details_view.expect_tab :overview + details_view.expect_subject + + yield details_view + end + private def within_story(story, &) diff --git a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb b/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb index d7e09fa8f8cc..a950505fdf6e 100644 --- a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb +++ b/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb @@ -117,7 +117,7 @@ render expect(view).to render_template(partial: "_burndown", count: 0) - expect(rendered).to include(I18n.t("backlogs.no_burndown_data")) + expect(rendered).to include(I18n.t("rb_burndown_charts.show.blankslate_title")) end end end diff --git a/modules/backlogs/spec/views/shared/not_configured_spec.rb b/modules/backlogs/spec/views/shared/not_configured_spec.rb index f3fa729b26d0..57d240e138ff 100644 --- a/modules/backlogs/spec/views/shared/not_configured_spec.rb +++ b/modules/backlogs/spec/views/shared/not_configured_spec.rb @@ -29,6 +29,8 @@ require "spec_helper" RSpec.describe "shared/not_configured" do + before { assign(:project, create(:project)) } + it "renders without errors" do render end